Files
chatwoot/spec/enterprise/services/voice/outbound_call_builder_spec.rb
6c9efc4e92 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>
2026-07-02 16:06:11 +05:30

132 lines
4.7 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Voice::OutboundCallBuilder do
let(:account) { create(:account) }
let(:channel) { create(:channel_twilio_sms, :with_voice, account: account, phone_number: '+15551230000') }
let(:inbox) { channel.inbox }
let(:user) { create(:user, account: account) }
let(:contact) { create(:contact, account: account, phone_number: '+15550001111') }
let(:call_sid) { 'CA1234567890abcdef' }
before do
allow(Twilio::VoiceWebhookSetupService).to receive(:new)
.and_return(instance_double(Twilio::VoiceWebhookSetupService, perform: "AP#{SecureRandom.hex(8)}"))
allow(inbox).to receive(:channel).and_return(channel)
allow(channel).to receive(:initiate_call).and_return({ call_sid: call_sid })
end
describe '.perform!' do
it 'creates a conversation, Call, and voice_call message' do
call = nil
expect do
call = described_class.perform!(
account: account,
inbox: inbox,
user: user,
contact: contact
)
end.to change(account.conversations, :count).by(1).and change(Call, :count).by(1)
aggregate_failures do
expect(call).to be_a(Call)
expect(call.provider_call_id).to eq(call_sid)
expect(call.direction).to eq('outgoing')
expect(call.status).to eq('ringing')
expect(call.accepted_by_agent_id).to eq(user.id)
expect(call.conference_sid).to eq("conf_account_#{account.id}_call_#{call.id}")
voice_message = call.conversation.messages.voice_calls.last
expect(call.message_id).to eq(voice_message.id)
expect(voice_message.message_type).to eq('outgoing')
expect(voice_message.call).to eq(call)
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,
inbox: inbox,
user: user,
contact: contact
)
expect(call.conversation.identifier).to be_nil
expect(call.conversation.additional_attributes).not_to include('call_status', 'call_direction', 'agent_id', 'conference_sid')
end
it 'raises an error when contact is missing a phone number' do
contact.update!(phone_number: nil)
expect do
described_class.perform!(
account: account,
inbox: inbox,
user: user,
contact: contact
)
end.to raise_error(ArgumentError, 'Contact phone number required')
end
it 'raises an error when user is nil' do
expect do
described_class.perform!(
account: account,
inbox: inbox,
user: nil,
contact: contact
)
end.to raise_error(ArgumentError, 'Agent required')
end
end
end