Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
431c36e102 |
@@ -50,6 +50,9 @@ Rails.application.routes.draw do
|
||||
resource :bulk_actions, only: [:create]
|
||||
resources :agents, only: [:index, :create, :update, :destroy] do
|
||||
post :bulk_create, on: :collection
|
||||
member do
|
||||
get 'capacity', to: 'agents/capacity#show'
|
||||
end
|
||||
end
|
||||
namespace :captain do
|
||||
resources :assistants do
|
||||
@@ -97,6 +100,13 @@ Rails.application.routes.draw do
|
||||
end
|
||||
resources :sla_policies, only: [:index, :create, :show, :update, :destroy]
|
||||
resources :custom_roles, only: [:index, :create, :show, :update, :destroy]
|
||||
resources :agent_capacity_policies, only: [:index, :create, :show, :update, :destroy] do
|
||||
member do
|
||||
post 'users', to: 'agent_capacity_policies#assign_user'
|
||||
delete 'users/:user_id', to: 'agent_capacity_policies#unassign_user'
|
||||
put 'inbox_limits/:inbox_id', to: 'agent_capacity_policies#update_inbox_limit'
|
||||
end
|
||||
end
|
||||
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
|
||||
resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
|
||||
namespace :channels do
|
||||
|
||||
@@ -320,6 +320,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "status", default: 0, null: false
|
||||
t.jsonb "metadata", default: {}
|
||||
t.index ["account_id"], name: "index_captain_documents_on_account_id"
|
||||
t.index ["assistant_id", "external_link"], name: "index_captain_documents_on_assistant_id_and_external_link", unique: true
|
||||
t.index ["assistant_id"], name: "index_captain_documents_on_assistant_id"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
before_action :fetch_policy, only: [:show, :update, :destroy, :assign_user, :unassign_user, :update_inbox_limit]
|
||||
before_action :check_enterprise_authorization
|
||||
|
||||
def index
|
||||
@agent_capacity_policies = Current.account.agent_capacity_policies
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@agent_capacity_policy = Current.account.agent_capacity_policies.create!(permitted_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@agent_capacity_policy.update!(permitted_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent_capacity_policy.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def assign_user
|
||||
user = Current.account.users.find(params[:user_id])
|
||||
account_user = Current.account.account_users.find_by!(user: user)
|
||||
account_user.update!(agent_capacity_policy: @agent_capacity_policy)
|
||||
render json: { message: 'User assigned successfully' }
|
||||
end
|
||||
|
||||
def unassign_user
|
||||
user = Current.account.users.find(params[:user_id])
|
||||
account_user = Current.account.account_users.find_by!(user: user)
|
||||
account_user.update!(agent_capacity_policy: nil)
|
||||
render json: { message: 'User unassigned successfully' }
|
||||
end
|
||||
|
||||
def update_inbox_limit
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
inbox_limit = @agent_capacity_policy.inbox_capacity_limits.find_or_initialize_by(inbox: inbox)
|
||||
inbox_limit.update!(conversation_limit: params[:conversation_limit])
|
||||
render json: inbox_limit
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.require(:agent_capacity_policy).permit(:name, :description, exclusion_rules: {})
|
||||
end
|
||||
|
||||
def fetch_policy
|
||||
@agent_capacity_policy = Current.account.agent_capacity_policies.find(params[:id])
|
||||
end
|
||||
|
||||
def check_enterprise_authorization
|
||||
authorize(Enterprise::AgentCapacityPolicy)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
class Api::V1::Accounts::Agents::CapacityController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
before_action :fetch_agent
|
||||
|
||||
def show
|
||||
account_user = Current.account.account_users.find_by!(user: @agent)
|
||||
capacity_service = Enterprise::AssignmentV2::CapacityService.new(account_user)
|
||||
inbox = fetch_inbox
|
||||
|
||||
render json: build_capacity_response(account_user, capacity_service, inbox)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_agent
|
||||
@agent = Current.account.users.find(params[:id])
|
||||
end
|
||||
|
||||
def fetch_inbox
|
||||
return if params[:inbox_id].blank?
|
||||
|
||||
Current.account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
|
||||
def build_capacity_response(account_user, capacity_service, inbox)
|
||||
response = {
|
||||
has_capacity: capacity_service.agent_has_capacity?(inbox),
|
||||
overall_capacity: capacity_service.agent_overall_capacity,
|
||||
inbox_capacity: inbox ? capacity_service.agent_capacity_for_inbox(inbox) : nil,
|
||||
current_conversations_count: account_user.user.assigned_conversations.open.count
|
||||
}
|
||||
|
||||
add_inbox_conversations_count(response, account_user, inbox) if inbox
|
||||
response
|
||||
end
|
||||
|
||||
def add_inbox_conversations_count(response, account_user, inbox)
|
||||
response[:inbox_conversations_count] = account_user.user.assigned_conversations.open.where(inbox: inbox).count
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Enterprise::AgentCapacityPolicy < ApplicationRecord
|
||||
self.table_name = 'agent_capacity_policies'
|
||||
|
||||
belongs_to :account, class_name: '::Account'
|
||||
has_many :inbox_capacity_limits, class_name: 'Enterprise::InboxCapacityLimit', dependent: :destroy
|
||||
has_many :inboxes, through: :inbox_capacity_limits, class_name: '::Inbox'
|
||||
has_many :account_users, class_name: '::AccountUser', dependent: :nullify
|
||||
|
||||
validates :name, presence: true, length: { maximum: 255 }
|
||||
|
||||
def applicable_for_time?(time = Time.current)
|
||||
return true if exclusion_rules.blank?
|
||||
|
||||
!excluded_for_time?(time)
|
||||
end
|
||||
|
||||
def capacity_for_inbox(inbox)
|
||||
inbox_capacity_limits.find_by(inbox: inbox)&.conversation_limit
|
||||
end
|
||||
|
||||
def overall_capacity
|
||||
exclusion_rules['overall_capacity'] || Float::INFINITY
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def excluded_for_time?(time)
|
||||
excluded_by_hours?(time) || excluded_by_days?(time)
|
||||
end
|
||||
|
||||
def excluded_by_hours?(time)
|
||||
return false if exclusion_rules['hours'].blank?
|
||||
|
||||
current_hour = time.hour
|
||||
exclusion_rules['hours'].include?(current_hour)
|
||||
end
|
||||
|
||||
def excluded_by_days?(time)
|
||||
return false if exclusion_rules['days'].blank?
|
||||
|
||||
current_day = time.strftime('%A').downcase
|
||||
exclusion_rules['days'].include?(current_day)
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@ module Enterprise::Concerns::Account
|
||||
has_many :sla_policies, dependent: :destroy_async
|
||||
has_many :applied_slas, dependent: :destroy_async
|
||||
has_many :custom_roles, dependent: :destroy_async
|
||||
has_many :agent_capacity_policies, dependent: :destroy_async, class_name: 'Enterprise::AgentCapacityPolicy'
|
||||
|
||||
has_many :captain_assistants, dependent: :destroy_async, class_name: 'Captain::Assistant'
|
||||
has_many :captain_assistant_responses, dependent: :destroy_async, class_name: 'Captain::AssistantResponse'
|
||||
|
||||
@@ -3,5 +3,6 @@ module Enterprise::Concerns::AccountUser
|
||||
|
||||
included do
|
||||
belongs_to :custom_role, optional: true
|
||||
belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy', optional: true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Enterprise::Concerns::InboxAgentAvailability
|
||||
def apply_agent_filters(scope, options)
|
||||
scope = super(scope, options)
|
||||
|
||||
# Apply capacity filtering if requested
|
||||
scope = filter_by_capacity(scope) if options[:check_capacity]
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_by_capacity(inbox_members_scope)
|
||||
return inbox_members_scope unless assignment_policy&.enabled?
|
||||
|
||||
inbox_members_scope.select do |inbox_member|
|
||||
account_user = AccountUser.find_by(account: account, user: inbox_member.user)
|
||||
next true if account_user&.agent_capacity_policy.blank?
|
||||
|
||||
capacity_service = Enterprise::AssignmentV2::CapacityService.new(account_user)
|
||||
capacity_service.agent_has_capacity?(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,9 +2,17 @@ module Enterprise::Inbox
|
||||
def member_ids_with_assignment_capacity
|
||||
return super unless enable_auto_assignment?
|
||||
|
||||
max_assignment_limit = auto_assignment_config['max_assignment_limit']
|
||||
overloaded_agent_ids = max_assignment_limit.present? ? get_agent_ids_over_assignment_limit(max_assignment_limit) : []
|
||||
super - overloaded_agent_ids
|
||||
member_ids = apply_max_assignment_limit(super)
|
||||
apply_capacity_policy_filter(member_ids)
|
||||
end
|
||||
|
||||
def available_agents(options = {})
|
||||
agents = super(options)
|
||||
|
||||
# Apply capacity filtering if requested and assignment policy is enabled
|
||||
agents = filter_agents_by_capacity(agents) if options[:check_capacity] && assignment_policy&.enabled?
|
||||
|
||||
agents
|
||||
end
|
||||
|
||||
def active_bot?
|
||||
@@ -17,6 +25,36 @@ module Enterprise::Inbox
|
||||
|
||||
private
|
||||
|
||||
def filter_agents_by_capacity(inbox_members_scope)
|
||||
inbox_members_scope.select do |inbox_member|
|
||||
account_user = AccountUser.find_by(account: account, user: inbox_member.user)
|
||||
next true if account_user&.agent_capacity_policy.blank?
|
||||
|
||||
capacity_service = Enterprise::AssignmentV2::CapacityService.new(account_user)
|
||||
capacity_service.agent_has_capacity?(self)
|
||||
end
|
||||
end
|
||||
|
||||
def apply_max_assignment_limit(member_ids)
|
||||
max_assignment_limit = auto_assignment_config['max_assignment_limit']
|
||||
return member_ids if max_assignment_limit.blank?
|
||||
|
||||
overloaded_agent_ids = get_agent_ids_over_assignment_limit(max_assignment_limit)
|
||||
member_ids - overloaded_agent_ids
|
||||
end
|
||||
|
||||
def apply_capacity_policy_filter(member_ids)
|
||||
return member_ids unless assignment_policy&.enabled?
|
||||
|
||||
account_users = AccountUser.where(account_id: account_id, user_id: member_ids)
|
||||
account_users.select do |account_user|
|
||||
next true if account_user.agent_capacity_policy.blank?
|
||||
|
||||
capacity_service = Enterprise::AssignmentV2::CapacityService.new(account_user)
|
||||
capacity_service.agent_has_capacity?(self)
|
||||
end.map(&:user_id)
|
||||
end
|
||||
|
||||
def more_responses?
|
||||
account.usage_limits[:captain][:responses][:current_available].positive?
|
||||
end
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Enterprise::InboxCapacityLimit < ApplicationRecord
|
||||
self.table_name = 'inbox_capacity_limits'
|
||||
|
||||
belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy'
|
||||
belongs_to :inbox, class_name: '::Inbox'
|
||||
|
||||
validates :conversation_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :inbox_id, uniqueness: { scope: :agent_capacity_policy_id }
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
class Enterprise::AgentCapacityPolicyPolicy < ApplicationPolicy
|
||||
def index?
|
||||
@account_user.administrator? || @account_user.agent?
|
||||
end
|
||||
|
||||
def update?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def show?
|
||||
@account_user.administrator? || @account_user.agent?
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def assign_user?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def unassign_user?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def update_inbox_limit?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
@@ -7,4 +7,19 @@ module Enterprise::AssignmentV2::AssignmentService
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Override find_agent_for_conversation to include capacity checks
|
||||
def find_agent_for_conversation(_conversation)
|
||||
available_agents = inbox.available_agents(
|
||||
check_rate_limits: true,
|
||||
check_capacity: true
|
||||
)
|
||||
|
||||
if available_agents.empty?
|
||||
log_no_agents_available
|
||||
return nil
|
||||
end
|
||||
|
||||
selector_service.select_agent(available_agents)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Enterprise::AssignmentV2::CapacityService
|
||||
def initialize(account_user)
|
||||
@account_user = account_user
|
||||
@account = account_user.account
|
||||
end
|
||||
|
||||
def agent_has_capacity?(inbox = nil)
|
||||
return true unless capacity_policy_applicable?
|
||||
|
||||
if inbox
|
||||
check_inbox_capacity(inbox)
|
||||
else
|
||||
check_overall_capacity
|
||||
end
|
||||
end
|
||||
|
||||
def agent_capacity_for_inbox(inbox)
|
||||
return Float::INFINITY unless capacity_policy_applicable?
|
||||
|
||||
policy = @account_user.agent_capacity_policy
|
||||
inbox_limit = policy.capacity_for_inbox(inbox)
|
||||
return Float::INFINITY unless inbox_limit
|
||||
|
||||
current_count = current_conversations_count(inbox)
|
||||
[inbox_limit - current_count, 0].max
|
||||
end
|
||||
|
||||
def agent_overall_capacity
|
||||
return Float::INFINITY unless capacity_policy_applicable?
|
||||
|
||||
policy = @account_user.agent_capacity_policy
|
||||
overall_limit = policy.overall_capacity
|
||||
return Float::INFINITY if overall_limit == Float::INFINITY
|
||||
|
||||
current_count = current_conversations_count
|
||||
[overall_limit - current_count, 0].max
|
||||
end
|
||||
|
||||
def current_conversations_count(inbox = nil)
|
||||
scope = @account_user.user.conversations
|
||||
.joins(:account)
|
||||
.where(account: @account, status: :open)
|
||||
scope = scope.where(inbox: inbox) if inbox
|
||||
scope.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def capacity_policy_applicable?
|
||||
return false if @account_user.agent_capacity_policy.blank?
|
||||
|
||||
@account_user.agent_capacity_policy.applicable_for_time?
|
||||
end
|
||||
|
||||
def check_inbox_capacity(inbox)
|
||||
policy = @account_user.agent_capacity_policy
|
||||
inbox_limit = policy.capacity_for_inbox(inbox)
|
||||
|
||||
return check_overall_capacity unless inbox_limit
|
||||
|
||||
current_count = current_conversations_count(inbox)
|
||||
current_count < inbox_limit && check_overall_capacity
|
||||
end
|
||||
|
||||
def check_overall_capacity
|
||||
policy = @account_user.agent_capacity_policy
|
||||
overall_limit = policy.overall_capacity
|
||||
|
||||
return true if overall_limit == Float::INFINITY
|
||||
|
||||
current_count = current_conversations_count
|
||||
current_count < overall_limit
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
json.id @agent_capacity_policy.id
|
||||
json.name @agent_capacity_policy.name
|
||||
json.description @agent_capacity_policy.description
|
||||
json.exclusion_rules @agent_capacity_policy.exclusion_rules
|
||||
json.created_at @agent_capacity_policy.created_at
|
||||
json.updated_at @agent_capacity_policy.updated_at
|
||||
json.account_id @agent_capacity_policy.account_id
|
||||
json.inbox_capacity_limits @agent_capacity_policy.inbox_capacity_limits do |limit|
|
||||
json.id limit.id
|
||||
json.inbox_id limit.inbox_id
|
||||
json.conversation_limit limit.conversation_limit
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
json.array! @agent_capacity_policies do |policy|
|
||||
json.id policy.id
|
||||
json.name policy.name
|
||||
json.description policy.description
|
||||
json.exclusion_rules policy.exclusion_rules
|
||||
json.created_at policy.created_at
|
||||
json.updated_at policy.updated_at
|
||||
json.account_id policy.account_id
|
||||
json.inbox_capacity_limits policy.inbox_capacity_limits do |limit|
|
||||
json.id limit.id
|
||||
json.inbox_id limit.inbox_id
|
||||
json.conversation_limit limit.conversation_limit
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
json.id @agent_capacity_policy.id
|
||||
json.name @agent_capacity_policy.name
|
||||
json.description @agent_capacity_policy.description
|
||||
json.exclusion_rules @agent_capacity_policy.exclusion_rules
|
||||
json.created_at @agent_capacity_policy.created_at
|
||||
json.updated_at @agent_capacity_policy.updated_at
|
||||
json.account_id @agent_capacity_policy.account_id
|
||||
json.inbox_capacity_limits @agent_capacity_policy.inbox_capacity_limits do |limit|
|
||||
json.id limit.id
|
||||
json.inbox_id limit.inbox_id
|
||||
json.conversation_limit limit.conversation_limit
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
json.id @agent_capacity_policy.id
|
||||
json.name @agent_capacity_policy.name
|
||||
json.description @agent_capacity_policy.description
|
||||
json.exclusion_rules @agent_capacity_policy.exclusion_rules
|
||||
json.created_at @agent_capacity_policy.created_at
|
||||
json.updated_at @agent_capacity_policy.updated_at
|
||||
json.account_id @agent_capacity_policy.account_id
|
||||
json.inbox_capacity_limits @agent_capacity_policy.inbox_capacity_limits do |limit|
|
||||
json.id limit.id
|
||||
json.inbox_id limit.inbox_id
|
||||
json.conversation_limit limit.conversation_limit
|
||||
end
|
||||
@@ -0,0 +1,128 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::AgentCapacityPolicy do
|
||||
let(:account) { create(:account) }
|
||||
let(:policy) { described_class.create!(account: account, name: 'Test Policy') }
|
||||
|
||||
describe 'associations' do
|
||||
it 'belongs to account' do
|
||||
expect(policy.account).to eq(account)
|
||||
end
|
||||
|
||||
it 'has many inbox capacity limits' do
|
||||
expect(policy).to respond_to(:inbox_capacity_limits)
|
||||
end
|
||||
|
||||
it 'has many inboxes through inbox capacity limits' do
|
||||
expect(policy).to respond_to(:inboxes)
|
||||
end
|
||||
|
||||
it 'has many account users' do
|
||||
expect(policy).to respond_to(:account_users)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it 'validates presence of name' do
|
||||
invalid_policy = described_class.new(account: account)
|
||||
expect(invalid_policy).not_to be_valid
|
||||
expect(invalid_policy.errors[:name]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'validates length of name' do
|
||||
invalid_policy = described_class.new(account: account, name: 'a' * 256)
|
||||
expect(invalid_policy).not_to be_valid
|
||||
expect(invalid_policy.errors[:name]).to include('is too long (maximum is 255 characters)')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#applicable_for_time?' do
|
||||
context 'when no exclusion rules' do
|
||||
it 'returns true' do
|
||||
expect(policy.applicable_for_time?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with hour exclusions' do
|
||||
let(:policy) do
|
||||
described_class.create!(
|
||||
account: account,
|
||||
name: 'Hour Exclusion Policy',
|
||||
exclusion_rules: { 'hours' => [10, 11, 12] }
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns false during excluded hours' do
|
||||
time = Time.zone.parse('10:30')
|
||||
expect(policy.applicable_for_time?(time)).to be false
|
||||
end
|
||||
|
||||
it 'returns true outside excluded hours' do
|
||||
time = Time.zone.parse('13:30')
|
||||
expect(policy.applicable_for_time?(time)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with day exclusions' do
|
||||
let(:policy) do
|
||||
described_class.create!(
|
||||
account: account,
|
||||
name: 'Day Exclusion Policy',
|
||||
exclusion_rules: { 'days' => %w[saturday sunday] }
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns false on excluded days' do
|
||||
time = Time.zone.parse('2024-01-06 10:00') # Saturday
|
||||
expect(policy.applicable_for_time?(time)).to be false
|
||||
end
|
||||
|
||||
it 'returns true on non-excluded days' do
|
||||
time = Time.zone.parse('2024-01-08 10:00') # Monday
|
||||
expect(policy.applicable_for_time?(time)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#capacity_for_inbox' do
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
|
||||
it 'returns the conversation limit for the inbox' do
|
||||
Enterprise::InboxCapacityLimit.create!(
|
||||
agent_capacity_policy: policy,
|
||||
inbox: inbox,
|
||||
conversation_limit: 10
|
||||
)
|
||||
expect(policy.capacity_for_inbox(inbox)).to eq(10)
|
||||
end
|
||||
|
||||
it 'returns nil for inbox without limit' do
|
||||
other_inbox = create(:inbox, account: account)
|
||||
expect(policy.capacity_for_inbox(other_inbox)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#overall_capacity' do
|
||||
context 'when overall_capacity is set' do
|
||||
let(:policy) do
|
||||
described_class.create!(
|
||||
account: account,
|
||||
name: 'Overall Capacity Policy',
|
||||
exclusion_rules: { 'overall_capacity' => 25 }
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the overall capacity' do
|
||||
expect(policy.overall_capacity).to eq(25)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when overall_capacity is not set' do
|
||||
it 'returns infinity' do
|
||||
expect(policy.overall_capacity).to eq(Float::INFINITY)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,141 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Assignment with Capacity' do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
|
||||
|
||||
let(:agent1) { create(:user, accounts: [account]) }
|
||||
let(:agent2) { create(:user, accounts: [account]) }
|
||||
let(:account_user1) { AccountUser.find_by(account: account, user: agent1) }
|
||||
let(:account_user2) { AccountUser.find_by(account: account, user: agent2) }
|
||||
|
||||
let(:capacity_policy) do
|
||||
Enterprise::AgentCapacityPolicy.create!(
|
||||
account: account,
|
||||
name: 'Test Capacity Policy',
|
||||
exclusion_rules: { 'overall_capacity' => 5 }
|
||||
)
|
||||
end
|
||||
|
||||
let!(:inbox_capacity_limit) do
|
||||
Enterprise::InboxCapacityLimit.create!(
|
||||
agent_capacity_policy: capacity_policy,
|
||||
inbox: inbox,
|
||||
conversation_limit: 3
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
# Create and setup assignment policy for the inbox
|
||||
@assignment_policy = create(:assignment_policy, account: account, enabled: true)
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: @assignment_policy)
|
||||
|
||||
# Add agents to inbox
|
||||
create(:inbox_member, inbox: inbox, user: agent1)
|
||||
create(:inbox_member, inbox: inbox, user: agent2)
|
||||
|
||||
# Set agents online using presence and status
|
||||
OnlineStatusTracker.update_presence(account.id, 'User', agent1.id)
|
||||
OnlineStatusTracker.update_presence(account.id, 'User', agent2.id)
|
||||
OnlineStatusTracker.set_status(account.id, agent1.id, 'online')
|
||||
OnlineStatusTracker.set_status(account.id, agent2.id, 'online')
|
||||
|
||||
# Also set account_user availability
|
||||
account_user1.update!(availability: 'online')
|
||||
account_user2.update!(availability: 'online')
|
||||
|
||||
# Assign capacity policy to agent1 only
|
||||
account_user1.update!(agent_capacity_policy: capacity_policy)
|
||||
end
|
||||
|
||||
describe 'capacity-based agent filtering' do
|
||||
context 'when agent has capacity' do
|
||||
it 'includes agent in available agents' do
|
||||
available_agents = inbox.available_agents(check_capacity: true)
|
||||
expect(available_agents.map(&:user)).to include(agent1, agent2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when agent reaches inbox capacity limit' do
|
||||
before do
|
||||
# Create 3 conversations for agent1 (at inbox limit)
|
||||
create_list(:conversation, 3, account: account, inbox: inbox, assignee: agent1, status: :open)
|
||||
end
|
||||
|
||||
it 'excludes agent from available agents for that inbox' do
|
||||
available_agents = inbox.available_agents(check_capacity: true)
|
||||
expect(available_agents.map(&:user)).not_to include(agent1)
|
||||
expect(available_agents.map(&:user)).to include(agent2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when agent reaches overall capacity limit' do
|
||||
before do
|
||||
# Create 5 conversations for agent1 (at overall limit)
|
||||
create_list(:conversation, 5, account: account, assignee: agent1, status: :open)
|
||||
end
|
||||
|
||||
it 'excludes agent from all inbox assignments' do
|
||||
available_agents = inbox.available_agents(check_capacity: true)
|
||||
expect(available_agents.map(&:user)).not_to include(agent1)
|
||||
expect(available_agents.map(&:user)).to include(agent2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when capacity policy is not applicable (time exclusion)' do
|
||||
before do
|
||||
capacity_policy.update!(exclusion_rules: {
|
||||
'overall_capacity' => 5,
|
||||
'hours' => [Time.current.hour]
|
||||
})
|
||||
# Create 5 conversations for agent1 (would be at limit if policy was active)
|
||||
create_list(:conversation, 5, account: account, assignee: agent1, status: :open)
|
||||
end
|
||||
|
||||
it 'includes agent in available agents' do
|
||||
available_agents = inbox.available_agents(check_capacity: true)
|
||||
expect(available_agents.map(&:user)).to include(agent1, agent2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'capacity-aware assignment' do
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, status: :open, assignee: nil) }
|
||||
|
||||
context 'when agents have capacity' do
|
||||
it 'both agents are available for assignment' do
|
||||
available = inbox.available_agents(check_capacity: true)
|
||||
expect(available.map(&:user)).to include(agent1, agent2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when one agent is at capacity' do
|
||||
before do
|
||||
# Agent1 at inbox capacity
|
||||
create_list(:conversation, 3, account: account, inbox: inbox, assignee: agent1, status: :open)
|
||||
end
|
||||
|
||||
it 'only agent with capacity is available' do
|
||||
available = inbox.available_agents(check_capacity: true)
|
||||
expect(available.map(&:user)).not_to include(agent1)
|
||||
expect(available.map(&:user)).to include(agent2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when all agents are at capacity' do
|
||||
before do
|
||||
# Both agents at capacity
|
||||
account_user2.update!(agent_capacity_policy: capacity_policy)
|
||||
create_list(:conversation, 3, account: account, inbox: inbox, assignee: agent1, status: :open)
|
||||
create_list(:conversation, 3, account: account, inbox: inbox, assignee: agent2, status: :open)
|
||||
end
|
||||
|
||||
it 'no agents are available' do
|
||||
available = inbox.available_agents(check_capacity: true)
|
||||
expect(available).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,168 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::AssignmentV2::CapacityService do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, accounts: [account]) }
|
||||
let(:account_user) { AccountUser.find_by(account: account, user: user) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:service) { described_class.new(account_user) }
|
||||
|
||||
describe '#agent_has_capacity?' do
|
||||
context 'without capacity policy' do
|
||||
it 'returns true' do
|
||||
expect(service.agent_has_capacity?).to be true
|
||||
expect(service.agent_has_capacity?(inbox)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with capacity policy' do
|
||||
let(:policy) { Enterprise::AgentCapacityPolicy.create!(account: account, name: 'Test Policy') }
|
||||
|
||||
before do
|
||||
account_user.update!(agent_capacity_policy: policy)
|
||||
end
|
||||
|
||||
context 'when policy is not applicable' do
|
||||
before do
|
||||
allow(policy).to receive(:applicable_for_time?).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.agent_has_capacity?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when checking inbox capacity' do
|
||||
let!(:inbox_limit) do
|
||||
Enterprise::InboxCapacityLimit.create!(
|
||||
agent_capacity_policy: policy,
|
||||
inbox: inbox,
|
||||
conversation_limit: 5
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns true when under limit' do
|
||||
create_list(:conversation, 3, account: account, inbox: inbox, assignee: user, status: :open)
|
||||
expect(service.agent_has_capacity?(inbox)).to be true
|
||||
end
|
||||
|
||||
it 'returns false when at limit' do
|
||||
create_list(:conversation, 5, account: account, inbox: inbox, assignee: user, status: :open)
|
||||
expect(service.agent_has_capacity?(inbox)).to be false
|
||||
end
|
||||
|
||||
it 'checks overall capacity too' do
|
||||
policy.update!(exclusion_rules: { 'overall_capacity' => 10 })
|
||||
create_list(:conversation, 9, account: account, assignee: user, status: :open)
|
||||
|
||||
# Under inbox limit but close to overall limit
|
||||
expect(service.agent_has_capacity?(inbox)).to be true
|
||||
|
||||
# Add one more to hit overall limit
|
||||
create(:conversation, account: account, assignee: user, status: :open)
|
||||
expect(service.agent_has_capacity?(inbox)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when checking overall capacity' do
|
||||
before do
|
||||
policy.update!(exclusion_rules: { 'overall_capacity' => 10 })
|
||||
end
|
||||
|
||||
it 'returns true when under limit' do
|
||||
create_list(:conversation, 8, account: account, assignee: user, status: :open)
|
||||
expect(service.agent_has_capacity?).to be true
|
||||
end
|
||||
|
||||
it 'returns false when at limit' do
|
||||
create_list(:conversation, 10, account: account, assignee: user, status: :open)
|
||||
expect(service.agent_has_capacity?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#agent_capacity_for_inbox' do
|
||||
context 'without capacity policy' do
|
||||
it 'returns infinity' do
|
||||
expect(service.agent_capacity_for_inbox(inbox)).to eq(Float::INFINITY)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with capacity policy and inbox limit' do
|
||||
let(:policy) { Enterprise::AgentCapacityPolicy.create!(account: account, name: 'Test Policy') }
|
||||
let!(:inbox_limit) do
|
||||
Enterprise::InboxCapacityLimit.create!(
|
||||
agent_capacity_policy: policy,
|
||||
inbox: inbox,
|
||||
conversation_limit: 5
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
account_user.update!(agent_capacity_policy: policy)
|
||||
end
|
||||
|
||||
it 'returns remaining capacity' do
|
||||
create_list(:conversation, 2, account: account, inbox: inbox, assignee: user, status: :open)
|
||||
expect(service.agent_capacity_for_inbox(inbox)).to eq(3)
|
||||
end
|
||||
|
||||
it 'returns 0 when at capacity' do
|
||||
create_list(:conversation, 5, account: account, inbox: inbox, assignee: user, status: :open)
|
||||
expect(service.agent_capacity_for_inbox(inbox)).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#agent_overall_capacity' do
|
||||
context 'without capacity policy' do
|
||||
it 'returns infinity' do
|
||||
expect(service.agent_overall_capacity).to eq(Float::INFINITY)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with capacity policy and overall limit' do
|
||||
let(:policy) do
|
||||
Enterprise::AgentCapacityPolicy.create!(
|
||||
account: account,
|
||||
name: 'Overall Policy',
|
||||
exclusion_rules: { 'overall_capacity' => 10 }
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
account_user.update!(agent_capacity_policy: policy)
|
||||
end
|
||||
|
||||
it 'returns remaining capacity' do
|
||||
create_list(:conversation, 6, account: account, assignee: user, status: :open)
|
||||
expect(service.agent_overall_capacity).to eq(4)
|
||||
end
|
||||
|
||||
it 'returns 0 when at capacity' do
|
||||
create_list(:conversation, 10, account: account, assignee: user, status: :open)
|
||||
expect(service.agent_overall_capacity).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#current_conversations_count' do
|
||||
it 'counts open conversations assigned to user' do
|
||||
create_list(:conversation, 3, account: account, assignee: user, status: :open)
|
||||
create(:conversation, account: account, assignee: user, status: :resolved)
|
||||
create(:conversation, account: account, status: :open)
|
||||
|
||||
expect(service.current_conversations_count).to eq(3)
|
||||
end
|
||||
|
||||
it 'counts inbox-specific conversations when inbox provided' do
|
||||
create_list(:conversation, 2, account: account, inbox: inbox, assignee: user, status: :open)
|
||||
create(:conversation, account: account, assignee: user, status: :open)
|
||||
|
||||
expect(service.current_conversations_count(inbox)).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :agent_capacity_policy, class: 'Enterprise::AgentCapacityPolicy' do
|
||||
account
|
||||
name { Faker::Name.name }
|
||||
description { Faker::Lorem.sentence }
|
||||
exclusion_rules { {} }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :inbox_capacity_limit, class: 'Enterprise::InboxCapacityLimit' do
|
||||
association :agent_capacity_policy, factory: :agent_capacity_policy
|
||||
inbox
|
||||
conversation_limit { 10 }
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user