From 4fd9bddb9de08666f7b65ba4b3509f02080ea62d Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 19 Dec 2024 14:47:19 -0800 Subject: [PATCH] feat(v4): Add API to fetch aggregate reports for inboxes (#10604) The Inbox Overview section is being updated to offer a more detailed report, showing an overall view of the account grouped by inboxes. To view detailed reports and access specific graphs for individual inboxes, click on the inbox name to navigate to its dedicated report page. --------- Co-authored-by: Sojan Jose --- .../v2/reports/inbox_summary_builder.rb | 62 +++++++++++ .../v2/accounts/summary_reports_controller.rb | 9 +- config/routes.rb | 1 + .../v2/reports/inbox_summary_builder_spec.rb | 101 ++++++++++++++++++ .../summary_reports_controller_spec.rb | 51 +++++++++ 5 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 app/builders/v2/reports/inbox_summary_builder.rb create mode 100644 spec/builders/v2/reports/inbox_summary_builder_spec.rb diff --git a/app/builders/v2/reports/inbox_summary_builder.rb b/app/builders/v2/reports/inbox_summary_builder.rb new file mode 100644 index 000000000..489350670 --- /dev/null +++ b/app/builders/v2/reports/inbox_summary_builder.rb @@ -0,0 +1,62 @@ +class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder + pattr_initialize [:account!, :params!] + + def build + load_data + prepare_report + end + + private + + attr_reader :conversations_count, :resolved_count, + :avg_resolution_time, :avg_first_response_time, :avg_reply_time + + def load_data + @conversations_count = fetch_conversations_count + @resolved_count = fetch_resolved_count + @avg_resolution_time = fetch_average_time('conversation_resolved') + @avg_first_response_time = fetch_average_time('first_response') + @avg_reply_time = fetch_average_time('reply_time') + end + + def fetch_conversations_count + account.conversations.where(created_at: range).group(group_by_key).count + end + + def fetch_resolved_count + reporting_events.where(name: 'conversation_resolved').group(group_by_key).count + end + + def fetch_average_time(event_name) + get_grouped_average(reporting_events.where(name: event_name)) + end + + def reporting_events + @reporting_events ||= account.reporting_events.where(created_at: range) + end + + def prepare_report + account.inboxes.map do |inbox| + build_inbox_stats(inbox) + end + end + + def build_inbox_stats(inbox) + { + id: inbox.id, + conversations_count: conversations_count[inbox.id] || 0, + resolved_conversations_count: resolved_count[inbox.id] || 0, + avg_resolution_time: avg_resolution_time[inbox.id], + avg_first_response_time: avg_first_response_time[inbox.id], + avg_reply_time: avg_reply_time[inbox.id] + } + end + + def group_by_key + :inbox_id + end + + def average_value_key + ActiveModel::Type::Boolean.new.cast(params[:business_hours]) ? :value_in_business_hours : :value + end +end diff --git a/app/controllers/api/v2/accounts/summary_reports_controller.rb b/app/controllers/api/v2/accounts/summary_reports_controller.rb index 0cbd6dd8e..989952cfd 100644 --- a/app/controllers/api/v2/accounts/summary_reports_controller.rb +++ b/app/controllers/api/v2/accounts/summary_reports_controller.rb @@ -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] + before_action :prepare_builder_params, only: [:agent, :team, :inbox] def agent render_report_with(V2::Reports::AgentSummaryBuilder) @@ -10,6 +10,10 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr render_report_with(V2::Reports::TeamSummaryBuilder) end + def inbox + render_report_with(V2::Reports::InboxSummaryBuilder) + end + private def check_authorization @@ -26,8 +30,7 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr def render_report_with(builder_class) builder = builder_class.new(account: Current.account, params: @builder_params) - data = builder.build - render json: data + render json: builder.build end def permitted_params diff --git a/config/routes.rb b/config/routes.rb index d33a40827..0c2c7804e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -328,6 +328,7 @@ Rails.application.routes.draw do collection do get :agent get :team + get :inbox end end resources :reports, only: [:index] do diff --git a/spec/builders/v2/reports/inbox_summary_builder_spec.rb b/spec/builders/v2/reports/inbox_summary_builder_spec.rb new file mode 100644 index 000000000..9421b96f6 --- /dev/null +++ b/spec/builders/v2/reports/inbox_summary_builder_spec.rb @@ -0,0 +1,101 @@ +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 eq( + [ + { + 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 eq( + [ + { + 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 diff --git a/spec/controllers/api/v2/accounts/summary_reports_controller_spec.rb b/spec/controllers/api/v2/accounts/summary_reports_controller_spec.rb index 23ed3e65c..f6c429e79 100644 --- a/spec/controllers/api/v2/accounts/summary_reports_controller_spec.rb +++ b/spec/controllers/api/v2/accounts/summary_reports_controller_spec.rb @@ -59,6 +59,57 @@ RSpec.describe 'Summary Reports API', type: :request do end end + describe 'GET /api/v2/accounts/:account_id/summary_reports/inbox' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v2/accounts/#{account.id}/summary_reports/inbox" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:params) do + { + since: start_of_today.to_s, + until: end_of_today.to_s, + business_hours: true + } + end + + it 'returns unauthorized for inbox' do + get "/api/v2/accounts/#{account.id}/summary_reports/inbox", + params: params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + + it 'calls V2::Reports::InboxSummaryBuilder with the right params if the user is an admin' do + inbox_summary_builder = double + allow(V2::Reports::InboxSummaryBuilder).to receive(:new).and_return(inbox_summary_builder) + allow(inbox_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }]) + + get "/api/v2/accounts/#{account.id}/summary_reports/inbox", + params: params, + headers: admin.create_new_auth_token, + as: :json + + expect(V2::Reports::InboxSummaryBuilder).to have_received(:new).with(account: account, params: params) + expect(inbox_summary_builder).to have_received(:build) + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(1) + expect(json_response.first['conversations_count']).to eq(110) + expect(json_response.first['avg_reply_time']).to be_nil + end + end + end + describe 'GET /api/v2/accounts/:account_id/summary_reports/team' do context 'when it is an unauthenticated user' do it 'returns unauthorized' do