fix: assign outbound voice call conversation to the calling agent (#14906)

Outbound voice calls were being auto-assigned to the wrong agent. When
an agent placed an outbound call, the conversation was created without
an assignee, so inboxes with auto-assignment enabled would round-robin
it to a different agent instead of keeping it with the person who
actually made the call. This made it hard to tell which agent was on an
active call.

## What changed

- Set the calling agent as the conversation's assignee when creating an
outbound voice call conversation.
- This prevents the generic auto-assignment handler from treating the
conversation as unassigned and reassigning it.
- Added specs covering the assignment, including a regression case with
inbox auto-assignment enabled.

**Note:** this applies to newly placed calls; it does not retroactively
fix conversations that were already mis-assigned.

---------

Co-authored-by: Tanmay Deep Sharma <tanmaydeepsharma21@gmail.com>
Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
This commit is contained in:
Shivam Mishra
2026-07-02 16:06:11 +05:30
committed by GitHub
co-authored by Tanmay Deep Sharma Tanmay Deep Sharma
parent 34d8741b9b
commit 6c9efc4e92
4 changed files with 88 additions and 0 deletions
@@ -113,6 +113,31 @@ RSpec.describe 'WhatsApp Calls API', type: :request do
expect(Call.find_by(provider_call_id: 'wacid_outbound')).to have_attributes(direction: 'outgoing', status: 'ringing')
end
it 'assigns the conversation to the agent placing the call when it is unassigned' do
allow(provider_service).to receive(:initiate_call).and_return({ 'calls' => [{ 'id' => 'wacid_outbound' }] })
post "/api/v1/accounts/#{account.id}/whatsapp_calls/initiate",
params: { conversation_id: initiate_conversation.display_id, sdp_offer: 'sdp_offer' },
headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(initiate_conversation.reload.assignee_id).to eq(agent.id)
end
it 'keeps the existing assignee when the conversation is already assigned' do
other_agent = create(:user, account: account, role: :agent)
create(:inbox_member, user: other_agent, inbox: inbox)
initiate_conversation.update!(assignee: other_agent)
allow(provider_service).to receive(:initiate_call).and_return({ 'calls' => [{ 'id' => 'wacid_outbound' }] })
post "/api/v1/accounts/#{account.id}/whatsapp_calls/initiate",
params: { conversation_id: initiate_conversation.display_id, sdp_offer: 'sdp_offer' },
headers: agent.create_new_auth_token
expect(response).to have_http_status(:ok)
expect(initiate_conversation.reload.assignee_id).to eq(other_agent.id)
end
it 'sends a permission request and records the wamid when Meta returns NoCallPermission' do
allow(provider_service).to receive(:initiate_call).and_raise(Voice::CallErrors::NoCallPermission)
allow(provider_service).to receive(:send_call_permission_request).and_return({ 'messages' => [{ 'id' => 'wamid.req_xyz' }] })
@@ -44,6 +44,54 @@ RSpec.describe Voice::OutboundCallBuilder do
end
end
it 'assigns the conversation to the agent placing the call' do
call = described_class.perform!(
account: account,
inbox: inbox,
user: user,
contact: contact
)
expect(call.conversation.assignee_id).to eq(user.id)
end
it 'keeps the calling agent assigned even when auto-assignment would pick an online agent' do
other_agent = create(:user, account: account)
create(:inbox_member, inbox: inbox, user: other_agent)
create(:inbox_member, inbox: inbox, user: user)
inbox.update!(enable_auto_assignment: true)
# Only other_agent is online, so round-robin would claim the conversation unless the caller wins at creation.
OnlineStatusTracker.update_presence(account.id, 'User', other_agent.id)
OnlineStatusTracker.set_status(account.id, other_agent.id, 'online')
call = described_class.perform!(
account: account,
inbox: inbox,
user: user,
contact: contact
)
expect(call.conversation.assignee_id).to eq(user.id)
end
it 'claims a reused conversation for the caller when it is unassigned' do
# Reload so the builder gets a DB-fresh record, mirroring the controller's find_by load.
conversation = create(:conversation, account: account, inbox: inbox, contact: contact).reload
described_class.perform!(account: account, inbox: inbox, user: user, contact: contact, conversation: conversation)
expect(conversation.reload.assignee_id).to eq(user.id)
end
it 'keeps the existing assignee when a reused conversation is already assigned' do
other_agent = create(:user, account: account)
conversation = create(:conversation, account: account, inbox: inbox, contact: contact, assignee: other_agent).reload
described_class.perform!(account: account, inbox: inbox, user: user, contact: contact, conversation: conversation)
expect(conversation.reload.assignee_id).to eq(other_agent.id)
end
it 'does not set conversation.identifier or write call state to additional_attributes' do
call = described_class.perform!(
account: account,