mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 18:22:53 +00:00
feat: label reports overview (#11194)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
101
app/builders/v2/reports/label_summary_builder.rb
Normal file
101
app/builders/v2/reports/label_summary_builder.rb
Normal file
@@ -0,0 +1,101 @@
|
||||
class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
attr_reader :account, :params
|
||||
|
||||
# rubocop:disable Lint/MissingSuper
|
||||
# the parent class has no initialize
|
||||
def initialize(account:, params:)
|
||||
@account = account
|
||||
@params = params
|
||||
|
||||
timezone_offset = (params[:timezone_offset] || 0).to_f
|
||||
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
|
||||
end
|
||||
# rubocop:enable Lint/MissingSuper
|
||||
|
||||
def build
|
||||
labels = account.labels.to_a
|
||||
return [] if labels.empty?
|
||||
|
||||
report_data = collect_report_data
|
||||
labels.map { |label| build_label_report(label, report_data) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collect_report_data
|
||||
conversation_filter = build_conversation_filter
|
||||
use_business_hours = use_business_hours?
|
||||
|
||||
{
|
||||
conversation_counts: fetch_conversation_counts(conversation_filter),
|
||||
resolved_counts: fetch_resolved_counts(conversation_filter),
|
||||
resolution_metrics: fetch_metrics(conversation_filter, 'conversation_resolved', use_business_hours),
|
||||
first_response_metrics: fetch_metrics(conversation_filter, 'first_response', use_business_hours),
|
||||
reply_metrics: fetch_metrics(conversation_filter, 'reply', use_business_hours)
|
||||
}
|
||||
end
|
||||
|
||||
def build_label_report(label, report_data)
|
||||
{
|
||||
id: label.id,
|
||||
name: label.title,
|
||||
conversations_count: report_data[:conversation_counts][label.title] || 0,
|
||||
avg_resolution_time: report_data[:resolution_metrics][label.title] || 0,
|
||||
avg_first_response_time: report_data[:first_response_metrics][label.title] || 0,
|
||||
avg_reply_time: report_data[:reply_metrics][label.title] || 0,
|
||||
resolved_conversations_count: report_data[:resolved_counts][label.title] || 0
|
||||
}
|
||||
end
|
||||
|
||||
def use_business_hours?
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||
end
|
||||
|
||||
def build_conversation_filter
|
||||
conversation_filter = { account_id: account.id }
|
||||
conversation_filter[:created_at] = range if range.present?
|
||||
|
||||
conversation_filter
|
||||
end
|
||||
|
||||
def fetch_conversation_counts(conversation_filter)
|
||||
fetch_counts(conversation_filter)
|
||||
end
|
||||
|
||||
def fetch_resolved_counts(conversation_filter)
|
||||
fetch_counts(conversation_filter.merge(status: :resolved))
|
||||
end
|
||||
|
||||
def fetch_counts(conversation_filter)
|
||||
ActsAsTaggableOn::Tagging
|
||||
.joins('INNER JOIN conversations ON taggings.taggable_id = conversations.id')
|
||||
.joins('INNER JOIN tags ON taggings.tag_id = tags.id')
|
||||
.where(
|
||||
taggable_type: 'Conversation',
|
||||
context: 'labels',
|
||||
conversations: conversation_filter
|
||||
)
|
||||
.select('tags.name, COUNT(taggings.*) AS count')
|
||||
.group('tags.name')
|
||||
.each_with_object({}) { |record, hash| hash[record.name] = record.count }
|
||||
end
|
||||
|
||||
def fetch_metrics(conversation_filter, event_name, use_business_hours)
|
||||
ReportingEvent
|
||||
.joins('INNER JOIN conversations ON reporting_events.conversation_id = conversations.id')
|
||||
.joins('INNER JOIN taggings ON taggings.taggable_id = conversations.id')
|
||||
.joins('INNER JOIN tags ON taggings.tag_id = tags.id')
|
||||
.where(
|
||||
conversations: conversation_filter,
|
||||
name: event_name,
|
||||
taggings: { taggable_type: 'Conversation', context: 'labels' }
|
||||
)
|
||||
.group('tags.name')
|
||||
.order('tags.name')
|
||||
.select(
|
||||
'tags.name',
|
||||
use_business_hours ? 'AVG(reporting_events.value_in_business_hours) as avg_value' : 'AVG(reporting_events.value) as avg_value'
|
||||
)
|
||||
.each_with_object({}) { |record, hash| hash[record.name] = record.avg_value.to_f }
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox]
|
||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label]
|
||||
|
||||
def agent
|
||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||
@@ -14,6 +14,10 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
||||
render_report_with(V2::Reports::InboxSummaryBuilder)
|
||||
end
|
||||
|
||||
def label
|
||||
render_report_with(V2::Reports::LabelSummaryBuilder)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
|
||||
@@ -36,9 +36,13 @@ module Api::V2::Accounts::ReportsHelper
|
||||
end
|
||||
|
||||
def generate_labels_report
|
||||
Current.account.labels.map do |label|
|
||||
label_report = report_builder({ type: :label, id: label.id }).short_summary
|
||||
[label.title] + generate_readable_report_metrics(label_report)
|
||||
reports = V2::Reports::LabelSummaryBuilder.new(
|
||||
account: Current.account,
|
||||
params: build_params({})
|
||||
).build
|
||||
|
||||
reports.map do |report|
|
||||
[report[:name]] + generate_readable_report_metrics(report)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -35,6 +35,16 @@ class SummaryReportsAPI extends ApiClient {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getLabelReports({ since, until, businessHours } = {}) {
|
||||
return axios.get(`${this.url}/label`, {
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
business_hours: businessHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new SummaryReportsAPI();
|
||||
|
||||
@@ -87,7 +87,7 @@ const newReportRoutes = () => [
|
||||
{
|
||||
name: 'Reports Label',
|
||||
label: t('SIDEBAR.REPORTS_LABEL'),
|
||||
to: accountScopedRoute('label_reports'),
|
||||
to: accountScopedRoute('label_reports_index'),
|
||||
},
|
||||
{
|
||||
name: 'Reports Inbox',
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
},
|
||||
"LABEL_REPORTS": {
|
||||
"HEADER": "Labels Overview",
|
||||
"DESCRIPTION": "Track label performance with key metrics including conversations, response times, resolution times, and resolved cases. Click a label name for detailed insights.",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
||||
@@ -559,6 +560,7 @@
|
||||
"INBOX": "Inbox",
|
||||
"AGENT": "Agent",
|
||||
"TEAM": "Team",
|
||||
"LABEL": "Label",
|
||||
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
|
||||
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
|
||||
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
import SummaryReports from './components/SummaryReports.vue';
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const summarReportsRef = ref(null);
|
||||
|
||||
const onDownloadClick = () => {
|
||||
summarReportsRef.value.downloadReports();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReportHeader
|
||||
:header-title="$t('LABEL_REPORTS.HEADER')"
|
||||
:header-description="$t('LABEL_REPORTS.DESCRIPTION')"
|
||||
>
|
||||
<V4Button
|
||||
:label="$t('LABEL_REPORTS.DOWNLOAD_LABEL_REPORTS')"
|
||||
icon="i-ph-download-simple"
|
||||
size="sm"
|
||||
@click="onDownloadClick"
|
||||
/>
|
||||
</ReportHeader>
|
||||
|
||||
<SummaryReports
|
||||
ref="summarReportsRef"
|
||||
action-key="summaryReports/fetchLabelSummaryReports"
|
||||
getter-key="labels/getLabels"
|
||||
fetch-items-key="labels/get"
|
||||
summary-key="summaryReports/getLabelSummaryReports"
|
||||
type="label"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useFunctionGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import WootReports from './components/WootReports.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const label = useFunctionGetter('labels/getLabelById', route.params.id);
|
||||
|
||||
onMounted(() => store.dispatch('labels/get'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WootReports
|
||||
v-if="label.id"
|
||||
:key="label.id"
|
||||
type="label"
|
||||
getter-key="labels/getLabels"
|
||||
action-key="labels/get"
|
||||
:selected-item="label"
|
||||
:download-button-label="$t('LABEL_REPORTS.DOWNLOAD_LABEL_REPORTS')"
|
||||
:report-title="$t('LABEL_REPORTS.HEADER')"
|
||||
has-back-button
|
||||
/>
|
||||
<div v-else class="w-full py-20">
|
||||
<Spinner class="mx-auto" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -108,7 +108,8 @@ const tableData = computed(() =>
|
||||
} = rowMetrics;
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
// we fallback on title, label for instance does not have a name property
|
||||
name: row.name ?? row.title,
|
||||
type: props.type,
|
||||
conversationsCount: renderCount(conversationsCount),
|
||||
avgFirstResponseTime: renderAvgTime(avgFirstResponseTime),
|
||||
@@ -177,7 +178,7 @@ defineExpose({ downloadReports });
|
||||
<template>
|
||||
<ReportFilterSelector @filter-change="onFilterChange" />
|
||||
<div
|
||||
class="flex-1 overflow-auto px-5 py-6 mt-5 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2"
|
||||
class="flex-1 overflow-auto px-2 py-2 mt-5 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2"
|
||||
>
|
||||
<Table :table="table" />
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,12 @@ import Index from './Index.vue';
|
||||
import AgentReportsIndex from './AgentReportsIndex.vue';
|
||||
import InboxReportsIndex from './InboxReportsIndex.vue';
|
||||
import TeamReportsIndex from './TeamReportsIndex.vue';
|
||||
import LabelReportsIndex from './LabelReportsIndex.vue';
|
||||
|
||||
import AgentReportsShow from './AgentReportsShow.vue';
|
||||
import InboxReportsShow from './InboxReportsShow.vue';
|
||||
import TeamReportsShow from './TeamReportsShow.vue';
|
||||
import LabelReportsShow from './LabelReportsShow.vue';
|
||||
|
||||
import AgentReports from './AgentReports.vue';
|
||||
import InboxReports from './InboxReports.vue';
|
||||
@@ -104,6 +106,22 @@ const revisedReportRoutes = [
|
||||
},
|
||||
component: TeamReportsShow,
|
||||
},
|
||||
{
|
||||
path: 'labels_overview',
|
||||
name: 'label_reports_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: LabelReportsIndex,
|
||||
},
|
||||
{
|
||||
path: 'labels/:id',
|
||||
name: 'label_reports_show',
|
||||
meta: {
|
||||
permissions: ['administrator', 'report_manage'],
|
||||
},
|
||||
component: LabelReportsShow,
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
@@ -26,6 +26,9 @@ export const getters = {
|
||||
.filter(record => record.show_on_sidebar)
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
},
|
||||
getLabelById: _state => id => {
|
||||
return _state.records.find(record => record.id === Number(id));
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
||||
@@ -7,6 +7,7 @@ vi.mock('dashboard/api/summaryReports', () => ({
|
||||
getInboxReports: vi.fn(),
|
||||
getAgentReports: vi.fn(),
|
||||
getTeamReports: vi.fn(),
|
||||
getLabelReports: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -25,10 +26,12 @@ describe('Summary Reports Store', () => {
|
||||
inboxSummaryReports: [],
|
||||
agentSummaryReports: [],
|
||||
teamSummaryReports: [],
|
||||
labelSummaryReports: [],
|
||||
uiFlags: {
|
||||
isFetchingInboxSummaryReports: false,
|
||||
isFetchingAgentSummaryReports: false,
|
||||
isFetchingTeamSummaryReports: false,
|
||||
isFetchingLabelSummaryReports: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -39,6 +42,7 @@ describe('Summary Reports Store', () => {
|
||||
inboxSummaryReports: [{ id: 1 }],
|
||||
agentSummaryReports: [{ id: 2 }],
|
||||
teamSummaryReports: [{ id: 3 }],
|
||||
labelSummaryReports: [{ id: 4 }],
|
||||
uiFlags: { isFetchingInboxSummaryReports: true },
|
||||
};
|
||||
|
||||
@@ -54,6 +58,10 @@ describe('Summary Reports Store', () => {
|
||||
expect(store.getters.getTeamSummaryReports(state)).toEqual([{ id: 3 }]);
|
||||
});
|
||||
|
||||
it('should return label summary reports', () => {
|
||||
expect(store.getters.getLabelSummaryReports(state)).toEqual([{ id: 4 }]);
|
||||
});
|
||||
|
||||
it('should return UI flags', () => {
|
||||
expect(store.getters.getUIFlags(state)).toEqual({
|
||||
isFetchingInboxSummaryReports: true,
|
||||
@@ -86,6 +94,14 @@ describe('Summary Reports Store', () => {
|
||||
expect(state.teamSummaryReports).toEqual(data);
|
||||
});
|
||||
|
||||
it('should set label summary report', () => {
|
||||
const state = { ...initialState };
|
||||
const data = [{ id: 4 }];
|
||||
|
||||
store.mutations.setLabelSummaryReport(state, data);
|
||||
expect(state.labelSummaryReports).toEqual(data);
|
||||
});
|
||||
|
||||
it('should merge UI flags with existing flags', () => {
|
||||
const state = {
|
||||
uiFlags: { flag1: true, flag2: false },
|
||||
@@ -185,5 +201,29 @@ describe('Summary Reports Store', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLabelSummaryReports', () => {
|
||||
it('should fetch label reports successfully', async () => {
|
||||
const params = { labelId: 789 };
|
||||
const mockResponse = {
|
||||
data: [{ label_id: 789, label_name: 'Test Label' }],
|
||||
};
|
||||
|
||||
SummaryReportsAPI.getLabelReports.mockResolvedValue(mockResponse);
|
||||
|
||||
await store.actions.fetchLabelSummaryReports({ commit }, params);
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingLabelSummaryReports: true,
|
||||
});
|
||||
expect(SummaryReportsAPI.getLabelReports).toHaveBeenCalledWith(params);
|
||||
expect(commit).toHaveBeenCalledWith('setLabelSummaryReport', [
|
||||
{ labelId: 789, labelName: 'Test Label' },
|
||||
]);
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingLabelSummaryReports: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,11 @@ const typeMap = {
|
||||
apiMethod: 'getTeamReports',
|
||||
mutationKey: 'setTeamSummaryReport',
|
||||
},
|
||||
label: {
|
||||
flagKey: 'isFetchingLabelSummaryReports',
|
||||
apiMethod: 'getLabelReports',
|
||||
mutationKey: 'setLabelSummaryReport',
|
||||
},
|
||||
};
|
||||
|
||||
async function fetchSummaryReports(type, params, { commit }) {
|
||||
@@ -38,10 +43,12 @@ export const initialState = {
|
||||
inboxSummaryReports: [],
|
||||
agentSummaryReports: [],
|
||||
teamSummaryReports: [],
|
||||
labelSummaryReports: [],
|
||||
uiFlags: {
|
||||
isFetchingInboxSummaryReports: false,
|
||||
isFetchingAgentSummaryReports: false,
|
||||
isFetchingTeamSummaryReports: false,
|
||||
isFetchingLabelSummaryReports: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,6 +62,9 @@ export const getters = {
|
||||
getTeamSummaryReports(state) {
|
||||
return state.teamSummaryReports;
|
||||
},
|
||||
getLabelSummaryReports(state) {
|
||||
return state.labelSummaryReports;
|
||||
},
|
||||
getUIFlags(state) {
|
||||
return state.uiFlags;
|
||||
},
|
||||
@@ -72,6 +82,10 @@ export const actions = {
|
||||
fetchTeamSummaryReports({ commit }, params) {
|
||||
return fetchSummaryReports('team', params, { commit });
|
||||
},
|
||||
|
||||
fetchLabelSummaryReports({ commit }, params) {
|
||||
return fetchSummaryReports('label', params, { commit });
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -84,6 +98,9 @@ export const mutations = {
|
||||
setTeamSummaryReport(state, data) {
|
||||
state.teamSummaryReports = data;
|
||||
},
|
||||
setLabelSummaryReport(state, data) {
|
||||
state.labelSummaryReports = data;
|
||||
},
|
||||
setUIFlags(state, uiFlag) {
|
||||
state.uiFlags = { ...state.uiFlags, ...uiFlag };
|
||||
},
|
||||
|
||||
@@ -344,6 +344,7 @@ Rails.application.routes.draw do
|
||||
get :agent
|
||||
get :team
|
||||
get :inbox
|
||||
get :label
|
||||
end
|
||||
end
|
||||
resources :reports, only: [:index] do
|
||||
|
||||
105
lib/seeders/reports/conversation_creator.rb
Normal file
105
lib/seeders/reports/conversation_creator.rb
Normal file
@@ -0,0 +1,105 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'faker'
|
||||
require 'active_support/testing/time_helpers'
|
||||
|
||||
class Seeders::Reports::ConversationCreator
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
def initialize(account:, resources:)
|
||||
@account = account
|
||||
@contacts = resources[:contacts]
|
||||
@inboxes = resources[:inboxes]
|
||||
@teams = resources[:teams]
|
||||
@labels = resources[:labels]
|
||||
@agents = resources[:agents]
|
||||
@priorities = [nil, 'urgent', 'high', 'medium', 'low']
|
||||
end
|
||||
|
||||
def create_conversation(created_at:)
|
||||
conversation = nil
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
travel_to(created_at) do
|
||||
conversation = build_conversation
|
||||
conversation.save!
|
||||
|
||||
add_labels_to_conversation(conversation)
|
||||
create_messages_for_conversation(conversation)
|
||||
resolve_conversation_if_needed(conversation)
|
||||
end
|
||||
|
||||
travel_back
|
||||
end
|
||||
|
||||
conversation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_conversation
|
||||
contact = @contacts.sample
|
||||
inbox = @inboxes.sample
|
||||
|
||||
contact_inbox = find_or_create_contact_inbox(contact, inbox)
|
||||
assignee = select_assignee(inbox)
|
||||
team = select_team
|
||||
priority = @priorities.sample
|
||||
|
||||
contact_inbox.conversations.new(
|
||||
account: @account,
|
||||
inbox: inbox,
|
||||
contact: contact,
|
||||
assignee: assignee,
|
||||
team: team,
|
||||
priority: priority
|
||||
)
|
||||
end
|
||||
|
||||
def find_or_create_contact_inbox(contact, inbox)
|
||||
inbox.contact_inboxes.find_or_create_by!(
|
||||
contact: contact,
|
||||
source_id: SecureRandom.hex
|
||||
)
|
||||
end
|
||||
|
||||
def select_assignee(inbox)
|
||||
rand(10) < 8 ? inbox.members.sample : nil
|
||||
end
|
||||
|
||||
def select_team
|
||||
rand(10) < 7 ? @teams.sample : nil
|
||||
end
|
||||
|
||||
def add_labels_to_conversation(conversation)
|
||||
labels_to_add = @labels.sample(rand(5..20))
|
||||
conversation.update_labels(labels_to_add.map(&:title))
|
||||
end
|
||||
|
||||
def create_messages_for_conversation(conversation)
|
||||
message_creator = Seeders::Reports::MessageCreator.new(
|
||||
account: @account,
|
||||
agents: @agents,
|
||||
conversation: conversation
|
||||
)
|
||||
message_creator.create_messages
|
||||
end
|
||||
|
||||
def resolve_conversation_if_needed(conversation)
|
||||
return unless rand < 0.7
|
||||
|
||||
resolution_delay = rand((30.minutes)..(24.hours))
|
||||
travel(resolution_delay)
|
||||
conversation.update!(status: :resolved)
|
||||
|
||||
trigger_conversation_resolved_event(conversation)
|
||||
end
|
||||
|
||||
def trigger_conversation_resolved_event(conversation)
|
||||
event_data = { conversation: conversation }
|
||||
|
||||
ReportingEventListener.instance.conversation_resolved(
|
||||
Events::Base.new('conversation_resolved', Time.current, event_data)
|
||||
)
|
||||
end
|
||||
end
|
||||
141
lib/seeders/reports/message_creator.rb
Normal file
141
lib/seeders/reports/message_creator.rb
Normal file
@@ -0,0 +1,141 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'faker'
|
||||
require 'active_support/testing/time_helpers'
|
||||
|
||||
class Seeders::Reports::MessageCreator
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
MESSAGES_PER_CONVERSATION = 5
|
||||
|
||||
def initialize(account:, agents:, conversation:)
|
||||
@account = account
|
||||
@agents = agents
|
||||
@conversation = conversation
|
||||
end
|
||||
|
||||
def create_messages
|
||||
message_count = rand(MESSAGES_PER_CONVERSATION..MESSAGES_PER_CONVERSATION + 5)
|
||||
first_agent_reply = true
|
||||
|
||||
message_count.times do |i|
|
||||
message = create_single_message(i)
|
||||
first_agent_reply = handle_reply_tracking(message, i, first_agent_reply)
|
||||
end
|
||||
end
|
||||
|
||||
def create_single_message(index)
|
||||
is_incoming = index.even?
|
||||
add_realistic_delay(index, is_incoming) if index.positive?
|
||||
create_message(is_incoming)
|
||||
end
|
||||
|
||||
def handle_reply_tracking(message, index, first_agent_reply)
|
||||
return first_agent_reply if index.even? # Skip incoming messages
|
||||
|
||||
handle_agent_reply_events(message, first_agent_reply)
|
||||
false # No longer first reply after any agent message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_realistic_delay(_message_index, is_incoming)
|
||||
delay = calculate_message_delay(is_incoming)
|
||||
travel(delay)
|
||||
end
|
||||
|
||||
def calculate_message_delay(is_incoming)
|
||||
if is_incoming
|
||||
# Customer response time: 1 minute to 4 hours
|
||||
rand((1.minute)..(4.hours))
|
||||
elsif business_hours_active?(Time.current)
|
||||
# Agent response time varies by business hours
|
||||
rand((30.seconds)..(30.minutes))
|
||||
else
|
||||
rand((1.hour)..(8.hours))
|
||||
end
|
||||
end
|
||||
|
||||
def create_message(is_incoming)
|
||||
if is_incoming
|
||||
create_incoming_message
|
||||
else
|
||||
create_outgoing_message
|
||||
end
|
||||
end
|
||||
|
||||
def create_incoming_message
|
||||
@conversation.messages.create!(
|
||||
account: @account,
|
||||
inbox: @conversation.inbox,
|
||||
message_type: :incoming,
|
||||
content: generate_message_content,
|
||||
sender: @conversation.contact
|
||||
)
|
||||
end
|
||||
|
||||
def create_outgoing_message
|
||||
sender = @conversation.assignee || @agents.sample
|
||||
|
||||
@conversation.messages.create!(
|
||||
account: @account,
|
||||
inbox: @conversation.inbox,
|
||||
message_type: :outgoing,
|
||||
content: generate_message_content,
|
||||
sender: sender
|
||||
)
|
||||
end
|
||||
|
||||
def generate_message_content
|
||||
Faker::Lorem.paragraph(sentence_count: rand(1..5))
|
||||
end
|
||||
|
||||
def handle_agent_reply_events(message, is_first_reply)
|
||||
if is_first_reply
|
||||
trigger_first_reply_event(message)
|
||||
else
|
||||
trigger_reply_event(message)
|
||||
end
|
||||
end
|
||||
|
||||
def business_hours_active?(time)
|
||||
weekday = time.wday
|
||||
hour = time.hour
|
||||
weekday.between?(1, 5) && hour.between?(9, 17)
|
||||
end
|
||||
|
||||
def trigger_first_reply_event(message)
|
||||
event_data = {
|
||||
message: message,
|
||||
conversation: message.conversation
|
||||
}
|
||||
|
||||
ReportingEventListener.instance.first_reply_created(
|
||||
Events::Base.new('first_reply_created', Time.current, event_data)
|
||||
)
|
||||
end
|
||||
|
||||
def trigger_reply_event(message)
|
||||
waiting_since = calculate_waiting_since(message)
|
||||
|
||||
event_data = {
|
||||
message: message,
|
||||
conversation: message.conversation,
|
||||
waiting_since: waiting_since
|
||||
}
|
||||
|
||||
ReportingEventListener.instance.reply_created(
|
||||
Events::Base.new('reply_created', Time.current, event_data)
|
||||
)
|
||||
end
|
||||
|
||||
def calculate_waiting_since(message)
|
||||
last_customer_message = message.conversation.messages
|
||||
.where(message_type: :incoming)
|
||||
.where('created_at < ?', message.created_at)
|
||||
.order(:created_at)
|
||||
.last
|
||||
|
||||
last_customer_message&.created_at || message.conversation.created_at
|
||||
end
|
||||
end
|
||||
234
lib/seeders/reports/report_data_seeder.rb
Normal file
234
lib/seeders/reports/report_data_seeder.rb
Normal file
@@ -0,0 +1,234 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Reports Data Seeder
|
||||
#
|
||||
# Generates realistic test data for performance testing of reports and analytics.
|
||||
# Creates conversations, messages, contacts, agents, teams, and labels with proper
|
||||
# reporting events (first response times, resolution times, etc.) using time travel
|
||||
# to generate historical data with realistic timestamps.
|
||||
#
|
||||
# Usage:
|
||||
# ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data
|
||||
#
|
||||
# This will create:
|
||||
# - 1000 conversations with realistic message exchanges
|
||||
# - 100 contacts with realistic profiles
|
||||
# - 20 agents assigned to teams and inboxes
|
||||
# - 5 teams with realistic distribution
|
||||
# - 30 labels with random assignments
|
||||
# - 3 inboxes with agent assignments
|
||||
# - Realistic reporting events with historical timestamps
|
||||
#
|
||||
# Note: This seeder clears existing data for the account before seeding.
|
||||
|
||||
require 'faker'
|
||||
require_relative 'conversation_creator'
|
||||
require_relative 'message_creator'
|
||||
|
||||
# rubocop:disable Rails/Output
|
||||
class Seeders::Reports::ReportDataSeeder
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
TOTAL_CONVERSATIONS = 1000
|
||||
TOTAL_CONTACTS = 100
|
||||
TOTAL_AGENTS = 20
|
||||
TOTAL_TEAMS = 5
|
||||
TOTAL_LABELS = 30
|
||||
TOTAL_INBOXES = 3
|
||||
MESSAGES_PER_CONVERSATION = 5
|
||||
START_DATE = 3.months.ago # rubocop:disable Rails/RelativeDateConstant
|
||||
END_DATE = Time.current
|
||||
|
||||
def initialize(account:)
|
||||
raise 'Account Seeding is not allowed.' unless ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?)
|
||||
|
||||
@account = account
|
||||
@teams = []
|
||||
@agents = []
|
||||
@labels = []
|
||||
@inboxes = []
|
||||
@contacts = []
|
||||
end
|
||||
|
||||
def perform!
|
||||
puts "Starting reports data seeding for account: #{@account.name}"
|
||||
|
||||
# Clear existing data
|
||||
clear_existing_data
|
||||
|
||||
create_teams
|
||||
create_agents
|
||||
create_labels
|
||||
create_inboxes
|
||||
create_contacts
|
||||
|
||||
create_conversations
|
||||
|
||||
puts "Completed reports data seeding for account: #{@account.name}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clear_existing_data
|
||||
puts "Clearing existing data for account: #{@account.id}"
|
||||
@account.teams.destroy_all
|
||||
@account.conversations.destroy_all
|
||||
@account.labels.destroy_all
|
||||
@account.inboxes.destroy_all
|
||||
@account.contacts.destroy_all
|
||||
@account.agents.destroy_all
|
||||
@account.reporting_events.destroy_all
|
||||
end
|
||||
|
||||
def create_teams
|
||||
TOTAL_TEAMS.times do |i|
|
||||
team = @account.teams.create!(
|
||||
name: "#{Faker::Company.industry} Team #{i + 1}"
|
||||
)
|
||||
@teams << team
|
||||
print "\rCreating teams: #{i + 1}/#{TOTAL_TEAMS}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_agents
|
||||
TOTAL_AGENTS.times do |i|
|
||||
user = create_single_agent(i)
|
||||
assign_agent_to_teams(user)
|
||||
@agents << user
|
||||
print "\rCreating agents: #{i + 1}/#{TOTAL_AGENTS}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_single_agent(index)
|
||||
random_suffix = SecureRandom.hex(4)
|
||||
user = User.create!(
|
||||
name: Faker::Name.name,
|
||||
email: "agent_#{index + 1}_#{random_suffix}@#{@account.domain || 'example.com'}",
|
||||
password: 'Password1!.',
|
||||
confirmed_at: Time.current
|
||||
)
|
||||
user.skip_confirmation!
|
||||
user.save!
|
||||
|
||||
AccountUser.create!(
|
||||
account_id: @account.id,
|
||||
user_id: user.id,
|
||||
role: :agent
|
||||
)
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def assign_agent_to_teams(user)
|
||||
teams_to_assign = @teams.sample(rand(1..3))
|
||||
teams_to_assign.each do |team|
|
||||
TeamMember.create!(
|
||||
team_id: team.id,
|
||||
user_id: user.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def create_labels
|
||||
TOTAL_LABELS.times do |i|
|
||||
label = @account.labels.create!(
|
||||
title: "Label-#{i + 1}-#{Faker::Lorem.word}",
|
||||
description: Faker::Company.catch_phrase,
|
||||
color: Faker::Color.hex_color
|
||||
)
|
||||
@labels << label
|
||||
print "\rCreating labels: #{i + 1}/#{TOTAL_LABELS}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_inboxes
|
||||
TOTAL_INBOXES.times do |_i|
|
||||
inbox = create_single_inbox
|
||||
assign_agents_to_inbox(inbox)
|
||||
@inboxes << inbox
|
||||
print "\rCreating inboxes: #{@inboxes.size}/#{TOTAL_INBOXES}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_single_inbox
|
||||
channel = Channel::WebWidget.create!(
|
||||
website_url: "https://#{Faker::Internet.domain_name}",
|
||||
account_id: @account.id
|
||||
)
|
||||
|
||||
@account.inboxes.create!(
|
||||
name: "#{Faker::Company.name} Website",
|
||||
channel: channel
|
||||
)
|
||||
end
|
||||
|
||||
def assign_agents_to_inbox(inbox)
|
||||
agents_to_assign = if @inboxes.empty?
|
||||
# First inbox gets all agents to ensure coverage
|
||||
@agents
|
||||
else
|
||||
# Subsequent inboxes get random selection with some overlap
|
||||
min_agents = [@agents.size / TOTAL_INBOXES, 10].max
|
||||
max_agents = [(@agents.size * 0.8).to_i, 50].min
|
||||
@agents.sample(rand(min_agents..max_agents))
|
||||
end
|
||||
|
||||
agents_to_assign.each do |agent|
|
||||
InboxMember.create!(inbox: inbox, user: agent)
|
||||
end
|
||||
end
|
||||
|
||||
def create_contacts
|
||||
TOTAL_CONTACTS.times do |i|
|
||||
contact = @account.contacts.create!(
|
||||
name: Faker::Name.name,
|
||||
email: Faker::Internet.email,
|
||||
phone_number: Faker::PhoneNumber.cell_phone_in_e164,
|
||||
identifier: SecureRandom.uuid,
|
||||
additional_attributes: {
|
||||
company: Faker::Company.name,
|
||||
city: Faker::Address.city,
|
||||
country: Faker::Address.country,
|
||||
customer_since: Faker::Date.between(from: 2.years.ago, to: Time.zone.today)
|
||||
}
|
||||
)
|
||||
@contacts << contact
|
||||
|
||||
print "\rCreating contacts: #{i + 1}/#{TOTAL_CONTACTS}"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
|
||||
def create_conversations
|
||||
conversation_creator = Seeders::Reports::ConversationCreator.new(
|
||||
account: @account,
|
||||
resources: {
|
||||
contacts: @contacts,
|
||||
inboxes: @inboxes,
|
||||
teams: @teams,
|
||||
labels: @labels,
|
||||
agents: @agents
|
||||
}
|
||||
)
|
||||
|
||||
TOTAL_CONVERSATIONS.times do |i|
|
||||
created_at = Faker::Time.between(from: START_DATE, to: END_DATE)
|
||||
conversation_creator.create_conversation(created_at: created_at)
|
||||
|
||||
completion_percentage = ((i + 1).to_f / TOTAL_CONVERSATIONS * 100).round
|
||||
print "\rCreating conversations: #{i + 1}/#{TOTAL_CONVERSATIONS} (#{completion_percentage}%)"
|
||||
end
|
||||
|
||||
print "\n"
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/Output
|
||||
24
lib/tasks/seed_reports_data.rake
Normal file
24
lib/tasks/seed_reports_data.rake
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace :db do
|
||||
namespace :seed do
|
||||
desc 'Seed test data for reports with conversations, contacts, agents, teams, and realistic reporting events'
|
||||
task reports_data: :environment do
|
||||
if ENV['ACCOUNT_ID'].blank?
|
||||
puts 'Please provide an ACCOUNT_ID environment variable'
|
||||
puts 'Usage: ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data'
|
||||
exit 1
|
||||
end
|
||||
|
||||
ENV['ENABLE_ACCOUNT_SEEDING'] = 'true' if ENV['ENABLE_ACCOUNT_SEEDING'].blank?
|
||||
|
||||
account_id = ENV.fetch('ACCOUNT_ID', nil)
|
||||
account = Account.find(account_id)
|
||||
|
||||
puts "Starting reports data seeding for account: #{account.name} (ID: #{account.id})"
|
||||
|
||||
seeder = Seeders::Reports::ReportDataSeeder.new(account: account)
|
||||
seeder.perform!
|
||||
|
||||
puts "Finished seeding reports data for account: #{account.name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
317
spec/builders/v2/reports/label_summary_builder_spec.rb
Normal file
317
spec/builders/v2/reports/label_summary_builder_spec.rb
Normal file
@@ -0,0 +1,317 @@
|
||||
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',
|
||||
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
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user