feat: Add new APIs for live reports with team filter (#10994)

This PR is part of the larger #10849 implementation and introduces a new
Live Reports API to provide real-time conversation metrics.

The /live_reports/conversation_metrics endpoint returns account-level or
team-level conversation statistics, including open, pending, unattended,
and unassigned conversation counts.

The /live_reports/grouped_conversation_metrics endpoint accepts a group
parameter, either team_id or assignee_id, and returns open and
unattended conversation counts based on the specified grouping.
This commit is contained in:
Pranav
2025-02-27 16:11:04 -08:00
committed by GitHub
parent 0fbb9b91b2
commit 80c87da8c9
4 changed files with 242 additions and 1 deletions

View File

@@ -0,0 +1,64 @@
class Api::V2::Accounts::LiveReportsController < Api::V1::Accounts::BaseController
before_action :load_conversations, only: [:conversation_metrics, :grouped_conversation_metrics]
before_action :set_group_scope, only: [:grouped_conversation_metrics]
before_action :check_authorization
def conversation_metrics
render json: {
open: @conversations.open.count,
unattended: @conversations.open.unattended.count,
unassigned: @conversations.open.unassigned.count,
pending: @conversations.pending.count
}
end
def grouped_conversation_metrics
count_by_group = @conversations.open.group(@group_scope).count
unattended_by_group = @conversations.open.unattended.group(@group_scope).count
unassigned_by_group = @conversations.open.unassigned.group(@group_scope).count
group_metrics = count_by_group.map do |group_id, count|
metric = {
open: count,
unattended: unattended_by_group[group_id] || 0,
unassigned: unassigned_by_group[group_id] || 0
}
metric[@group_scope] = group_id
metric
end
render json: group_metrics
end
private
def check_authorization
authorize :report, :view?
end
def set_group_scope
render json: { error: 'invalid group_by' }, status: :unprocessable_entity and return unless %w[
team_id
assignee_id
].include?(permitted_params[:group_by])
@group_scope = permitted_params[:group_by]
end
def team
return unless permitted_params[:team_id]
@team ||= Current.account.teams.find(permitted_params[:team_id])
end
def load_conversations
scope = Current.account.conversations
scope = scope.where(team_id: team.id) if team.present?
@conversations = scope
end
def permitted_params
params.permit(:team_id, :group_by)
end
end

View File

@@ -337,6 +337,12 @@ Rails.application.routes.draw do
get :bot_metrics
end
end
resources :live_reports, only: [] do
collection do
get :conversation_metrics
get :grouped_conversation_metrics
end
end
end
end
end

View File

@@ -0,0 +1,171 @@
require 'rails_helper'
RSpec.describe 'Api::V2::Accounts::LiveReports', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:team) { create(:team, account: account) }
let(:team_member) { create(:team_member, team: team, user: admin) }
describe 'GET /api/v2/accounts/{account.id}/live_reports/conversation_metrics' do
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated but not authorized' do
it 'returns forbidden' do
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated and authorized' do
before do
create(:conversation, :with_assignee, account: account, status: :open)
create(:conversation, account: account, status: :open)
create(:conversation, :with_assignee, account: account, status: :pending)
create(:conversation, :with_assignee, account: account, status: :open) do |conversation|
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
end
it 'returns conversation metrics' do
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['open']).to eq(3)
expect(response_data['unattended']).to eq(2)
expect(response_data['unassigned']).to eq(1)
expect(response_data['pending']).to eq(1)
end
context 'with team_id parameter' do
before do
create(:conversation, account: account, status: :open, team_id: team.id)
create(:conversation, account: account, status: :open)
end
it 'returns metrics filtered by team' do
get "/api/v2/accounts/#{account.id}/live_reports/conversation_metrics",
params: { team_id: team.id },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['open']).to eq(1)
expect(response_data['unattended']).to eq(1)
expect(response_data['unassigned']).to eq(1)
expect(response_data['pending']).to eq(0)
end
end
end
end
describe 'GET /api/v2/accounts/{account.id}/live_reports/grouped_conversation_metrics' do
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'team_id' }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated but not authorized' do
it 'returns forbidden' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'team_id' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'with invalid group_by parameter' do
it 'returns unprocessable_entity error' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'invalid_param' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('invalid group_by')
end
end
context 'when grouped by team_id' do
let(:assignee1) { create(:user, account: account) }
before do
create(:conversation, account: account, status: :open, team_id: team.id)
create(:conversation, account: account, status: :open, team_id: team.id)
create(:conversation, account: account, status: :open, team_id: team.id) do |conversation|
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
create(:conversation, account: account, status: :open, assignee_id: assignee1.id)
create(:conversation, account: account, status: :open) do |conversation|
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
end
it 'returns metrics grouped by team' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'team_id' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.size).to eq(2)
expect(response_data).to include(
{ 'team_id' => nil, 'open' => 2, 'unattended' => 1, 'unassigned' => 1 }
)
expect(response_data).to include(
{ 'team_id' => team.id, 'open' => 3, 'unattended' => 2, 'unassigned' => 3 }
)
end
end
context 'when filtering by assignee_id' do
let(:assignee1) { create(:user, account: account) }
before do
create(:conversation, assignee_id: agent.id, account: account, status: :open)
create(:conversation, account: account, status: :open)
create(:conversation, assignee_id: agent.id, account: account, status: :open) do |conversation|
create(:message, account: account, conversation: conversation, message_type: :outgoing)
end
end
it 'returns metrics grouped by assignee' do
get "/api/v2/accounts/#{account.id}/live_reports/grouped_conversation_metrics",
params: { group_by: 'assignee_id' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.size).to eq 2
expect(response_data).to include(
{ 'assignee_id' => agent.id, 'open' => 2, 'unassigned' => 0, 'unattended' => 1 }
)
expect(response_data).to include(
{ 'assignee_id' => nil, 'open' => 1, 'unassigned' => 1, 'unattended' => 1 }
)
end
end
end
end

View File

@@ -1,6 +1,6 @@
FactoryBot.define do
factory :team do
name { 'MyString' }
sequence(:name) { |n| "Team #{n}" }
description { 'MyText' }
allow_auto_assign { true }
account