fix(voice): lock webhook transitions and skip unknown terminates

- Wrap mark_outbound_accepted, accept_outbound_call, and handle_terminate in
  call.with_lock so they serialize against Whatsapp::CallService (which holds
  call.with_lock across the Meta API call). Without this, an ACCEPTED webhook
  arriving mid-terminate could overwrite the freshly-finalized terminal status
  back to in_progress and broadcast an erroneous outbound_accepted.

- Drop build_missed_inbound_call. An outbound terminate landing in the tiny
  window between provider_service.initiate_call and Call.create! had no local
  row, so the fallback materialised an inbound row with the same
  provider_call_id and tripped the unique (provider, provider_call_id) index
  on the controller's create. The "out-of-order webhook" case it protected is
  rare in practice; trade that for eliminating the false-positive collision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tanmay Deep Sharma
2026-05-07 16:20:07 +07:00
co-authored by Claude Opus 4.7
parent c5236ec67e
commit dfdadc470d
2 changed files with 53 additions and 35 deletions
@@ -114,6 +114,21 @@ describe Whatsapp::IncomingCallService do
end
end
describe 'terminate with no local row yet' do
it 'logs and skips instead of materialising an inbound missed-call row' 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: 'terminate', duration: 0, terminate_reason: 'no_answer')
expect { described_class.new(inbox: inbox, params: params).perform }
.not_to change(Call, :count)
expect(Rails.logger).to have_received(:warn).with(/Terminate for unknown call/)
expect(ActionCable.server).not_to have_received(:broadcast)
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)