From 48452a42f4ee37ec56735b578d6416dd54155f18 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 25 Mar 2024 12:24:43 +0530 Subject: [PATCH] feat: Add SLA metrics API (#9132) --- config/routes.rb | 5 + .../v1/accounts/applied_slas_controller.rb | 58 ++++++ enterprise/app/models/applied_sla.rb | 11 + .../accounts/applied_slas/index.json.jbuilder | 14 ++ .../applied_slas/metrics.json.jbuilder | 3 + .../api/v1/models/_sla_event.json.jbuilder | 5 + .../accounts/applied_slas_controller_spec.rb | 188 ++++++++++++++++++ 7 files changed, 284 insertions(+) create mode 100644 enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb create mode 100644 enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/applied_slas/metrics.json.jbuilder create mode 100644 enterprise/app/views/api/v1/models/_sla_event.json.jbuilder create mode 100644 spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb diff --git a/config/routes.rb b/config/routes.rb index cfa14c854..6be458e07 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -144,6 +144,11 @@ Rails.application.routes.draw do get :download end end + resources :applied_slas, only: [:index] do + collection do + get :metrics + end + end resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy] resources :custom_filters, only: [:index, :show, :create, :update, :destroy] resources :inboxes, only: [:index, :show, :create, :update, :destroy] do diff --git a/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb b/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb new file mode 100644 index 000000000..5eaaf2d68 --- /dev/null +++ b/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb @@ -0,0 +1,58 @@ +class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAccountsController + include Sift + include DateRangeHelper + + RESULTS_PER_PAGE = 25 + + before_action :set_applied_slas, only: [:index, :metrics] + before_action :set_current_page, only: [:index] + before_action :paginate_slas, only: [:index] + before_action :check_admin_authorization? + + sort_on :created_at, type: :datetime + + def index; end + + def metrics + @total_applied_slas = total_applied_slas + @number_of_sla_breaches = number_of_sla_breaches + @hit_rate = hit_rate + end + + private + + def total_applied_slas + @total_applied_slas ||= @applied_slas.count + end + + def number_of_sla_breaches + @number_of_sla_breaches ||= @applied_slas.missed.count + end + + def hit_rate + number_of_sla_breaches.zero? ? '100%' : "#{hit_rate_percentage}%" + end + + def hit_rate_percentage + ((total_applied_slas - number_of_sla_breaches) / total_applied_slas.to_f * 100).round(2) + end + + def set_applied_slas + initial_query = Current.account.applied_slas.includes(:conversation) + @applied_slas = initial_query + .filter_by_date_range(range) + .filter_by_inbox_id(params[:inbox_id]) + .filter_by_team_id(params[:team_id]) + .filter_by_sla_policy_id(params[:sla_policy_id]) + .filter_by_label_list(params[:label_list]) + .filter_by_assigned_agent_id(params[:assigned_agent_id]) + end + + def paginate_slas + @applied_slas = @applied_slas.page(@current_page).per(RESULTS_PER_PAGE) + end + + def set_current_page + @current_page = params[:page] || 1 + end +end diff --git a/enterprise/app/models/applied_sla.rb b/enterprise/app/models/applied_sla.rb index c2dde8377..4558d5d23 100644 --- a/enterprise/app/models/applied_sla.rb +++ b/enterprise/app/models/applied_sla.rb @@ -29,6 +29,17 @@ class AppliedSla < ApplicationRecord enum sla_status: { active: 0, hit: 1, missed: 2 } + scope :filter_by_date_range, ->(range) { where(created_at: range) if range.present? } + scope :filter_by_inbox_id, ->(inbox_id) { where(inbox_id: inbox_id) if inbox_id.present? } + scope :filter_by_team_id, ->(team_id) { where(team_id: team_id) if team_id.present? } + scope :filter_by_sla_policy_id, ->(sla_policy_id) { where(sla_policy_id: sla_policy_id) if sla_policy_id.present? } + scope :filter_by_label_list, ->(label_list) { joins(:conversation).where(conversations: { cached_label_list: label_list }) if label_list.present? } + scope :filter_by_assigned_agent_id, lambda { |assigned_agent_id| + if assigned_agent_id.present? + joins(:conversation).where(conversations: { assigned_agent_id: assigned_agent_id }) + end + } + scope :missed, -> { where(sla_status: :missed) } private def ensure_account_id diff --git a/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder new file mode 100644 index 000000000..e9a905d56 --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder @@ -0,0 +1,14 @@ +json.array! @applied_slas do |applied_sla| + json.id applied_sla.id + json.sla_policy_id applied_sla.sla_policy_id + json.conversation_id applied_sla.conversation_id + json.sla_status applied_sla.sla_status + json.created_at applied_sla.created_at + json.updated_at applied_sla.updated_at + json.conversation do + json.partial! 'api/v1/models/conversation', conversation: applied_sla.conversation + end + json.sla_events applied_sla.sla_events do |sla_event| + json.partial! 'api/v1/models/sla_event', formats: [:json], sla_event: sla_event + end +end diff --git a/enterprise/app/views/api/v1/accounts/applied_slas/metrics.json.jbuilder b/enterprise/app/views/api/v1/accounts/applied_slas/metrics.json.jbuilder new file mode 100644 index 000000000..13f184845 --- /dev/null +++ b/enterprise/app/views/api/v1/accounts/applied_slas/metrics.json.jbuilder @@ -0,0 +1,3 @@ +json.total_applied_slas @total_applied_slas +json.number_of_sla_breaches @number_of_sla_breaches +json.hit_rate @hit_rate diff --git a/enterprise/app/views/api/v1/models/_sla_event.json.jbuilder b/enterprise/app/views/api/v1/models/_sla_event.json.jbuilder new file mode 100644 index 000000000..e51defc0a --- /dev/null +++ b/enterprise/app/views/api/v1/models/_sla_event.json.jbuilder @@ -0,0 +1,5 @@ +json.id sla_event.id +json.event_type sla_event.event_type +json.meta sla_event.meta +json.updated_at sla_event.updated_at.to_i +json.created_at sla_event.created_at.to_i diff --git a/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb new file mode 100644 index 000000000..e10d08f85 --- /dev/null +++ b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb @@ -0,0 +1,188 @@ +require 'rails_helper' + +RSpec.describe 'Applied SLAs API', type: :request do + let(:account) { create(:account) } + let(:administrator) { create(:user, account: account, role: :administrator) } + let(:agent1) { create(:user, account: account, role: :agent) } + let(:agent2) { create(:user, account: account, role: :agent) } + let(:conversation1) { create(:conversation, account: account, assignee: agent1) } + let(:conversation2) { create(:conversation, account: account, assignee: agent2) } + let(:conversation3) { create(:conversation, account: account, assignee: agent2) } + let(:sla_policy1) { create(:sla_policy, account: account) } + let(:sla_policy2) { create(:sla_policy, account: account) } + + before do + AppliedSla.destroy_all + end + + describe 'GET /api/v1/accounts/{account.id}/applied_slas/metrics' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/applied_slas/metrics" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'returns the sla metrics' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, sla_status: 'missed') + + get "/api/v1/accounts/#{account.id}/applied_slas/metrics", + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body).to include('total_applied_slas' => 1) + expect(body).to include('number_of_sla_breaches' => 1) + expect(body).to include('hit_rate' => '0.0%') + end + + it 'filters sla metrics based on a date range' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago) + + get "/api/v1/accounts/#{account.id}/applied_slas/metrics", + params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s }, + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body).to include('total_applied_slas' => 1) + expect(body).to include('number_of_sla_breaches' => 0) + expect(body).to include('hit_rate' => '100%') + end + + it 'filters sla metrics based on a date range and agent ids' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed') + + get "/api/v1/accounts/#{account.id}/applied_slas/metrics", + params: { agent_ids: [agent2.id] }, + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body).to include('total_applied_slas' => 3) + expect(body).to include('number_of_sla_breaches' => 1) + expect(body).to include('hit_rate' => '66.67%') + end + + it 'filters sla metrics based on sla policy ids' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, sla_status: 'missed') + create(:applied_sla, sla_policy: sla_policy2, conversation: conversation2, sla_status: 'missed') + + get "/api/v1/accounts/#{account.id}/applied_slas/metrics", + params: { sla_policy_id: sla_policy1.id }, + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body).to include('total_applied_slas' => 2) + expect(body).to include('number_of_sla_breaches' => 1) + expect(body).to include('hit_rate' => '50.0%') + end + + it 'filters sla metrics based on labels' do + conversation2.update_labels('label1') + conversation3.update_labels('label1') + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed') + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago) + + get "/api/v1/accounts/#{account.id}/applied_slas/metrics", + params: { label_list: ['label1'] }, + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body).to include('total_applied_slas' => 2) + expect(body).to include('number_of_sla_breaches' => 1) + expect(body).to include('hit_rate' => '50.0%') + end + end + end + + describe 'GET /api/v1/accounts/{account.id}/applied_slas' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/applied_slas" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'returns the applied slas' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2) + get "/api/v1/accounts/#{account.id}/applied_slas", + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body.size).to eq(2) + expect(body.first).to include('id') + expect(body.first).to include('sla_policy_id' => sla_policy1.id) + expect(body.first).to include('conversation_id' => conversation1.id) + end + + it 'filters applied slas based on a date range' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago) + + get "/api/v1/accounts/#{account.id}/applied_slas", + params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s }, + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body.size).to eq(1) + end + + it 'filters applied slas based on a date range and agent ids' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago) + + get "/api/v1/accounts/#{account.id}/applied_slas", + params: { agent_ids: [agent2.id] }, + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body.size).to eq(3) + end + + it 'filters applied slas based on sla policy ids' do + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2) + create(:applied_sla, sla_policy: sla_policy2, conversation: conversation2) + + get "/api/v1/accounts/#{account.id}/applied_slas", + params: { sla_policy_id: sla_policy1.id }, + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body.size).to eq(2) + end + + it 'filters applied slas based on labels' do + conversation2.update_labels('label1') + conversation3.update_labels('label1') + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago) + create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago) + + get "/api/v1/accounts/#{account.id}/applied_slas", + params: { label_list: ['label1'] }, + headers: administrator.create_new_auth_token + expect(response).to have_http_status(:success) + body = JSON.parse(response.body) + + expect(body.size).to eq(2) + end + end + end +end