From e59df461f8fb3ec796d5c7a664c51c504af95b78 Mon Sep 17 00:00:00 2001 From: Muhsin Date: Fri, 30 Jan 2026 15:18:10 +0400 Subject: [PATCH] feat: Change API --- .../outgoing_messages_count_builder.rb | 81 ++++++++++++ .../api/v2/accounts/reports_controller.rb | 13 ++ config/routes.rb | 1 + .../v2/accounts/reports_controller_spec.rb | 119 ++++++++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 app/builders/v2/reports/outgoing_messages_count_builder.rb diff --git a/app/builders/v2/reports/outgoing_messages_count_builder.rb b/app/builders/v2/reports/outgoing_messages_count_builder.rb new file mode 100644 index 000000000..74b271faa --- /dev/null +++ b/app/builders/v2/reports/outgoing_messages_count_builder.rb @@ -0,0 +1,81 @@ +class V2::Reports::OutgoingMessagesCountBuilder + include DateRangeHelper + attr_reader :account, :params + + def initialize(account, params) + @account = account + @params = params + end + + def build + send("build_by_#{params[:group_by]}") + end + + private + + def base_messages + scope = account.messages.outgoing.unscope(:order).where(created_at: range) + scope = scope.where.not(sender_type: ['AgentBot', 'Captain::Assistant']) if params[:group_by].to_s == 'agent' + scope + end + + def build_by_agent + counts = base_messages + .joins('INNER JOIN conversations ON messages.conversation_id = conversations.id') + .where.not(conversations: { assignee_id: nil }) + .group('conversations.assignee_id') + .count + + user_names = account.users.where(id: counts.keys).index_by(&:id) + + counts.map do |user_id, count| + user = user_names[user_id] + { id: user_id, name: user&.name, outgoing_messages_count: count } + end + end + + def build_by_team + counts = base_messages + .joins('INNER JOIN conversations ON messages.conversation_id = conversations.id') + .where.not(conversations: { team_id: nil }) + .group('conversations.team_id') + .count + + team_names = account.teams.where(id: counts.keys).index_by(&:id) + + counts.map do |team_id, count| + team = team_names[team_id] + { id: team_id, name: team&.name, outgoing_messages_count: count } + end + end + + def build_by_inbox + counts = base_messages + .group(:inbox_id) + .count + + inbox_names = account.inboxes.where(id: counts.keys).index_by(&:id) + + counts.map do |inbox_id, count| + inbox = inbox_names[inbox_id] + { id: inbox_id, name: inbox&.name, outgoing_messages_count: count } + end + end + + def build_by_label + counts = base_messages + .joins('INNER JOIN conversations ON messages.conversation_id = conversations.id') + .joins("INNER JOIN taggings ON taggings.taggable_id = conversations.id + AND taggings.taggable_type = 'Conversation' AND taggings.context = 'labels'") + .joins('INNER JOIN tags ON tags.id = taggings.tag_id') + .group('tags.name') + .count + + label_ids = account.labels.where(title: counts.keys).index_by(&:title) + + counts.map do |label_name, count| + label = label_ids[label_name] + { id: label&.id, name: label_name, outgoing_messages_count: count } + end + end +end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index 714aeb0c9..78a6c2c01 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -62,6 +62,11 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController render json: bot_metrics end + def outgoing_messages_count + builder = V2::Reports::OutgoingMessagesCountBuilder.new(Current.account, outgoing_messages_count_params) + render json: builder.build + end + private def generate_csv(filename, template) @@ -139,4 +144,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController def conversation_metrics V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics end + + def outgoing_messages_count_params + { + group_by: params[:group_by], + since: params[:since], + until: params[:until] + } + end end diff --git a/config/routes.rb b/config/routes.rb index 83aed5b79..0057e593f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -444,6 +444,7 @@ Rails.application.routes.draw do get :conversations_summary get :conversation_traffic get :bot_metrics + get :outgoing_messages_count end end resource :year_in_review, only: [:show] diff --git a/spec/controllers/api/v2/accounts/reports_controller_spec.rb b/spec/controllers/api/v2/accounts/reports_controller_spec.rb index 2d505822f..95b5e064b 100644 --- a/spec/controllers/api/v2/accounts/reports_controller_spec.rb +++ b/spec/controllers/api/v2/accounts/reports_controller_spec.rb @@ -196,4 +196,123 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do end end end + + describe 'GET /api/v2/accounts/{account.id}/reports/outgoing_messages_count' do + let(:since_epoch) { 1.week.ago.to_i.to_s } + let(:until_epoch) { 1.day.from_now.to_i.to_s } + + context 'when unauthenticated' do + it 'returns unauthorized' do + get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count", + params: { group_by: 'agent', since: since_epoch, until: until_epoch } + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count", + params: { group_by: 'agent', since: since_epoch, until: until_epoch }, + headers: agent.create_new_auth_token, as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as admin' do + let(:agent2) { create(:user, account: account, role: :agent) } + let(:team) { create(:team, account: account) } + let(:inbox2) { create(:inbox, account: account) } + + # Separate conversations for agent and team grouping because + # model callbacks clear assignee_id when team is set. + before do + conv_agent = create(:conversation, account: account, inbox: inbox, assignee: agent) + conv_agent2 = create(:conversation, account: account, inbox: inbox2, assignee: agent2) + conv_team = create(:conversation, account: account, inbox: inbox, team: team) + + create_list(:message, 3, account: account, conversation: conv_agent, inbox: inbox, message_type: :outgoing) + create_list(:message, 2, account: account, conversation: conv_agent2, inbox: inbox2, message_type: :outgoing) + create_list(:message, 4, account: account, conversation: conv_team, inbox: inbox, message_type: :outgoing) + # incoming message should not be counted + create(:message, account: account, conversation: conv_agent, inbox: inbox, message_type: :incoming) + end + + it 'returns outgoing message counts grouped by agent' do + get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count", + params: { group_by: 'agent', since: since_epoch, until: until_epoch }, + headers: admin.create_new_auth_token, as: :json + + expect(response).to have_http_status(:success) + data = response.parsed_body + expect(data).to be_an(Array) + + agent_entry = data.find { |e| e['id'] == agent.id } + agent2_entry = data.find { |e| e['id'] == agent2.id } + expect(agent_entry['outgoing_messages_count']).to eq(3) + expect(agent2_entry['outgoing_messages_count']).to eq(2) + end + + it 'returns outgoing message counts grouped by team' do + get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count", + params: { group_by: 'team', since: since_epoch, until: until_epoch }, + headers: admin.create_new_auth_token, as: :json + + expect(response).to have_http_status(:success) + data = response.parsed_body + expect(data).to be_an(Array) + expect(data.length).to eq(1) + expect(data.first['id']).to eq(team.id) + expect(data.first['outgoing_messages_count']).to eq(4) + end + + it 'returns outgoing message counts grouped by inbox' do + get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count", + params: { group_by: 'inbox', since: since_epoch, until: until_epoch }, + headers: admin.create_new_auth_token, as: :json + + expect(response).to have_http_status(:success) + data = response.parsed_body + expect(data).to be_an(Array) + + inbox_entry = data.find { |e| e['id'] == inbox.id } + inbox2_entry = data.find { |e| e['id'] == inbox2.id } + expect(inbox_entry['outgoing_messages_count']).to eq(7) + expect(inbox2_entry['outgoing_messages_count']).to eq(2) + end + + it 'returns outgoing message counts grouped by label' do + label = create(:label, account: account, title: 'support') + conversation = account.conversations.first + conversation.label_list.add('support') + conversation.save! + + get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count", + params: { group_by: 'label', since: since_epoch, until: until_epoch }, + headers: admin.create_new_auth_token, as: :json + + expect(response).to have_http_status(:success) + data = response.parsed_body + expect(data).to be_an(Array) + expect(data.length).to eq(1) + expect(data.first['id']).to eq(label.id) + expect(data.first['name']).to eq('support') + end + + it 'excludes bot messages when grouped by agent' do + bot = create(:agent_bot) + bot_conversation = create(:conversation, account: account, inbox: inbox, assignee: agent) + create(:message, account: account, conversation: bot_conversation, inbox: inbox, + message_type: :outgoing, sender: bot) + + get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count", + params: { group_by: 'agent', since: since_epoch, until: until_epoch }, + headers: admin.create_new_auth_token, as: :json + + data = response.parsed_body + agent_entry = data.find { |e| e['id'] == agent.id } + # 3 from before block; bot message excluded + expect(agent_entry['outgoing_messages_count']).to eq(3) + end + end + end end