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