diff --git a/app/builders/v2/reports/agent_summary_builder.rb b/app/builders/v2/reports/agent_summary_builder.rb new file mode 100644 index 000000000..382c67bf5 --- /dev/null +++ b/app/builders/v2/reports/agent_summary_builder.rb @@ -0,0 +1,53 @@ +class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder + pattr_initialize [:account!, :params!] + + def build + set_grouped_conversations_count + set_grouped_avg_reply_time + set_grouped_avg_first_response_time + set_grouped_avg_resolution_time + prepare_report + end + + private + + def set_grouped_conversations_count + @grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count + end + + def set_grouped_avg_resolution_time + @grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved')) + end + + def set_grouped_avg_first_response_time + @grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response')) + end + + def set_grouped_avg_reply_time + @grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time')) + end + + def group_by_key + :user_id + end + + def reporting_events + @reporting_events ||= Current.account.reporting_events.where(created_at: range) + end + + def prepare_report + account.account_users.each_with_object([]) do |account_user, arr| + arr << { + id: account_user.user_id, + conversations_count: @grouped_conversations_count[account_user.user_id], + avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id], + avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id], + avg_reply_time: @grouped_avg_reply_time[account_user.user_id] + } + end + end + + def average_value_key + ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value + end +end diff --git a/app/builders/v2/reports/base_summary_builder.rb b/app/builders/v2/reports/base_summary_builder.rb new file mode 100644 index 000000000..33fe5e400 --- /dev/null +++ b/app/builders/v2/reports/base_summary_builder.rb @@ -0,0 +1,17 @@ +class V2::Reports::BaseSummaryBuilder + include DateRangeHelper + + private + + def group_by_key + # Override this method + end + + def get_grouped_average(events) + events.group(group_by_key).average(average_value_key) + end + + def average_value_key + params[:business_hours].present? ? :value_in_business_hours : :value + end +end diff --git a/app/builders/v2/reports/team_summary_builder.rb b/app/builders/v2/reports/team_summary_builder.rb new file mode 100644 index 000000000..3d7e765b3 --- /dev/null +++ b/app/builders/v2/reports/team_summary_builder.rb @@ -0,0 +1,49 @@ +class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder + pattr_initialize [:account!, :params!] + + def build + set_grouped_conversations_count + set_grouped_avg_reply_time + set_grouped_avg_first_response_time + set_grouped_avg_resolution_time + prepare_report + end + + private + + def set_grouped_conversations_count + @grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count + end + + def set_grouped_avg_resolution_time + @grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved')) + end + + def set_grouped_avg_first_response_time + @grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response')) + end + + def set_grouped_avg_reply_time + @grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time')) + end + + def reporting_events + @reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation) + end + + def group_by_key + 'conversations.team_id' + end + + def prepare_report + account.teams.each_with_object([]) do |team, arr| + arr << { + id: team.id, + conversations_count: @grouped_conversations_count[team.id], + avg_resolution_time: @grouped_avg_resolution_time[team.id], + avg_first_response_time: @grouped_avg_first_response_time[team.id], + avg_reply_time: @grouped_avg_reply_time[team.id] + } + end + end +end diff --git a/app/controllers/api/v2/accounts/summary_reports_controller.rb b/app/controllers/api/v2/accounts/summary_reports_controller.rb new file mode 100644 index 000000000..0cbd6dd8e --- /dev/null +++ b/app/controllers/api/v2/accounts/summary_reports_controller.rb @@ -0,0 +1,36 @@ +class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController + before_action :check_authorization + before_action :prepare_builder_params, only: [:agent, :team] + + def agent + render_report_with(V2::Reports::AgentSummaryBuilder) + end + + def team + render_report_with(V2::Reports::TeamSummaryBuilder) + end + + private + + def check_authorization + authorize :report, :view? + end + + def prepare_builder_params + @builder_params = { + since: permitted_params[:since], + until: permitted_params[:until], + business_hours: ActiveModel::Type::Boolean.new.cast(permitted_params[:business_hours]) + } + end + + def render_report_with(builder_class) + builder = builder_class.new(account: Current.account, params: @builder_params) + data = builder.build + render json: data + end + + def permitted_params + params.permit(:since, :until, :business_hours) + end +end diff --git a/app/policies/report_policy.rb b/app/policies/report_policy.rb new file mode 100644 index 000000000..7e7eeffdf --- /dev/null +++ b/app/policies/report_policy.rb @@ -0,0 +1,5 @@ +class ReportPolicy < ApplicationPolicy + def view? + @account_user.administrator? + end +end diff --git a/config/routes.rb b/config/routes.rb index 210d5bc0e..8c4d4124b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -294,6 +294,12 @@ Rails.application.routes.draw do namespace :v2 do resources :accounts, only: [:create] do scope module: :accounts do + resources :summary_reports, only: [] do + collection do + get :agent + get :team + end + end resources :reports, only: [:index] do collection do get :summary diff --git a/spec/controllers/api/v2/accounts/summary_reports_controller_spec.rb b/spec/controllers/api/v2/accounts/summary_reports_controller_spec.rb new file mode 100644 index 000000000..23ed3e65c --- /dev/null +++ b/spec/controllers/api/v2/accounts/summary_reports_controller_spec.rb @@ -0,0 +1,112 @@ +require 'rails_helper' + +RSpec.describe 'Summary Reports API', type: :request do + let(:account) { create(:account) } + let(:admin) { create(:user, account: account, role: :administrator) } + let(:agent) { create(:user, account: account, role: :agent) } + let(:default_timezone) { ActiveSupport::TimeZone[0]&.name } + let(:start_of_today) { Time.current.in_time_zone(default_timezone).beginning_of_day.to_i } + let(:end_of_today) { Time.current.in_time_zone(default_timezone).end_of_day.to_i } + + describe 'GET /api/v2/accounts/:account_id/summary_reports/agent' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v2/accounts/#{account.id}/summary_reports/agent" + + 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 agents' do + get "/api/v2/accounts/#{account.id}/summary_reports/agent", + params: params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + + it 'calls V2::Reports::AgentSummaryBuilder with the right params if the user is an admin' do + agent_summary_builder = double + allow(V2::Reports::AgentSummaryBuilder).to receive(:new).and_return(agent_summary_builder) + allow(agent_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }]) + + get "/api/v2/accounts/#{account.id}/summary_reports/agent", + params: params, + headers: admin.create_new_auth_token, + as: :json + + expect(V2::Reports::AgentSummaryBuilder).to have_received(:new).with(account: account, params: params) + expect(agent_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 + get "/api/v2/accounts/#{account.id}/summary_reports/team" + + 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 agents' do + get "/api/v2/accounts/#{account.id}/summary_reports/team", + params: params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + + it 'calls V2::Reports::TeamSummaryBuilder with the right params if the user is an admin' do + team_summary_builder = double + allow(V2::Reports::TeamSummaryBuilder).to receive(:new).and_return(team_summary_builder) + allow(team_summary_builder).to receive(:build).and_return([{ id: 1, conversations_count: 110 }]) + + get "/api/v2/accounts/#{account.id}/summary_reports/team", + params: params, + headers: admin.create_new_auth_token, + as: :json + + expect(V2::Reports::TeamSummaryBuilder).to have_received(:new).with(account: account, params: params) + expect(team_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 +end