fix: resolve V2 capacity bypass in team assignment (#13904)

## Description

When Assignment V2 is enabled, the V2 capacity policies
(AgentCapacityPolicy / InboxCapacityLimit) are not respected during
team-based assignment paths. The system falls back to the legacy V1
max_assignment_limit, and since V1 is deprecated and typically
unconfigured in V2 setups, agents receive unlimited assignments
regardless of their V2 capacity.

Root cause: Inbox class directly defined
member_ids_with_assignment_capacity, which shadowed the
Enterprise::InboxAgentAvailability module override in Ruby's method
resolution order (MRO). This made the V2 capacity check unreachable
(dead code) for any code path using member_ids_with_assignment_capacity.

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

⏺ Before the fix
1. Enable assignment_v2 + advanced_assignment on account
2. Create AgentCapacityPolicy with InboxCapacityLimit = 1 for an inbox
3. Assign the policy to an agent (e.g., John)
4. Create 1 open conversation assigned to John (now at capacity)
5. Create a new unassigned conversation in the same inbox
6. Assign a team (containing John) to that conversation
7. Result: John gets assigned despite being at capacity
⏺ After the fix
Same steps 1–6.
7. Result: John is NOT assigned — conversation stays unassigned (no
agents with capacity available)


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Tanmay Deep Sharma
2026-03-27 15:38:17 +05:30
committed by GitHub
parent 2b296c06fb
commit 9efd554693
3 changed files with 99 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
module Enterprise::Inbox module Enterprise::Inbox
def member_ids_with_assignment_capacity def member_ids_with_assignment_capacity
return super unless enable_auto_assignment? return super unless enable_auto_assignment?
return filter_by_capacity(available_agents).map(&:user_id) if auto_assignment_v2_enabled?
max_assignment_limit = auto_assignment_config['max_assignment_limit'] 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) : [] overloaded_agent_ids = max_assignment_limit.present? ? get_agent_ids_over_assignment_limit(max_assignment_limit) : []

View File

@@ -21,7 +21,7 @@ module Enterprise::InboxAgentAvailability
end end
def capacity_filtering_enabled? def capacity_filtering_enabled?
account.feature_enabled?('assignment_v2') && account.feature_enabled?('advanced_assignment') &&
account.account_users.joins(:agent_capacity_policy).exists? account.account_users.joins(:agent_capacity_policy).exists?
end end

View File

@@ -37,6 +37,103 @@ RSpec.describe Inbox do
end end
end end
describe 'member_ids_with_assignment_capacity with V2 capacity' do
let(:account) { create(:account) }
let(:v2_inbox) { create(:inbox, account: account, enable_auto_assignment: true) }
let(:agent_capacity_policy) { create(:agent_capacity_policy, account: account) }
let!(:agent1) { create(:user, account: account, role: :agent, auto_offline: false) }
let!(:agent2) { create(:user, account: account, role: :agent, auto_offline: false) }
before do
create(:inbox_member, inbox: v2_inbox, user: agent1)
create(:inbox_member, inbox: v2_inbox, user: agent2)
allow(OnlineStatusTracker).to receive(:get_available_users).and_return(
agent1.id.to_s => 'online',
agent2.id.to_s => 'online'
)
end
context 'when assignment_v2 is enabled with capacity policies' do
before do
account.enable_features('assignment_v2', 'advanced_assignment')
account.save!
create(:inbox_capacity_limit, agent_capacity_policy: agent_capacity_policy, inbox: v2_inbox, conversation_limit: 1)
agent1.account_users.find_by(account: account).update!(agent_capacity_policy: agent_capacity_policy)
agent2.account_users.find_by(account: account).update!(agent_capacity_policy: agent_capacity_policy)
end
it 'filters out agents at capacity' do
create(:conversation, inbox: v2_inbox, account: account, assignee: agent1, status: :open)
result = v2_inbox.member_ids_with_assignment_capacity
expect(result).to include(agent2.id)
expect(result).not_to include(agent1.id)
end
it 'filters out all agents when all are at capacity' do
create(:conversation, inbox: v2_inbox, account: account, assignee: agent1, status: :open)
create(:conversation, inbox: v2_inbox, account: account, assignee: agent2, status: :open)
expect(v2_inbox.member_ids_with_assignment_capacity).to be_empty
end
it 'skips V1 max_assignment_limit when V2 is enabled' do
v2_inbox.update(auto_assignment_config: { max_assignment_limit: 100 })
create(:conversation, inbox: v2_inbox, account: account, assignee: agent1, status: :open)
result = v2_inbox.member_ids_with_assignment_capacity
expect(result).not_to include(agent1.id)
end
end
context 'when assignment_v2 is enabled without capacity policies' do
before do
account.enable_features('assignment_v2', 'advanced_assignment')
account.save!
end
it 'returns all online agents' do
result = v2_inbox.member_ids_with_assignment_capacity
expect(result).to contain_exactly(agent1.id, agent2.id)
end
end
context 'when advanced_assignment is disabled (downgraded account with stale policies)' do
before do
account.enable_features('assignment_v2')
account.save!
create(:inbox_capacity_limit, agent_capacity_policy: agent_capacity_policy, inbox: v2_inbox, conversation_limit: 1)
agent1.account_users.find_by(account: account).update!(agent_capacity_policy: agent_capacity_policy)
create(:conversation, inbox: v2_inbox, account: account, assignee: agent1, status: :open)
end
it 'does not enforce capacity limits' do
result = v2_inbox.member_ids_with_assignment_capacity
expect(result).to include(agent1.id)
end
end
context 'when assignment_v2 is disabled (V1 path)' do
before do
v2_inbox.update(auto_assignment_config: { max_assignment_limit: 2 })
end
it 'uses V1 max_assignment_limit' do
create_list(:conversation, 2, inbox: v2_inbox, account: account, assignee: agent1, status: :open)
result = v2_inbox.member_ids_with_assignment_capacity
expect(result).not_to include(agent1.id)
expect(result).to include(agent2.id)
end
end
end
describe 'audit log' do describe 'audit log' do
context 'when inbox is created' do context 'when inbox is created' do
it 'has associated audit log created' do it 'has associated audit log created' do