add exclusion_rules support

This commit is contained in:
Tanmay Sharma
2025-08-28 20:39:30 +07:00
parent 096bffa893
commit 873094175b
4 changed files with 220 additions and 0 deletions
@@ -6,5 +6,6 @@ module Enterprise::Concerns::Inbox
has_one :captain_assistant,
through: :captain_inbox,
class_name: 'Captain::Assistant'
has_many :inbox_capacity_limits, dependent: :destroy
end
end
@@ -57,4 +57,40 @@ module Enterprise::AutoAssignment::AssignmentService
def account
inbox.account
end
# Override to apply exclusion rules
def unassigned_conversations(limit)
scope = inbox.conversations.unassigned.open
# Apply exclusion rules from capacity policy or assignment policy
scope = apply_exclusion_rules(scope)
# Apply conversation priority from config
scope = apply_conversation_priority(scope)
scope.limit(limit)
end
def apply_exclusion_rules(scope)
capacity_policy = inbox.inbox_capacity_limits.first&.agent_capacity_policy
return scope unless capacity_policy
exclusion_rules = capacity_policy.exclusion_rules || {}
scope = apply_label_exclusions(scope, exclusion_rules['excluded_labels'])
apply_age_exclusions(scope, exclusion_rules['exclude_older_than_hours'])
end
def apply_label_exclusions(scope, excluded_labels)
return scope if excluded_labels.blank?
scope.tagged_with(excluded_labels, exclude: true, on: :labels)
end
def apply_age_exclusions(scope, hours_threshold)
return scope if hours_threshold.blank?
hours = hours_threshold.to_i
return scope unless hours.positive?
scope.where('conversations.created_at >= ?', hours.hours.ago)
end
end
@@ -0,0 +1,181 @@
require 'rails_helper'
RSpec.describe Enterprise::AutoAssignment::AssignmentService, type: :service do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:agent1) { create(:user, account: account, name: 'Agent 1') }
let(:agent2) { create(:user, account: account, name: 'Agent 2') }
let(:assignment_service) { AutoAssignment::AssignmentService.new(inbox: inbox) }
before do
# Create inbox members
create(:inbox_member, inbox: inbox, user: agent1)
create(:inbox_member, inbox: inbox, user: agent2)
allow(account).to receive(:feature_enabled?).and_return(false)
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
# Set agents as online
OnlineStatusTracker.update_presence(account.id, 'User', agent1.id)
OnlineStatusTracker.set_status(account.id, agent1.id, 'online')
OnlineStatusTracker.update_presence(account.id, 'User', agent2.id)
OnlineStatusTracker.set_status(account.id, agent2.id, 'online')
end
describe 'exclusion rules' do
let(:capacity_policy) { create(:agent_capacity_policy, account: account) }
let(:label1) { create(:label, account: account, title: 'high-priority') }
let(:label2) { create(:label, account: account, title: 'vip') }
before do
create(:inbox_capacity_limit, inbox: inbox, agent_capacity_policy: capacity_policy, conversation_limit: 10)
inbox.enable_auto_assignment = true
inbox.save!
end
context 'when excluding conversations by label' do
let!(:conversation_with_label) { create(:conversation, inbox: inbox, assignee: nil) }
let!(:conversation_without_label) { create(:conversation, inbox: inbox, assignee: nil) }
before do
conversation_with_label.update_labels([label1.title])
capacity_policy.update!(exclusion_rules: {
'excluded_labels' => [label1.title]
})
end
it 'excludes conversations with specified labels' do
# First check conversations are unassigned
expect(conversation_with_label.assignee).to be_nil
expect(conversation_without_label.assignee).to be_nil
# Run bulk assignment
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
# Only the conversation without label should be assigned
expect(assigned_count).to eq(1)
expect(conversation_with_label.reload.assignee).to be_nil
expect(conversation_without_label.reload.assignee).to be_present
end
it 'handles bulk assignment correctly' do
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
# Only 1 conversation should be assigned (the one without label)
expect(assigned_count).to eq(1)
expect(conversation_with_label.reload.assignee).to be_nil
expect(conversation_without_label.reload.assignee).to be_present
end
it 'excludes conversations with multiple labels' do
conversation_without_label.update_labels([label2.title])
capacity_policy.update!(exclusion_rules: {
'excluded_labels' => [label1.title, label2.title]
})
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
# Both conversations should be excluded
expect(assigned_count).to eq(0)
expect(conversation_with_label.reload.assignee).to be_nil
expect(conversation_without_label.reload.assignee).to be_nil
end
end
context 'when excluding conversations by age' do
let!(:old_conversation) { create(:conversation, inbox: inbox, assignee: nil, created_at: 25.hours.ago) }
let!(:recent_conversation) { create(:conversation, inbox: inbox, assignee: nil, created_at: 1.hour.ago) }
before do
capacity_policy.update!(exclusion_rules: {
'exclude_older_than_hours' => 24
})
end
it 'excludes conversations older than specified hours' do
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
# Only recent conversation should be assigned
expect(assigned_count).to eq(1)
expect(old_conversation.reload.assignee).to be_nil
expect(recent_conversation.reload.assignee).to be_present
end
it 'handles different time thresholds' do
capacity_policy.update!(exclusion_rules: {
'exclude_older_than_hours' => 2
})
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
# Only conversation created within 2 hours should be assigned
expect(assigned_count).to eq(1)
expect(recent_conversation.reload.assignee).to be_present
end
end
context 'when combining exclusion rules' do
it 'applies both exclusion rules' do
# Create conversations
old_conversation_with_label = create(:conversation, inbox: inbox, assignee: nil, created_at: 25.hours.ago)
old_conversation_without_label = create(:conversation, inbox: inbox, assignee: nil, created_at: 25.hours.ago)
recent_conversation_with_label = create(:conversation, inbox: inbox, assignee: nil, created_at: 1.hour.ago)
recent_conversation_without_label = create(:conversation, inbox: inbox, assignee: nil, created_at: 1.hour.ago)
# Add labels
old_conversation_with_label.update_labels([label1.title])
recent_conversation_with_label.update_labels([label1.title])
capacity_policy.update!(exclusion_rules: {
'excluded_labels' => [label1.title],
'exclude_older_than_hours' => 24
})
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
# Only recent conversation without label should be assigned
expect(assigned_count).to eq(1)
expect(old_conversation_with_label.reload.assignee).to be_nil
expect(old_conversation_without_label.reload.assignee).to be_nil
expect(recent_conversation_with_label.reload.assignee).to be_nil
expect(recent_conversation_without_label.reload.assignee).to be_present
end
end
context 'when exclusion rules are empty' do
let!(:conversation1) { create(:conversation, inbox: inbox, assignee: nil) }
let!(:conversation2) { create(:conversation, inbox: inbox, assignee: nil) }
before do
capacity_policy.update!(exclusion_rules: {})
end
it 'assigns all eligible conversations' do
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
expect(assigned_count).to eq(2)
expect(conversation1.reload.assignee).to be_present
expect(conversation2.reload.assignee).to be_present
end
end
context 'when no capacity policy exists' do
let!(:conversation1) { create(:conversation, inbox: inbox, assignee: nil) }
let!(:conversation2) { create(:conversation, inbox: inbox, assignee: nil) }
before do
InboxCapacityLimit.destroy_all
end
it 'assigns all eligible conversations without exclusions' do
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
expect(assigned_count).to eq(2)
expect(conversation1.reload.assignee).to be_present
expect(conversation2.reload.assignee).to be_present
end
end
end
end
@@ -1,3 +1,5 @@
require 'rails_helper'
RSpec.describe AutoAssignment::AgentAssignmentService do
let!(:account) { create(:account) }
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }