fix(voice): bail on Meta failures and restrict initiate to WhatsApp inboxes
This commit is contained in:
@@ -14,10 +14,14 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def reject
|
||||
@call = Whatsapp::CallService.new(call: @call, agent: Current.user).reject
|
||||
rescue Voice::CallErrors::CallFailed => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
def terminate
|
||||
@call = Whatsapp::CallService.new(call: @call, agent: Current.user).terminate
|
||||
rescue Voice::CallErrors::CallFailed => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
# Browser-supplied recording captured via MediaRecorder. Idempotent: the
|
||||
@@ -62,9 +66,11 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
|
||||
authorize @conversation, :show?
|
||||
end
|
||||
|
||||
# WhatsApp Cloud Calling specifically — Twilio voice channels also expose
|
||||
# voice_enabled? but use a different (Voice::OutboundCallBuilder) initiation path.
|
||||
def calling_enabled?(conversation)
|
||||
channel = conversation.inbox.channel
|
||||
channel.respond_to?(:voice_enabled?) && channel.voice_enabled?
|
||||
channel.is_a?(Channel::Whatsapp) && channel.voice_enabled?
|
||||
end
|
||||
|
||||
# Browser-built SDP offer is forwarded to Meta; the connect webhook later delivers Meta's answer.
|
||||
|
||||
@@ -15,7 +15,7 @@ class Whatsapp::CallService
|
||||
call.with_lock do
|
||||
next if call.terminal? || call.in_progress?
|
||||
|
||||
invoke_provider(:reject_call)
|
||||
invoke_provider!(:reject_call)
|
||||
finalize_call('failed')
|
||||
end
|
||||
call
|
||||
@@ -25,9 +25,8 @@ class Whatsapp::CallService
|
||||
call.with_lock do
|
||||
next if call.terminal?
|
||||
|
||||
invoke_provider(:terminate_call)
|
||||
# An agent who hangs up before the contact picks up should mark the call no_answer,
|
||||
# mirroring the webhook terminate path in Whatsapp::IncomingCallService.
|
||||
invoke_provider!(:terminate_call)
|
||||
# Agent hangs up before contact picks up → no_answer; mirrors the webhook terminate path.
|
||||
finalize_call(call.in_progress? ? 'completed' : 'no_answer')
|
||||
end
|
||||
call
|
||||
@@ -56,11 +55,17 @@ class Whatsapp::CallService
|
||||
call.conversation.update!(assignee: agent) if call.conversation.assignee_id.blank?
|
||||
end
|
||||
|
||||
def invoke_provider(method)
|
||||
# Raise on Meta failure (bool false or transport error) so callers bail before
|
||||
# finalizing local state — otherwise we'd mark a still-active call as ended
|
||||
# and broadcast voice_call.ended while Meta thinks it's live.
|
||||
def invoke_provider!(method)
|
||||
success = call.inbox.channel.provider_service.public_send(method, call.provider_call_id)
|
||||
Rails.logger.error "[WHATSAPP CALL] #{method} returned false for #{call.provider_call_id}" unless success
|
||||
raise Voice::CallErrors::CallFailed, "Meta #{method} failed" unless success
|
||||
rescue Voice::CallErrors::CallFailed
|
||||
raise
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "[WHATSAPP CALL] #{method} failed: #{e.message}"
|
||||
Rails.logger.error "[WHATSAPP CALL] #{method} failed: #{e.class} #{e.message}"
|
||||
raise Voice::CallErrors::CallFailed, "Meta #{method} failed"
|
||||
end
|
||||
|
||||
def finalize_call(status)
|
||||
|
||||
@@ -136,6 +136,19 @@ RSpec.describe 'WhatsApp Calls API', type: :request do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Meta error')
|
||||
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)
|
||||
twilio_conversation = create(:conversation, account: account, inbox: twilio_channel.inbox)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/whatsapp_calls/initiate",
|
||||
params: { conversation_id: twilio_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.not_enabled'))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/whatsapp_calls/:id/upload_recording' do
|
||||
|
||||
@@ -83,6 +83,14 @@ describe Whatsapp::CallService do
|
||||
|
||||
expect(provider_service).not_to have_received(:reject_call)
|
||||
end
|
||||
|
||||
it 'raises CallFailed and leaves the call ringing when Meta rejects the request' do
|
||||
allow(provider_service).to receive(:reject_call).and_return(false)
|
||||
|
||||
expect { described_class.new(call: call, agent: agent).reject }
|
||||
.to raise_error(Voice::CallErrors::CallFailed)
|
||||
expect(call.reload.status).to eq('ringing')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#terminate' do
|
||||
|
||||
Reference in New Issue
Block a user