diff --git a/enterprise/app/services/whatsapp/incoming_call_service.rb b/enterprise/app/services/whatsapp/incoming_call_service.rb index 3abf0f4be..8976dd28f 100644 --- a/enterprise/app/services/whatsapp/incoming_call_service.rb +++ b/enterprise/app/services/whatsapp/incoming_call_service.rb @@ -19,12 +19,26 @@ class Whatsapp::IncomingCallService def handle_connect(payload) call = Call.whatsapp.find_by(provider_call_id: payload[:id]) - return create_inbound_call(payload) if call.nil? + if call.nil? + # Only an `offer` payload is a real inbound caller. An `answer` with no + # local row means Meta beat our outbound `Call.create!` (tiny window + # between initiate API response and DB insert) — do not mint an inbound + # row for it; the next status webhook (or a retry) will find it. + return create_inbound_call(payload) if inbound_offer?(payload) + + Rails.logger.warn "[WHATSAPP CALL] Outbound connect for unknown call #{payload[:id]}; skipping" + return + end + return accept_outbound_call(call, payload) if call.outgoing? Rails.logger.info "[WHATSAPP CALL] Duplicate inbound connect for #{payload[:id]}; ignoring" end + def inbound_offer?(payload) + payload.dig(:session, :sdp_type).to_s.downcase == 'offer' + end + def create_inbound_call(payload) sdp_offer = payload.dig(:session, :sdp) call = Voice::InboundCallBuilder.perform!( diff --git a/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb b/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb index f8cbb3576..caf69d74c 100644 --- a/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb +++ b/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb @@ -114,6 +114,21 @@ describe Whatsapp::IncomingCallService do end end + describe 'outbound connect with no local row yet' do + it 'does not mint an inbound call when sdp_type is answer' do + allow(inbox.channel).to receive(:voice_enabled?).and_return(true) + allow(Rails.logger).to receive(:warn) + allow(ActionCable.server).to receive(:broadcast) + + params = call_payload(event: 'connect', session: { sdp: 'sdp_answer', sdp_type: 'answer' }) + + expect { described_class.new(inbox: inbox, params: params).perform } + .not_to change(Call, :count) + expect(Rails.logger).to have_received(:warn).with(/Outbound connect for unknown call/) + expect(ActionCable.server).not_to have_received(:broadcast) + end + end + describe 'duplicate inbound connect' do let!(:call) do conversation = create(:conversation, account: account, inbox: inbox)