fix(voice): order accept guards correctly and validate contact phone upstream

This commit is contained in:
Tanmay Deep Sharma
2026-04-30 17:46:24 +07:00
parent a7fdc45977
commit cbd3f759b5
5 changed files with 26 additions and 5 deletions
+1
View File
@@ -119,6 +119,7 @@ en:
no_recording: 'No recording file provided'
no_message: 'Call has no associated message'
sdp_offer_required: 'sdp_offer is required'
contact_phone_required: 'Contact phone number is required'
permission_request_failed: 'Failed to send call permission request'
inboxes:
imap:
@@ -44,6 +44,7 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
def initiate
return render_could_not_create_error(I18n.t('errors.whatsapp.calls.not_enabled')) unless calling_enabled?(@conversation)
return render_could_not_create_error(I18n.t('errors.whatsapp.calls.sdp_offer_required')) if params[:sdp_offer].blank?
return render_could_not_create_error(I18n.t('errors.whatsapp.calls.contact_phone_required')) if @conversation.contact&.phone_number.blank?
@call = create_outbound_call(@conversation, params[:sdp_offer])
@message = Voice::CallMessageBuilder.new(@call).perform!
@@ -74,10 +75,9 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
end
# Browser-built SDP offer is forwarded to Meta; the connect webhook later delivers Meta's answer.
# Caller must validate `conversation.contact.phone_number` is present (initiate's guard does).
def create_outbound_call(conversation, sdp_offer)
contact_phone = conversation.contact&.phone_number
raise ArgumentError, 'Contact phone number not available' if contact_phone.blank?
contact_phone = conversation.contact.phone_number
result = conversation.inbox.channel.provider_service.initiate_call(contact_phone.delete('+'), sdp_offer)
provider_call_id = result.dig('calls', 0, 'id') || result['call_id']
@@ -35,8 +35,10 @@ class Whatsapp::CallService
private
def transition_to_in_progress!
raise Voice::CallErrors::NotRinging, 'Call is not in ringing state' unless call.ringing?
# Order matters: in_progress and terminal both make ringing? false, so we have to
# branch on in_progress? first to surface the distinct AlreadyAccepted state.
raise Voice::CallErrors::AlreadyAccepted, 'Call already accepted by another agent' if call.in_progress?
raise Voice::CallErrors::NotRinging, 'Call is not in ringing state' unless call.ringing?
forward_answer_to_meta!
call.update!(status: 'in_progress', accepted_by_agent_id: agent.id, started_at: Time.current,
@@ -137,6 +137,17 @@ RSpec.describe 'WhatsApp Calls API', type: :request do
expect(response.parsed_body['error']).to eq('Meta error')
end
it 'returns 422 when the conversation contact has no phone number' do
contact.update!(phone_number: nil)
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(:unprocessable_entity)
expect(response.parsed_body['error']).to eq(I18n.t('errors.whatsapp.calls.contact_phone_required'))
end
it 'returns 422 when the conversation belongs to a non-WhatsApp inbox' do
twilio_channel = create(:channel_twilio_sms, :with_voice, account: account, phone_number: '+15551239998')
create(:inbox_member, user: agent, inbox: twilio_channel.inbox)
@@ -50,9 +50,16 @@ describe Whatsapp::CallService do
expect(conversation.reload.assignee_id).to eq(agent.id)
end
it 'raises NotRinging when the call is no longer ringing' do
it 'raises AlreadyAccepted when another agent has already accepted the call' do
call.update!(status: 'in_progress')
expect { described_class.new(call: call, agent: agent, sdp_answer: sdp_answer).accept }
.to raise_error(Voice::CallErrors::AlreadyAccepted)
end
it 'raises NotRinging when the call has reached a terminal state' do
call.update!(status: 'completed')
expect { described_class.new(call: call, agent: agent, sdp_answer: sdp_answer).accept }
.to raise_error(Voice::CallErrors::NotRinging)
end