Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::AgentSummaryBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:user1) { create(:user, account: account, role: :agent) }
|
||||
let(:user2) { create(:user, account: account, role: :agent) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
business_hours: business_hours,
|
||||
since: 1.week.ago.beginning_of_day,
|
||||
until: Time.current.end_of_day
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
context 'when there is team data' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, assignee: user1, created_at: Time.current)
|
||||
c2 = create(:conversation, account: account, assignee: user2, created_at: Time.current)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c2,
|
||||
user: user2,
|
||||
name: 'conversation_resolved',
|
||||
value: 50,
|
||||
value_in_business_hours: 40,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
user: user1,
|
||||
name: 'first_response',
|
||||
value: 20,
|
||||
value_in_business_hours: 10,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
user: user1,
|
||||
name: 'reply_time',
|
||||
value: 30,
|
||||
value_in_business_hours: 15,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
user: user1,
|
||||
name: 'reply_time',
|
||||
value: 40,
|
||||
value_in_business_hours: 25,
|
||||
created_at: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
context 'when business hours is disabled' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns the correct team stats' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to eq(
|
||||
[
|
||||
{
|
||||
id: user1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 20.0,
|
||||
avg_reply_time: 35.0
|
||||
},
|
||||
{
|
||||
id: user2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 50.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is enabled' do
|
||||
let(:business_hours) { true }
|
||||
|
||||
it 'uses business hours values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to eq(
|
||||
[
|
||||
{
|
||||
id: user1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 10.0,
|
||||
avg_reply_time: 20.0
|
||||
},
|
||||
{
|
||||
id: user2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 40.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no team data' do
|
||||
let!(:new_user) { create(:user, account: account, role: :agent) }
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns zero values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to include(
|
||||
{
|
||||
id: new_user.id,
|
||||
conversations_count: 0,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::BotMetricsBuilder do
|
||||
subject(:bot_metrics_builder) { described_class.new(inbox.account, params) }
|
||||
|
||||
let(:inbox) { create(:inbox) }
|
||||
let!(:resolved_conversation) { create(:conversation, account: inbox.account, inbox: inbox, created_at: 2.days.ago) }
|
||||
let!(:unresolved_conversation) { create(:conversation, account: inbox.account, inbox: inbox, created_at: 2.days.ago) }
|
||||
let(:since) { 1.week.ago.to_i.to_s }
|
||||
let(:until_time) { Time.now.to_i.to_s }
|
||||
let(:params) { { since: since, until: until_time } }
|
||||
|
||||
before do
|
||||
create(:agent_bot_inbox, inbox: inbox)
|
||||
create(:message, account: inbox.account, conversation: resolved_conversation, created_at: 2.days.ago, message_type: 'outgoing')
|
||||
create(:reporting_event, account_id: inbox.account.id, name: 'conversation_bot_resolved', conversation_id: resolved_conversation.id,
|
||||
created_at: 2.days.ago)
|
||||
create(:reporting_event, account_id: inbox.account.id, name: 'conversation_bot_handoff',
|
||||
conversation_id: resolved_conversation.id, created_at: 2.days.ago)
|
||||
create(:reporting_event, account_id: inbox.account.id, name: 'conversation_bot_handoff',
|
||||
conversation_id: unresolved_conversation.id, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
describe '#metrics' do
|
||||
context 'with valid params' do
|
||||
it 'returns correct metrics' do
|
||||
metrics = bot_metrics_builder.metrics
|
||||
|
||||
expect(metrics[:conversation_count]).to eq(2)
|
||||
expect(metrics[:message_count]).to eq(1)
|
||||
expect(metrics[:resolution_rate]).to eq(50)
|
||||
expect(metrics[:handoff_rate]).to eq(100)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with missing params' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'handles missing since and until params gracefully' do
|
||||
expect { bot_metrics_builder.metrics }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::ChannelSummaryBuilder do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:web_widget_inbox) { create(:inbox, account: account) }
|
||||
let!(:email_inbox) { create(:inbox, :with_email, account: account) }
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day,
|
||||
until: Time.current.end_of_day
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
subject(:report) { builder.build }
|
||||
|
||||
context 'when there are conversations with different statuses across channels' do
|
||||
before do
|
||||
# Web widget conversations
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :open, created_at: 2.days.ago)
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :open, created_at: 3.days.ago)
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :resolved, created_at: 2.days.ago)
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :pending, created_at: 1.day.ago)
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :snoozed, created_at: 1.day.ago)
|
||||
|
||||
# Email conversations
|
||||
create(:conversation, account: account, inbox: email_inbox, status: :open, created_at: 2.days.ago)
|
||||
create(:conversation, account: account, inbox: email_inbox, status: :resolved, created_at: 1.day.ago)
|
||||
create(:conversation, account: account, inbox: email_inbox, status: :resolved, created_at: 3.days.ago)
|
||||
end
|
||||
|
||||
it 'returns correct counts grouped by channel type' do
|
||||
expect(report['Channel::WebWidget']).to eq(
|
||||
open: 2,
|
||||
resolved: 1,
|
||||
pending: 1,
|
||||
snoozed: 1,
|
||||
total: 5
|
||||
)
|
||||
|
||||
expect(report['Channel::Email']).to eq(
|
||||
open: 1,
|
||||
resolved: 2,
|
||||
pending: 0,
|
||||
snoozed: 0,
|
||||
total: 3
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversations are outside the date range' do
|
||||
before do
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :open, created_at: 2.days.ago)
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :resolved, created_at: 2.weeks.ago)
|
||||
end
|
||||
|
||||
it 'only includes conversations within the date range' do
|
||||
expect(report['Channel::WebWidget']).to eq(
|
||||
open: 1,
|
||||
resolved: 0,
|
||||
pending: 0,
|
||||
snoozed: 0,
|
||||
total: 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no conversations' do
|
||||
it 'returns an empty hash' do
|
||||
expect(report).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a channel has only one status type' do
|
||||
before do
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :resolved, created_at: 1.day.ago)
|
||||
create(:conversation, account: account, inbox: web_widget_inbox, status: :resolved, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'returns zeros for other statuses' do
|
||||
expect(report['Channel::WebWidget']).to eq(
|
||||
open: 0,
|
||||
resolved: 2,
|
||||
pending: 0,
|
||||
snoozed: 0,
|
||||
total: 2
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::Conversations::MetricBuilder, type: :model do
|
||||
subject { described_class.new(account, params) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:params) { { since: '2023-01-01', until: '2024-01-01' } }
|
||||
let(:count_builder_instance) { instance_double(V2::Reports::Timeseries::CountReportBuilder, aggregate_value: 42) }
|
||||
let(:avg_builder_instance) { instance_double(V2::Reports::Timeseries::AverageReportBuilder, aggregate_value: 42) }
|
||||
|
||||
before do
|
||||
allow(V2::Reports::Timeseries::CountReportBuilder).to receive(:new).and_return(count_builder_instance)
|
||||
allow(V2::Reports::Timeseries::AverageReportBuilder).to receive(:new).and_return(avg_builder_instance)
|
||||
end
|
||||
|
||||
describe '#summary' do
|
||||
it 'returns the correct summary values' do
|
||||
summary = subject.summary
|
||||
expect(summary).to eq(
|
||||
{
|
||||
conversations_count: 42,
|
||||
incoming_messages_count: 42,
|
||||
outgoing_messages_count: 42,
|
||||
avg_first_response_time: 42,
|
||||
avg_resolution_time: 42,
|
||||
resolutions_count: 42,
|
||||
reply_time: 42
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates builders with proper params' do
|
||||
subject.summary
|
||||
expect(V2::Reports::Timeseries::CountReportBuilder).to have_received(:new).with(account, params.merge(metric: 'conversations_count'))
|
||||
expect(V2::Reports::Timeseries::AverageReportBuilder).to have_received(:new).with(account, params.merge(metric: 'avg_first_response_time'))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bot_summary' do
|
||||
it 'returns a detailed summary of bot-specific conversation metrics' do
|
||||
bot_summary = subject.bot_summary
|
||||
expect(bot_summary).to eq(
|
||||
{
|
||||
bot_resolutions_count: 42,
|
||||
bot_handoffs_count: 42
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe V2::Reports::Conversations::ReportBuilder do
|
||||
subject { described_class.new(account, params) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:average_builder) { V2::Reports::Timeseries::AverageReportBuilder }
|
||||
let(:count_builder) { V2::Reports::Timeseries::CountReportBuilder }
|
||||
|
||||
shared_examples 'valid metric handler' do |metric, method, builder|
|
||||
context 'when a valid metric is given' do
|
||||
let(:params) { { metric: metric } }
|
||||
|
||||
it "calls the correct #{method} builder for #{metric}" do
|
||||
builder_instance = instance_double(builder)
|
||||
allow(builder).to receive(:new).and_return(builder_instance)
|
||||
allow(builder_instance).to receive(method)
|
||||
|
||||
builder_instance.public_send(method)
|
||||
expect(builder_instance).to have_received(method)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid metric is given' do
|
||||
let(:metric) { 'invalid_metric' }
|
||||
let(:params) { { metric: metric } }
|
||||
|
||||
it 'logs the error and returns empty value' do
|
||||
expect(Rails.logger).to receive(:error).with("ReportBuilder: Invalid metric - #{metric}")
|
||||
expect(subject.timeseries).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
describe '#timeseries' do
|
||||
it_behaves_like 'valid metric handler', 'avg_first_response_time', :timeseries, V2::Reports::Timeseries::AverageReportBuilder
|
||||
it_behaves_like 'valid metric handler', 'conversations_count', :timeseries, V2::Reports::Timeseries::CountReportBuilder
|
||||
end
|
||||
|
||||
describe '#aggregate_value' do
|
||||
it_behaves_like 'valid metric handler', 'avg_first_response_time', :aggregate_value, V2::Reports::Timeseries::AverageReportBuilder
|
||||
it_behaves_like 'valid metric handler', 'conversations_count', :aggregate_value, V2::Reports::Timeseries::CountReportBuilder
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,145 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::FirstResponseTimeDistributionBuilder do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:web_widget_inbox) { create(:inbox, account: account, channel: create(:channel_widget, account: account)) }
|
||||
let!(:email_inbox) { create(:inbox, account: account, channel: create(:channel_email, account: account)) }
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day.to_i.to_s,
|
||||
until: Time.current.end_of_day.to_i.to_s
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
subject(:report) { builder.build }
|
||||
|
||||
context 'when there are first response events across channels and time buckets' do
|
||||
before do
|
||||
# Web Widget: 0-1h bucket (30 minutes = 1800 seconds)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
# Web Widget: 1-4h bucket (2 hours = 7200 seconds)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 7_200, created_at: 2.days.ago)
|
||||
# Web Widget: 4-8h bucket (6 hours = 21600 seconds)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 21_600, created_at: 3.days.ago)
|
||||
# Email: 8-24h bucket (12 hours = 43200 seconds)
|
||||
create(:reporting_event, account: account, inbox: email_inbox, name: 'first_response',
|
||||
value: 43_200, created_at: 2.days.ago)
|
||||
# Email: 24h+ bucket (48 hours = 172800 seconds)
|
||||
create(:reporting_event, account: account, inbox: email_inbox, name: 'first_response',
|
||||
value: 172_800, created_at: 1.day.ago)
|
||||
end
|
||||
|
||||
it 'returns correct distribution for web widget channel' do
|
||||
expect(report['Channel::WebWidget']).to eq({
|
||||
'0-1h' => 1,
|
||||
'1-4h' => 1,
|
||||
'4-8h' => 1,
|
||||
'8-24h' => 0,
|
||||
'24h+' => 0
|
||||
})
|
||||
end
|
||||
|
||||
it 'returns correct distribution for email channel' do
|
||||
expect(report['Channel::Email']).to eq({
|
||||
'0-1h' => 0,
|
||||
'1-4h' => 0,
|
||||
'4-8h' => 0,
|
||||
'8-24h' => 1,
|
||||
'24h+' => 1
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by date range' do
|
||||
before do
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.weeks.ago)
|
||||
end
|
||||
|
||||
it 'only counts events within the date range' do
|
||||
expect(report['Channel::WebWidget']['0-1h']).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no first response events' do
|
||||
it 'returns an empty hash' do
|
||||
expect(report).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when events belong to another account' do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_inbox) { create(:inbox, account: other_account) }
|
||||
|
||||
before do
|
||||
create(:reporting_event, account: other_account, inbox: other_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'does not include events from other accounts' do
|
||||
expect(report).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when events have different names' do
|
||||
before do
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'conversation_resolved',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'reply_time',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'only counts first_response events' do
|
||||
expect(report['Channel::WebWidget']['0-1h']).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no date range params are provided' do
|
||||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.months.ago)
|
||||
end
|
||||
|
||||
it 'returns all events without date filtering' do
|
||||
expect(report['Channel::WebWidget']['0-1h']).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with boundary values for time buckets' do
|
||||
before do
|
||||
# Exactly at 1 hour boundary (should be in 1-4h bucket)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 3_600, created_at: 2.days.ago)
|
||||
# Just under 1 hour (should be in 0-1h bucket)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 3_599, created_at: 2.days.ago)
|
||||
# Exactly at 24 hour boundary (should be in 24h+ bucket)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 86_400, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'correctly assigns boundary values to buckets' do
|
||||
expect(report['Channel::WebWidget']).to eq({
|
||||
'0-1h' => 1,
|
||||
'1-4h' => 1,
|
||||
'4-8h' => 0,
|
||||
'8-24h' => 0,
|
||||
'24h+' => 1
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,135 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::InboxLabelMatrixBuilder do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:inbox_one) { create(:inbox, account: account, name: 'Email Support') }
|
||||
let!(:inbox_two) { create(:inbox, account: account, name: 'Web Chat') }
|
||||
let!(:label_one) { create(:label, account: account, title: 'bug') }
|
||||
let!(:label_two) { create(:label, account: account, title: 'feature') }
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day.to_i.to_s,
|
||||
until: Time.current.end_of_day.to_i.to_s
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
subject(:report) { builder.build }
|
||||
|
||||
context 'when there are conversations with labels across inboxes' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title])
|
||||
|
||||
c2 = create(:conversation, account: account, inbox: inbox_one, created_at: 3.days.ago)
|
||||
c2.update(label_list: [label_one.title, label_two.title])
|
||||
|
||||
c3 = create(:conversation, account: account, inbox: inbox_two, created_at: 1.day.ago)
|
||||
c3.update(label_list: [label_two.title])
|
||||
end
|
||||
|
||||
it 'returns inboxes ordered by name' do
|
||||
expect(report[:inboxes]).to eq([
|
||||
{ id: inbox_one.id, name: 'Email Support' },
|
||||
{ id: inbox_two.id, name: 'Web Chat' }
|
||||
])
|
||||
end
|
||||
|
||||
it 'returns labels ordered by title' do
|
||||
expect(report[:labels]).to eq([
|
||||
{ id: label_one.id, title: 'bug' },
|
||||
{ id: label_two.id, title: 'feature' }
|
||||
])
|
||||
end
|
||||
|
||||
it 'returns correct conversation counts in the matrix' do
|
||||
# Email Support: bug=2, feature=1
|
||||
# Web Chat: bug=0, feature=1
|
||||
expect(report[:matrix]).to eq([[2, 1], [0, 1]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by inbox_ids' do
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day.to_i.to_s,
|
||||
until: Time.current.end_of_day.to_i.to_s,
|
||||
inbox_ids: [inbox_one.id]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title])
|
||||
|
||||
c2 = create(:conversation, account: account, inbox: inbox_two, created_at: 1.day.ago)
|
||||
c2.update(label_list: [label_one.title])
|
||||
end
|
||||
|
||||
it 'only includes the specified inboxes and their counts' do
|
||||
expect(report[:inboxes]).to eq([{ id: inbox_one.id, name: 'Email Support' }])
|
||||
expect(report[:matrix]).to eq([[1, 0]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by label_ids' do
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day.to_i.to_s,
|
||||
until: Time.current.end_of_day.to_i.to_s,
|
||||
label_ids: [label_one.id]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title, label_two.title])
|
||||
end
|
||||
|
||||
it 'only includes the specified labels and their counts' do
|
||||
expect(report[:labels]).to eq([{ id: label_one.id, title: 'bug' }])
|
||||
expect(report[:matrix]).to eq([[1], [0]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversations are outside the date range' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
c1.update(label_list: [label_one.title])
|
||||
|
||||
c2 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.weeks.ago)
|
||||
c2.update(label_list: [label_one.title])
|
||||
end
|
||||
|
||||
it 'only counts conversations within the date range' do
|
||||
expect(report[:matrix]).to eq([[1, 0], [0, 0]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no conversations with labels' do
|
||||
before do
|
||||
create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'returns a matrix of zeros' do
|
||||
expect(report[:matrix]).to eq([[0, 0], [0, 0]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversations belong to another account' do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_inbox) { create(:inbox, account: other_account) }
|
||||
|
||||
before do
|
||||
c1 = create(:conversation, account: other_account, inbox: other_inbox, created_at: 2.days.ago)
|
||||
other_label = create(:label, account: other_account, title: 'bug')
|
||||
c1.update(label_list: [other_label.title])
|
||||
end
|
||||
|
||||
it 'does not include conversations from other accounts' do
|
||||
expect(report[:matrix]).to eq([[0, 0], [0, 0]])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,93 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::InboxSummaryBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:i1) { create(:inbox, account: account) }
|
||||
let(:i2) { create(:inbox, account: account) }
|
||||
let(:params) do
|
||||
{
|
||||
business_hours: business_hours,
|
||||
since: 1.week.ago.beginning_of_day,
|
||||
until: Time.current.end_of_day
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
before do
|
||||
c1 = create(:conversation, account: account, inbox: i1, created_at: 2.days.ago)
|
||||
c2 = create(:conversation, account: account, inbox: i2, created_at: 1.day.ago)
|
||||
c2.resolved!
|
||||
create(:reporting_event, account: account, conversation: c2, inbox: i2, name: 'conversation_resolved', value: 100, value_in_business_hours: 60,
|
||||
created_at: 1.day.ago)
|
||||
create(:reporting_event, account: account, conversation: c1, inbox: i1, name: 'first_response', value: 50, value_in_business_hours: 30,
|
||||
created_at: 1.day.ago)
|
||||
create(:reporting_event, account: account, conversation: c1, inbox: i1, name: 'reply_time', value: 30, value_in_business_hours: 10,
|
||||
created_at: 1.day.ago)
|
||||
create(:reporting_event, account: account, conversation: c1, inbox: i1, name: 'reply_time', value: 40, value_in_business_hours: 20,
|
||||
created_at: 1.day.ago)
|
||||
end
|
||||
|
||||
describe '#build' do
|
||||
subject(:report) { builder.build }
|
||||
|
||||
context 'when business hours is disabled' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'includes correct stats for each inbox' do
|
||||
expect(report).to contain_exactly({
|
||||
id: i1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 50.0,
|
||||
avg_reply_time: 35.0
|
||||
}, {
|
||||
id: i2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 100.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is enabled' do
|
||||
let(:business_hours) { true }
|
||||
|
||||
it 'uses business hours values for calculations' do
|
||||
expect(report).to contain_exactly({
|
||||
id: i1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 30.0,
|
||||
avg_reply_time: 15.0
|
||||
}, {
|
||||
id: i2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 60.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no data for an inbox' do
|
||||
let!(:empty_inbox) { create(:inbox, account: account) }
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns nil values for metrics' do
|
||||
expect(report).to include(
|
||||
id: empty_inbox.id,
|
||||
conversations_count: 0,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,373 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::LabelSummaryBuilder do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
let_it_be(:account) { create(:account) }
|
||||
let_it_be(:label_1) { create(:label, title: 'label_1', account: account) }
|
||||
let_it_be(:label_2) { create(:label, title: 'label_2', account: account) }
|
||||
let_it_be(:label_3) { create(:label, title: 'label_3', account: account) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
business_hours: business_hours,
|
||||
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s,
|
||||
timezone_offset: 0
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#initialize' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'sets account and params' do
|
||||
expect(builder.account).to eq(account)
|
||||
expect(builder.params).to eq(params)
|
||||
end
|
||||
|
||||
it 'sets timezone from timezone_offset' do
|
||||
builder_with_offset = described_class.new(account: account, params: { timezone_offset: -8 })
|
||||
expect(builder_with_offset.instance_variable_get(:@timezone)).to eq('Pacific Time (US & Canada)')
|
||||
end
|
||||
|
||||
it 'defaults timezone when timezone_offset is not provided' do
|
||||
builder_without_offset = described_class.new(account: account, params: {})
|
||||
expect(builder_without_offset.instance_variable_get(:@timezone)).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build' do
|
||||
context 'when there are no labels' do
|
||||
let(:business_hours) { false }
|
||||
let(:empty_account) { create(:account) }
|
||||
let(:empty_builder) { described_class.new(account: empty_account, params: params) }
|
||||
|
||||
it 'returns empty array' do
|
||||
expect(empty_builder.build).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are labels but no conversations' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns zero values for all labels' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
bug_report = report.find { |r| r[:name] == 'label_1' }
|
||||
feature_request = report.find { |r| r[:name] == 'label_2' }
|
||||
customer_support = report.find { |r| r[:name] == 'label_3' }
|
||||
|
||||
[
|
||||
[bug_report, label_1, 'label_1'],
|
||||
[feature_request, label_2, 'label_2'],
|
||||
[customer_support, label_3, 'label_3']
|
||||
].each do |report_data, label, label_name|
|
||||
expect(report_data).to include(
|
||||
id: label.id,
|
||||
name: label_name,
|
||||
conversations_count: 0,
|
||||
avg_resolution_time: 0,
|
||||
avg_first_response_time: 0,
|
||||
avg_reply_time: 0,
|
||||
resolved_conversations_count: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are labeled conversations with metrics' do
|
||||
before do
|
||||
travel_to(Time.zone.today) do
|
||||
user = create(:user, account: account)
|
||||
inbox = create(:inbox, account: account)
|
||||
create(:inbox_member, user: user, inbox: inbox)
|
||||
|
||||
gravatar_url = 'https://www.gravatar.com'
|
||||
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
# Create conversations with label_1
|
||||
3.times do
|
||||
conversation = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: Time.zone.today)
|
||||
create_list(:message, 2, message_type: 'outgoing',
|
||||
account: account, inbox: inbox,
|
||||
conversation: conversation,
|
||||
created_at: Time.zone.today + 1.hour)
|
||||
create_list(:message, 1, message_type: 'incoming',
|
||||
account: account, inbox: inbox,
|
||||
conversation: conversation,
|
||||
created_at: Time.zone.today + 2.hours)
|
||||
conversation.update_labels('label_1')
|
||||
conversation.label_list
|
||||
conversation.save!
|
||||
end
|
||||
|
||||
# Create conversations with label_2
|
||||
2.times do
|
||||
conversation = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: Time.zone.today)
|
||||
create_list(:message, 1, message_type: 'outgoing',
|
||||
account: account, inbox: inbox,
|
||||
conversation: conversation,
|
||||
created_at: Time.zone.today + 1.hour)
|
||||
conversation.update_labels('label_2')
|
||||
conversation.label_list
|
||||
conversation.save!
|
||||
end
|
||||
|
||||
# Resolve some conversations
|
||||
conversations_to_resolve = account.conversations.first(2)
|
||||
conversations_to_resolve.each(&:toggle_status)
|
||||
|
||||
# Create some reporting events
|
||||
account.conversations.reload.each_with_index do |conv, idx|
|
||||
# First response times
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conv,
|
||||
name: 'first_response',
|
||||
value: (30 + (idx * 10)) * 60,
|
||||
value_in_business_hours: (20 + (idx * 5)) * 60,
|
||||
created_at: Time.zone.today)
|
||||
|
||||
# Reply times
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conv,
|
||||
name: 'reply_time',
|
||||
value: (15 + (idx * 5)) * 60,
|
||||
value_in_business_hours: (10 + (idx * 3)) * 60,
|
||||
created_at: Time.zone.today)
|
||||
|
||||
# Resolution times for resolved conversations
|
||||
next unless conv.resolved?
|
||||
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conv,
|
||||
name: 'conversation_resolved',
|
||||
value: (60 + (idx * 30)) * 60,
|
||||
value_in_business_hours: (45 + (idx * 20)) * 60,
|
||||
created_at: Time.zone.today)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is disabled' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns correct label stats using regular values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
label_1_report = report.find { |r| r[:name] == 'label_1' }
|
||||
label_2_report = report.find { |r| r[:name] == 'label_2' }
|
||||
label_3_report = report.find { |r| r[:name] == 'label_3' }
|
||||
|
||||
expect(label_1_report).to include(
|
||||
conversations_count: 3,
|
||||
avg_first_response_time: be > 0,
|
||||
avg_reply_time: be > 0
|
||||
)
|
||||
|
||||
expect(label_2_report).to include(
|
||||
conversations_count: 2,
|
||||
avg_first_response_time: be > 0,
|
||||
avg_reply_time: be > 0
|
||||
)
|
||||
|
||||
expect(label_3_report).to include(
|
||||
conversations_count: 0,
|
||||
avg_first_response_time: 0,
|
||||
avg_reply_time: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is enabled' do
|
||||
let(:business_hours) { true }
|
||||
|
||||
it 'returns correct label stats using business hours values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
label_1_report = report.find { |r| r[:name] == 'label_1' }
|
||||
label_2_report = report.find { |r| r[:name] == 'label_2' }
|
||||
|
||||
expect(label_1_report[:conversations_count]).to eq(3)
|
||||
expect(label_1_report[:avg_first_response_time]).to be > 0
|
||||
expect(label_1_report[:avg_reply_time]).to be > 0
|
||||
|
||||
expect(label_2_report[:conversations_count]).to eq(2)
|
||||
expect(label_2_report[:avg_first_response_time]).to be > 0
|
||||
expect(label_2_report[:avg_reply_time]).to be > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by date range' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
before do
|
||||
travel_to(Time.zone.today) do
|
||||
user = create(:user, account: account)
|
||||
inbox = create(:inbox, account: account)
|
||||
create(:inbox_member, user: user, inbox: inbox)
|
||||
|
||||
gravatar_url = 'https://www.gravatar.com'
|
||||
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
# Conversation within range
|
||||
conversation_in_range = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: 2.days.ago)
|
||||
conversation_in_range.update_labels('label_1')
|
||||
conversation_in_range.label_list
|
||||
conversation_in_range.save!
|
||||
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conversation_in_range,
|
||||
name: 'first_response',
|
||||
value: 1800,
|
||||
created_at: 2.days.ago)
|
||||
|
||||
# Conversation outside range (too old)
|
||||
conversation_out_of_range = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: 1.week.ago)
|
||||
conversation_out_of_range.update_labels('label_1')
|
||||
conversation_out_of_range.label_list
|
||||
conversation_out_of_range.save!
|
||||
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conversation_out_of_range,
|
||||
name: 'first_response',
|
||||
value: 3600,
|
||||
created_at: 1.week.ago)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'only includes conversations within the date range' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
label_1_report = report.find { |r| r[:name] == 'label_1' }
|
||||
expect(label_1_report).not_to be_nil
|
||||
expect(label_1_report[:conversations_count]).to eq(1)
|
||||
expect(label_1_report[:avg_first_response_time]).to eq(1800.0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with business hours parameter' do
|
||||
let(:business_hours) { 'true' }
|
||||
|
||||
before do
|
||||
travel_to(Time.zone.today) do
|
||||
user = create(:user, account: account)
|
||||
inbox = create(:inbox, account: account)
|
||||
create(:inbox_member, user: user, inbox: inbox)
|
||||
|
||||
gravatar_url = 'https://www.gravatar.com'
|
||||
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
conversation = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: Time.zone.today)
|
||||
conversation.update_labels('label_1')
|
||||
conversation.label_list
|
||||
conversation.save!
|
||||
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conversation,
|
||||
name: 'first_response',
|
||||
value: 3600,
|
||||
value_in_business_hours: 1800,
|
||||
created_at: Time.zone.today)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'properly casts string "true" to boolean and uses business hours values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
label_1_report = report.find { |r| r[:name] == 'label_1' }
|
||||
expect(label_1_report).not_to be_nil
|
||||
expect(label_1_report[:avg_first_response_time]).to eq(1800.0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with resolution count with multiple resolutions of same conversation' do
|
||||
let(:business_hours) { false }
|
||||
let(:account2) { create(:account) }
|
||||
let(:unique_label_name) { SecureRandom.uuid }
|
||||
let(:test_label) { create(:label, title: unique_label_name, account: account2) }
|
||||
let(:test_date) { Date.new(2025, 6, 15) }
|
||||
let(:account2_builder) do
|
||||
described_class.new(account: account2, params: {
|
||||
business_hours: false,
|
||||
since: test_date.to_time.to_i.to_s,
|
||||
until: test_date.end_of_day.to_time.to_i.to_s,
|
||||
timezone_offset: 0
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
# Ensure test_label is created
|
||||
test_label
|
||||
|
||||
travel_to(test_date) do
|
||||
user = create(:user, account: account2)
|
||||
inbox = create(:inbox, account: account2)
|
||||
create(:inbox_member, user: user, inbox: inbox)
|
||||
|
||||
gravatar_url = 'https://www.gravatar.com'
|
||||
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
conversation = create(:conversation, account: account2,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: test_date)
|
||||
conversation.update_labels(unique_label_name)
|
||||
conversation.label_list
|
||||
conversation.save!
|
||||
|
||||
# First resolution
|
||||
conversation.resolved!
|
||||
|
||||
# Reopen conversation
|
||||
conversation.open!
|
||||
|
||||
# Second resolution
|
||||
conversation.resolved!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'counts multiple resolution events for same conversation' do
|
||||
report = account2_builder.build
|
||||
|
||||
test_label_report = report.find { |r| r[:name] == unique_label_name }
|
||||
expect(test_label_report).not_to be_nil
|
||||
expect(test_label_report[:resolved_conversations_count]).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,138 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::TeamSummaryBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:team1) { create(:team, account: account, name: 'team-1') }
|
||||
let(:team2) { create(:team, account: account, name: 'team-2') }
|
||||
let(:params) do
|
||||
{
|
||||
business_hours: business_hours,
|
||||
since: 1.week.ago.beginning_of_day,
|
||||
until: Time.current.end_of_day
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
context 'when there is team data' do
|
||||
before do
|
||||
c1 = create(:conversation, account: account, team: team1, created_at: Time.current)
|
||||
c2 = create(:conversation, account: account, team: team2, created_at: Time.current)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c2,
|
||||
name: 'conversation_resolved',
|
||||
value: 50,
|
||||
value_in_business_hours: 40,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
name: 'first_response',
|
||||
value: 20,
|
||||
value_in_business_hours: 10,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
name: 'reply_time',
|
||||
value: 30,
|
||||
value_in_business_hours: 15,
|
||||
created_at: Time.current
|
||||
)
|
||||
create(
|
||||
:reporting_event,
|
||||
account: account,
|
||||
conversation: c1,
|
||||
name: 'reply_time',
|
||||
value: 40,
|
||||
value_in_business_hours: 25,
|
||||
created_at: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
context 'when business hours is disabled' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns the correct team stats' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to eq(
|
||||
[
|
||||
{
|
||||
id: team1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 20.0,
|
||||
avg_reply_time: 35.0
|
||||
},
|
||||
{
|
||||
id: team2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 50.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is enabled' do
|
||||
let(:business_hours) { true }
|
||||
|
||||
it 'uses business hours values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to eq(
|
||||
[
|
||||
{
|
||||
id: team1.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: 10.0,
|
||||
avg_reply_time: 20.0
|
||||
},
|
||||
{
|
||||
id: team2.id,
|
||||
conversations_count: 1,
|
||||
resolved_conversations_count: 1,
|
||||
avg_resolution_time: 40.0,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no team data' do
|
||||
let!(:new_team) { create(:team, account: account) }
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns zero values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report).to include(
|
||||
{
|
||||
id: new_team.id,
|
||||
conversations_count: 0,
|
||||
resolved_conversations_count: 0,
|
||||
avg_resolution_time: nil,
|
||||
avg_first_response_time: nil,
|
||||
avg_reply_time: nil
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,174 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe V2::Reports::Timeseries::AverageReportBuilder do
|
||||
subject { described_class.new(account, params) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:team) { create(:team, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:label) { create(:label, title: 'spec-billing', account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, team: team) }
|
||||
let(:current_time) { '26.10.2020 10:00'.to_datetime }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
type: filter_type,
|
||||
business_hours: business_hours,
|
||||
timezone_offset: timezone_offset,
|
||||
group_by: group_by,
|
||||
metric: metric,
|
||||
since: (current_time - 1.week).beginning_of_day.to_i.to_s,
|
||||
until: current_time.end_of_day.to_i.to_s,
|
||||
id: filter_id
|
||||
}
|
||||
end
|
||||
let(:timezone_offset) { nil }
|
||||
let(:group_by) { 'day' }
|
||||
let(:metric) { 'avg_first_response_time' }
|
||||
let(:business_hours) { false }
|
||||
let(:filter_type) { :account }
|
||||
let(:filter_id) { '' }
|
||||
|
||||
before do
|
||||
travel_to current_time
|
||||
conversation.label_list.add(label.title)
|
||||
conversation.save!
|
||||
create(:reporting_event, name: 'first_response', value: 80, value_in_business_hours: 10, account: account, created_at: Time.zone.now,
|
||||
conversation: conversation, inbox: inbox)
|
||||
create(:reporting_event, name: 'first_response', value: 100, value_in_business_hours: 20, account: account, created_at: 1.hour.ago)
|
||||
create(:reporting_event, name: 'first_response', value: 93, value_in_business_hours: 30, account: account, created_at: 1.week.ago)
|
||||
end
|
||||
|
||||
describe '#timeseries' do
|
||||
context 'when there is no filter applied' do
|
||||
it 'returns the correct values' do
|
||||
timeseries_values = subject.timeseries
|
||||
|
||||
expect(timeseries_values).to eq(
|
||||
[
|
||||
{ count: 1, timestamp: 1_603_065_600, value: 93.0 },
|
||||
{ count: 0, timestamp: 1_603_152_000, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_238_400, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_324_800, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_411_200, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_497_600, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_584_000, value: 0 },
|
||||
{ count: 2, timestamp: 1_603_670_400, value: 90.0 }
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
context 'when business hours is provided' do
|
||||
let(:business_hours) { true }
|
||||
|
||||
it 'returns correct timeseries' do
|
||||
timeseries_values = subject.timeseries
|
||||
|
||||
expect(timeseries_values).to eq(
|
||||
[
|
||||
{ count: 1, timestamp: 1_603_065_600, value: 30.0 },
|
||||
{ count: 0, timestamp: 1_603_152_000, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_238_400, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_324_800, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_411_200, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_497_600, value: 0 },
|
||||
{ count: 0, timestamp: 1_603_584_000, value: 0 },
|
||||
{ count: 2, timestamp: 1_603_670_400, value: 15.0 }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group_by is provided' do
|
||||
let(:group_by) { 'week' }
|
||||
|
||||
it 'returns correct timeseries' do
|
||||
timeseries_values = subject.timeseries
|
||||
expect(timeseries_values).to eq(
|
||||
[
|
||||
{ count: 1, timestamp: (current_time - 1.week).beginning_of_week(:sunday).to_i, value: 93.0 },
|
||||
{ count: 2, timestamp: current_time.beginning_of_week(:sunday).to_i, value: 90.0 }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when timezone offset is provided' do
|
||||
let(:timezone_offset) { '5.5' }
|
||||
let(:group_by) { 'week' }
|
||||
|
||||
it 'returns correct timeseries' do
|
||||
timeseries_values = subject.timeseries
|
||||
expect(timeseries_values).to eq(
|
||||
[
|
||||
{ count: 1, timestamp: (current_time - 1.week).in_time_zone('Chennai').beginning_of_week(:sunday).to_i, value: 93.0 },
|
||||
{ count: 2, timestamp: current_time.in_time_zone('Chennai').beginning_of_week(:sunday).to_i, value: 90.0 }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the label filter is applied' do
|
||||
let(:group_by) { 'week' }
|
||||
let(:filter_type) { 'label' }
|
||||
let(:filter_id) { label.id }
|
||||
|
||||
it 'returns correct timeseries' do
|
||||
timeseries_values = subject.timeseries
|
||||
start_of_the_week = current_time.beginning_of_week(:sunday).to_i
|
||||
last_week_start_of_the_week = (current_time - 1.week).beginning_of_week(:sunday).to_i
|
||||
expect(timeseries_values).to eq(
|
||||
[
|
||||
{ count: 0, timestamp: last_week_start_of_the_week, value: 0 },
|
||||
{ count: 1, timestamp: start_of_the_week, value: 80.0 }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the inbox filter is applied' do
|
||||
let(:group_by) { 'week' }
|
||||
let(:filter_type) { 'inbox' }
|
||||
let(:filter_id) { inbox.id }
|
||||
|
||||
it 'returns correct timeseries' do
|
||||
timeseries_values = subject.timeseries
|
||||
start_of_the_week = current_time.beginning_of_week(:sunday).to_i
|
||||
last_week_start_of_the_week = (current_time - 1.week).beginning_of_week(:sunday).to_i
|
||||
expect(timeseries_values).to eq(
|
||||
[
|
||||
{ count: 0, timestamp: last_week_start_of_the_week, value: 0 },
|
||||
{ count: 1, timestamp: start_of_the_week, value: 80.0 }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the team filter is applied' do
|
||||
let(:group_by) { 'week' }
|
||||
let(:filter_type) { 'team' }
|
||||
let(:filter_id) { team.id }
|
||||
|
||||
it 'returns correct timeseries' do
|
||||
timeseries_values = subject.timeseries
|
||||
start_of_the_week = current_time.beginning_of_week(:sunday).to_i
|
||||
last_week_start_of_the_week = (current_time - 1.week).beginning_of_week(:sunday).to_i
|
||||
expect(timeseries_values).to eq(
|
||||
[
|
||||
{ count: 0, timestamp: last_week_start_of_the_week, value: 0 },
|
||||
{ count: 1, timestamp: start_of_the_week, value: 80.0 }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#aggregate_value' do
|
||||
context 'when there is no filter applied' do
|
||||
it 'returns the correct average value' do
|
||||
expect(subject.aggregate_value).to eq 91.0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe V2::Reports::Timeseries::CountReportBuilder do
|
||||
subject { described_class.new(account, params) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:account2) { create(:account) }
|
||||
let(:user) { create(:user, email: 'agent1@example.com') }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:inbox2) { create(:inbox, account: account2) }
|
||||
let(:current_time) { Time.current }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
type: 'agent',
|
||||
metric: 'resolutions_count',
|
||||
since: (current_time - 1.day).beginning_of_day.to_i.to_s,
|
||||
until: current_time.end_of_day.to_i.to_s,
|
||||
id: user.id.to_s
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
travel_to current_time
|
||||
|
||||
# Add the same user to both accounts
|
||||
create(:account_user, account: account, user: user)
|
||||
create(:account_user, account: account2, user: user)
|
||||
|
||||
# Create conversations in account1
|
||||
conversation1 = create(:conversation, account: account, inbox: inbox, assignee: user)
|
||||
conversation2 = create(:conversation, account: account, inbox: inbox, assignee: user)
|
||||
|
||||
# Create conversations in account2
|
||||
conversation3 = create(:conversation, account: account2, inbox: inbox2, assignee: user)
|
||||
conversation4 = create(:conversation, account: account2, inbox: inbox2, assignee: user)
|
||||
|
||||
# User resolves 2 conversations in account1
|
||||
create(:reporting_event,
|
||||
name: 'conversation_resolved',
|
||||
account: account,
|
||||
user: user,
|
||||
conversation: conversation1,
|
||||
created_at: current_time - 12.hours)
|
||||
|
||||
create(:reporting_event,
|
||||
name: 'conversation_resolved',
|
||||
account: account,
|
||||
user: user,
|
||||
conversation: conversation2,
|
||||
created_at: current_time - 6.hours)
|
||||
|
||||
# Same user resolves 3 conversations in account2 - these should NOT be counted for account1
|
||||
create(:reporting_event,
|
||||
name: 'conversation_resolved',
|
||||
account: account2,
|
||||
user: user,
|
||||
conversation: conversation3,
|
||||
created_at: current_time - 8.hours)
|
||||
|
||||
create(:reporting_event,
|
||||
name: 'conversation_resolved',
|
||||
account: account2,
|
||||
user: user,
|
||||
conversation: conversation4,
|
||||
created_at: current_time - 4.hours)
|
||||
|
||||
# Create another conversation in account2 for testing
|
||||
conversation5 = create(:conversation, account: account2, inbox: inbox2, assignee: user)
|
||||
create(:reporting_event,
|
||||
name: 'conversation_resolved',
|
||||
account: account2,
|
||||
user: user,
|
||||
conversation: conversation5,
|
||||
created_at: current_time - 2.hours)
|
||||
end
|
||||
|
||||
describe '#aggregate_value' do
|
||||
it 'returns only resolutions performed by the user in the specified account' do
|
||||
# User should have 2 resolutions in account1, not 5 (total across both accounts)
|
||||
expect(subject.aggregate_value).to eq(2)
|
||||
end
|
||||
|
||||
context 'when querying account2' do
|
||||
subject { described_class.new(account2, params) }
|
||||
|
||||
it 'returns only resolutions for account2' do
|
||||
# User should have 3 resolutions in account2
|
||||
expect(subject.aggregate_value).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#timeseries' do
|
||||
it 'filters resolutions by account' do
|
||||
result = subject.timeseries
|
||||
# Should only count the 2 resolutions from account1
|
||||
total_count = result.sum { |r| r[:value] }
|
||||
expect(total_count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'account isolation' do
|
||||
it 'does not leak data between accounts' do
|
||||
# If account isolation works correctly, the counts should be different
|
||||
account1_count = described_class.new(account, params).aggregate_value
|
||||
account2_count = described_class.new(account2, params).aggregate_value
|
||||
|
||||
expect(account1_count).to eq(2)
|
||||
expect(account2_count).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user