fix(voice): keep terminate handling idempotent for ended calls

A retried terminate webhook for an answered short call (duration=0)
would re-enter handle_terminate, find call.in_progress? false, fall
through to no_answer, and corrupt a completed call's status. Bail when
the call is already terminal so retries don't recompute the status.
This commit is contained in:
Tanmay Deep Sharma
2026-05-06 16:43:31 +07:00
parent 20af850432
commit cb180dfc5f
2 changed files with 15 additions and 0 deletions
@@ -52,6 +52,10 @@ class Whatsapp::IncomingCallService
def handle_terminate(payload)
call = Call.whatsapp.find_by(provider_call_id: payload[:id])
return unless call
# Webhook retries can re-deliver terminate after we've already finalized the
# call; don't recompute status or a duration=0 retry can flip a completed
# short call back to no_answer.
return if call.terminal?
duration = payload[:duration]&.to_i
status = answered?(call, duration) ? 'completed' : 'no_answer'
@@ -101,6 +101,17 @@ describe Whatsapp::IncomingCallService do
described_class.new(inbox: inbox, params: params).perform
expect(call.reload.status).to eq('no_answer')
end
it 'is a no-op when the call is already terminal so retries cannot flip a completed call to no_answer' do
call.update!(status: 'completed', duration_seconds: 5, direction: :outgoing, accepted_by_agent: nil)
allow(ActionCable.server).to receive(:broadcast)
params = call_payload(event: 'terminate', duration: 0, terminate_reason: 'completed_normally')
described_class.new(inbox: inbox, params: params).perform
expect(call.reload).to have_attributes(status: 'completed', duration_seconds: 5)
expect(ActionCable.server).not_to have_received(:broadcast)
end
end
describe 'duplicate inbound connect' do