From e5a03d08ea9d3c5fac6a3795b3d5d98ca31b54e1 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Thu, 30 Apr 2026 16:51:29 +0700 Subject: [PATCH] fix(voice): resolve contact via source_id and i18n call message bubble --- config/locales/en.yml | 3 +++ .../services/voice/call_message_builder.rb | 2 +- .../services/voice/inbound_call_builder.rb | 24 ++++++++++--------- .../whatsapp/call_permission_reply_service.rb | 6 ++--- .../voice/inbound_call_builder_spec.rb | 14 +++++++++++ 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index ae068d4f7..c225392a4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -244,6 +244,9 @@ en: whatsapp: list_button_label: 'Choose an item' call_permission_request_body: 'We would like to call you regarding your conversation.' + voice_call: + twilio: 'Voice Call' + whatsapp: 'WhatsApp Call' delivery_status: error_code: 'Error code: %{error_code}' activity: diff --git a/enterprise/app/services/voice/call_message_builder.rb b/enterprise/app/services/voice/call_message_builder.rb index 493cdcb55..11420d60d 100644 --- a/enterprise/app/services/voice/call_message_builder.rb +++ b/enterprise/app/services/voice/call_message_builder.rb @@ -27,7 +27,7 @@ class Voice::CallMessageBuilder def create_message! params = { - content: 'Voice Call', + content: I18n.t("conversations.messages.voice_call.#{call.provider}"), message_type: call.outgoing? ? 'outgoing' : 'incoming', content_type: 'voice_call', content_attributes: { 'data' => build_data_payload } diff --git a/enterprise/app/services/voice/inbound_call_builder.rb b/enterprise/app/services/voice/inbound_call_builder.rb index 512a36886..1bf953d5a 100644 --- a/enterprise/app/services/voice/inbound_call_builder.rb +++ b/enterprise/app/services/voice/inbound_call_builder.rb @@ -19,8 +19,8 @@ class Voice::InboundCallBuilder return existing if existing ActiveRecord::Base.transaction do - contact = ensure_contact! - contact_inbox = ensure_contact_inbox!(contact) + contact_inbox = ensure_contact_inbox! + contact = contact_inbox.contact conversation = resolve_conversation!(contact, contact_inbox) call = create_call!(contact, conversation) message = Voice::CallMessageBuilder.new(call).perform! @@ -43,21 +43,23 @@ class Voice::InboundCallBuilder .find_by(provider: provider, provider_call_id: call_sid) end + # Always look up by (inbox, source_id) first — that pair has a UNIQUE index, so + # creating with a colliding source_id under a different contact would raise + # RecordNotUnique. Reuse the existing ContactInbox (and its contact) when found. + def ensure_contact_inbox! + sid = source_id_for_provider + existing = inbox.contact_inboxes.find_by(source_id: sid) + return existing if existing + + ContactInbox.create!(contact: ensure_contact!, inbox: inbox, source_id: sid) + end + def ensure_contact! account.contacts.find_or_create_by!(phone_number: from_number) do |record| record.name = from_number if record.name.blank? end end - def ensure_contact_inbox!(contact) - ContactInbox.find_or_create_by!( - contact_id: contact.id, - inbox_id: inbox.id - ) do |record| - record.source_id = source_id_for_provider - end - end - # WhatsApp ContactInbox.source_id must be digits-only (the wa_id); Twilio accepts the +. def source_id_for_provider provider == :whatsapp ? from_number.to_s.delete_prefix('+') : from_number diff --git a/enterprise/app/services/whatsapp/call_permission_reply_service.rb b/enterprise/app/services/whatsapp/call_permission_reply_service.rb index c6db29daf..b1dfc38cd 100644 --- a/enterprise/app/services/whatsapp/call_permission_reply_service.rb +++ b/enterprise/app/services/whatsapp/call_permission_reply_service.rb @@ -29,10 +29,10 @@ class Whatsapp::CallPermissionReplyService { from_number: message[:from], accepted: accepted } end + # WhatsApp routing is anchored on contact_inboxes.source_id (the wa_id), not on + # contacts.phone_number — phone numbers can drift via normalization or edits. def find_contact(from_number) - inbox.contact_inboxes.joins(:contact) - .where(contacts: { phone_number: "+#{from_number}" }) - .first&.contact + inbox.contact_inboxes.find_by(source_id: from_number)&.contact end # Filter to threads that actually requested permission; multiple open threads otherwise hit the wrong one. diff --git a/spec/enterprise/services/voice/inbound_call_builder_spec.rb b/spec/enterprise/services/voice/inbound_call_builder_spec.rb index 3b1ede3ba..02f0b3885 100644 --- a/spec/enterprise/services/voice/inbound_call_builder_spec.rb +++ b/spec/enterprise/services/voice/inbound_call_builder_spec.rb @@ -86,6 +86,20 @@ RSpec.describe Voice::InboundCallBuilder do end end + context 'when a ContactInbox already exists for the source_id (different contact)' do + let!(:original_contact) { create(:contact, account: account, phone_number: '+15550009999') } + let!(:original_contact_inbox) do + create(:contact_inbox, contact: original_contact, inbox: inbox, source_id: from_number) + end + + it 'reuses the existing ContactInbox instead of raising RecordNotUnique' do + call = perform_builder + + expect(call.contact).to eq(original_contact) + expect(call.conversation.contact_inbox).to eq(original_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) }