diff --git a/enterprise/app/services/whatsapp/incoming_call_service.rb b/enterprise/app/services/whatsapp/incoming_call_service.rb index 3b4d17378..1bead1013 100644 --- a/enterprise/app/services/whatsapp/incoming_call_service.rb +++ b/enterprise/app/services/whatsapp/incoming_call_service.rb @@ -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' diff --git a/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb b/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb index 65e988e68..f8cbb3576 100644 --- a/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb +++ b/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb @@ -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