Merge branch 'feat/whatsapp-call-incoming-pipeline' into feat/whatsapp-call-meta-bridge
This commit is contained in:
@@ -61,8 +61,13 @@ class Voice::InboundCallBuilder
|
||||
end
|
||||
|
||||
# WhatsApp ContactInbox.source_id must be digits-only (the wa_id); Twilio accepts the +.
|
||||
# Run BR/AR-style wa_id normalization (same path messaging uses) so an inbound call
|
||||
# finds the existing ContactInbox instead of forking a new contact/conversation.
|
||||
def source_id_for_provider
|
||||
provider == :whatsapp ? from_number.to_s.delete_prefix('+') : from_number
|
||||
return from_number unless provider == :whatsapp
|
||||
|
||||
digits = from_number.to_s.delete_prefix('+')
|
||||
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider(digits, :cloud)
|
||||
end
|
||||
|
||||
def resolve_conversation!(contact, contact_inbox)
|
||||
|
||||
@@ -83,6 +83,10 @@ class Whatsapp::IncomingCallService
|
||||
# still surfaces in the dashboard instead of being silently dropped.
|
||||
call = Call.whatsapp.find_by(provider_call_id: payload[:id]) || build_missed_inbound_call(payload)
|
||||
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'
|
||||
|
||||
@@ -100,6 +100,31 @@ RSpec.describe Voice::InboundCallBuilder do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the WhatsApp wa_id needs Brazil normalization to match an existing ContactInbox' do
|
||||
let(:whatsapp_channel) do
|
||||
create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
|
||||
provider_config: { 'phone_number_id' => '123', 'calling_enabled' => true },
|
||||
validate_provider_config: false, sync_templates: false)
|
||||
end
|
||||
let(:whatsapp_inbox) { whatsapp_channel.inbox }
|
||||
let!(:stored_contact) { create(:contact, account: account, phone_number: '+5541988887777') }
|
||||
let!(:stored_contact_inbox) do
|
||||
create(:contact_inbox, contact: stored_contact, inbox: whatsapp_inbox, source_id: '5541988887777')
|
||||
end
|
||||
|
||||
it 'reuses the contact via normalized wa_id rather than forking a new ContactInbox' do
|
||||
call = described_class.perform!(
|
||||
inbox: whatsapp_inbox,
|
||||
from_number: '+554188887777',
|
||||
call_sid: 'wacall_br_1',
|
||||
provider: :whatsapp
|
||||
)
|
||||
|
||||
expect(call.contact).to eq(stored_contact)
|
||||
expect(call.conversation.contact_inbox).to eq(stored_contact_inbox)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the inbox has lock_to_single_conversation enabled' do
|
||||
let!(:contact) { create(:contact, account: account, phone_number: from_number) }
|
||||
let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox, source_id: from_number) }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user