From f0119b3f2b49138054efd00595931b3739b46e15 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Thu, 7 May 2026 17:00:11 +0700 Subject: [PATCH] fix(voice): preserve duration when agents hang up active WhatsApp calls finalize_call wasn't writing duration_seconds or end_reason, and the Meta terminate webhook that would have filled them in is suppressed by the terminal-state idempotency guard in IncomingCallService#handle_terminate. The result was completed-call bubbles with no duration. Compute the duration from started_at at agent-hangup time and stamp end_reason locally instead of waiting for a webhook that won't apply. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/services/whatsapp/call_service.rb | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/enterprise/app/services/whatsapp/call_service.rb b/enterprise/app/services/whatsapp/call_service.rb index ec8205e63..8a52ea6bc 100644 --- a/enterprise/app/services/whatsapp/call_service.rb +++ b/enterprise/app/services/whatsapp/call_service.rb @@ -30,8 +30,16 @@ class Whatsapp::CallService next if call.terminal? invoke_provider!(:terminate_call) - # Agent hangs up before contact picks up → no_answer; mirrors the webhook terminate path. - finalize_call(call.in_progress? ? 'completed' : 'no_answer') + # Compute duration from started_at locally — the webhook arrives after the + # call is already terminal and the idempotency guard there bails before it + # can fill these fields, so we have to record them here. + if call.in_progress? + duration = call.started_at ? (Time.current - call.started_at).to_i : nil + finalize_call('completed', duration_seconds: duration, end_reason: 'agent_hangup') + else + # Agent hangs up before contact picks up → no_answer; mirrors the webhook terminate path. + finalize_call('no_answer', end_reason: 'agent_hangup') + end end call end @@ -73,16 +81,16 @@ class Whatsapp::CallService raise Voice::CallErrors::CallFailed, "Meta #{method} failed" end - def finalize_call(status) + def finalize_call(status, **attrs) meta = (call.meta || {}).merge('ended_at' => Time.zone.now.to_i) - call.update!(status: status, meta: meta) - update_message_status(status) + call.update!(status: status, meta: meta, **attrs) + update_message_status(status, duration_seconds: attrs[:duration_seconds]) update_conversation_call_status(call.display_status) broadcast(:ended, status: call.display_status) end - def update_message_status(status) - Voice::CallMessageBuilder.new(call).update_status!(status: status, agent: agent) + def update_message_status(status, duration_seconds: nil) + Voice::CallMessageBuilder.new(call).update_status!(status: status, agent: agent, duration_seconds: duration_seconds) end def update_conversation_call_status(status)