fix(voice): don't mint inbound calls for outbound connect webhooks
If Meta delivered a connect for an outbound call before the controller committed the Call row (race between Meta's API response and our INSERT), the handler treated the unknown call_id as inbound and either tripped the unique (provider, provider_call_id) index or broadcast voice_call.incoming for an outbound. Use session.sdp_type to gate inbound creation on 'offer' only; 'answer' with no row is logged and skipped — the next status webhook finds the now-committed row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
74f9cd764b
commit
29fd83c1ea
@@ -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!(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user