diff --git a/app/builders/contact_inbox_with_contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb index 2ba9179ed..940eaf4a2 100644 --- a/app/builders/contact_inbox_with_contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -67,7 +67,7 @@ class ContactInboxWithContactBuilder def find_contact contact = find_contact_by_identifier(contact_attributes[:identifier]) contact ||= find_contact_by_email(contact_attributes[:email]) - contact ||= find_contact_by_phone_number(contact_attributes[:phone_number]) + contact ||= find_contact_by_phone_numbers contact ||= find_contact_by_instagram_source_id(source_id) if instagram_channel? contact @@ -107,9 +107,14 @@ class ContactInboxWithContactBuilder account.contacts.from_email(email) end - def find_contact_by_phone_number(phone_number) - return if phone_number.blank? + def find_contact_by_phone_numbers + phone_numbers = [contact_attributes[:phone_number], *Array(contact_attributes[:phone_number_candidates])].compact_blank.uniq - account.contacts.find_by(phone_number: phone_number) + phone_numbers.each do |phone_number| + contact = account.contacts.find_by(phone_number: phone_number) + return contact if contact + end + + nil end end diff --git a/app/services/whatsapp/incoming_message_base_service.rb b/app/services/whatsapp/incoming_message_base_service.rb index 09632c5b1..d7818896f 100644 --- a/app/services/whatsapp/incoming_message_base_service.rb +++ b/app/services/whatsapp/incoming_message_base_service.rb @@ -217,8 +217,10 @@ class Whatsapp::IncomingMessageBaseService message_phone_number = whatsapp_phone_number(messages_data.first[:from]) return false if message_phone_number.blank? - phone_number = "+#{message_phone_number}" - formatted_phone_number = TelephoneNumber.parse(phone_number).international_number - @contact.name == phone_number || @contact.name == formatted_phone_number + [message_phone_number, normalized_phone_number(message_phone_number)].uniq.any? do |number| + phone_number = "+#{number}" + formatted_phone_number = TelephoneNumber.parse(phone_number).international_number + @contact.name == phone_number || @contact.name == formatted_phone_number + end end end diff --git a/app/services/whatsapp/incoming_message_identifier_helper.rb b/app/services/whatsapp/incoming_message_identifier_helper.rb index 07dc70983..d29272f30 100644 --- a/app/services/whatsapp/incoming_message_identifier_helper.rb +++ b/app/services/whatsapp/incoming_message_identifier_helper.rb @@ -76,11 +76,13 @@ module Whatsapp::IncomingMessageIdentifierHelper phone_number = whatsapp_phone_number(phone_identifier) return { name: name } if phone_number.blank? - # Normalize before storing/matching so country-specific format variations (e.g. Brazil's - # missing 9th digit) resolve to the same contact instead of creating duplicates. - formatted_phone_number = "+#{normalized_phone_number(phone_number)}" + formatted_phone_number = "+#{phone_number}" display_name = name == phone_identifier ? formatted_phone_number : name - { name: display_name, phone_number: formatted_phone_number } + attributes = { name: display_name, phone_number: formatted_phone_number } + + normalized_number = normalized_phone_number(phone_number) + attributes[:phone_number_candidates] = ["+#{normalized_number}"] if normalized_number != phone_number + attributes end def normalized_phone_number(phone_number) diff --git a/app/services/whatsapp/phone_normalizers/brazil_phone_normalizer.rb b/app/services/whatsapp/phone_normalizers/brazil_phone_normalizer.rb index 24a1c406e..bd9afcb92 100644 --- a/app/services/whatsapp/phone_normalizers/brazil_phone_normalizer.rb +++ b/app/services/whatsapp/phone_normalizers/brazil_phone_normalizer.rb @@ -1,21 +1,23 @@ # Handles Brazil phone number normalization # ref: https://github.com/chatwoot/chatwoot/issues/5840 +# ref: https://www.gov.br/anatel/pt-br/regulado/numeracao/perguntas-frequentes # # Brazil changed its mobile number system by adding a "9" prefix to existing numbers. -# This normalizer adds the "9" digit if the number is 12 digits (making it 13 digits total) -# to match the new format: 55 + DDD + 9 + number +# This normalizer adds the "9" only to legacy eight-digit mobile ranges, leaving +# eight-digit landlines (which start with 2-5) unchanged. class Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer < Whatsapp::PhoneNormalizers::BasePhoneNormalizer COUNTRY_CODE_LENGTH = 2 DDD_LENGTH = 2 + LEGACY_MOBILE_NUMBER_PATTERN = /\A[6-9]\d{7}\z/ def normalize(waid) return waid unless handles_country?(waid) ddd = waid[COUNTRY_CODE_LENGTH, DDD_LENGTH] number = waid[COUNTRY_CODE_LENGTH + DDD_LENGTH, waid.length - (COUNTRY_CODE_LENGTH + DDD_LENGTH)] - normalized_number = "55#{ddd}#{number}" - normalized_number = "55#{ddd}9#{number}" if normalized_number.length != 13 - normalized_number + return waid unless number.match?(LEGACY_MOBILE_NUMBER_PATTERN) + + "55#{ddd}9#{number}" end private diff --git a/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb b/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb index 2248c1617..b8000d0f7 100644 --- a/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb @@ -376,6 +376,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do end context 'when an echo (coexistence) message arrives for a Brazil number missing the 9th digit' do + let(:echo_phone_number) { '554188887777' } let(:echo_params) do { object: 'whatsapp_business_account', @@ -385,7 +386,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do value: { message_echoes: [{ from: whatsapp_channel.phone_number.delete('+'), - to: '551112345678', + to: echo_phone_number, id: 'wamid.ECHO_MESSAGE_ID', timestamp: '1664799904', text: { body: 'Reply from the WhatsApp app' }, @@ -398,7 +399,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do end it 'reuses the existing contact stored with the normalized phone number instead of duplicating it' do - existing_contact = create(:contact, phone_number: '+5511912345678', account: whatsapp_channel.account) + existing_contact = create(:contact, phone_number: '+5541988887777', account: whatsapp_channel.account) expect do described_class.new(inbox: whatsapp_channel.inbox, params: echo_params, outgoing_echo: true).perform @@ -408,6 +409,65 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do expect(conversation.contact).to eq(existing_contact) expect(conversation.messages.last.content).to eq('Reply from the WhatsApp app') end + + it 'prefers an existing contact stored with the raw phone number' do + existing_contact = create(:contact, phone_number: '+554188887777', account: whatsapp_channel.account) + + expect do + described_class.new(inbox: whatsapp_channel.inbox, params: echo_params, outgoing_echo: true).perform + end.not_to change(Contact, :count) + + expect(whatsapp_channel.inbox.conversations.last.contact).to eq(existing_contact) + end + + context 'when the number is a valid landline' do + let(:echo_phone_number) { '554132345678' } + + it 'does not match a mobile contact created by adding a ninth digit' do + mobile_contact = create(:contact, phone_number: '+5541932345678', account: whatsapp_channel.account) + + expect do + described_class.new(inbox: whatsapp_channel.inbox, params: echo_params, outgoing_echo: true).perform + end.to change(Contact, :count).by(1) + + contact = whatsapp_channel.inbox.conversations.last.contact + expect(contact).not_to eq(mobile_contact) + expect(contact.phone_number).to eq('+554132345678') + end + end + end + + context 'when an incoming Brazil message supplies a profile name after normalized contact matching' do + let(:incoming_params) do + { + object: 'whatsapp_business_account', + entry: [{ + changes: [{ + value: { + contacts: [{ profile: { name: 'Maria Silva' }, wa_id: '554188887777' }], + messages: [{ + from: '554188887777', + id: 'wamid.BRAZIL_PROFILE_NAME', + timestamp: '1664799904', + text: { body: 'Hello' }, + type: 'text' + }] + } + }] + }] + }.with_indifferent_access + end + + it 'replaces a normalized phone-number placeholder with the profile name' do + existing_contact = create(:contact, name: '+5541988887777', phone_number: '+5541988887777', account: whatsapp_channel.account) + + expect do + described_class.new(inbox: whatsapp_channel.inbox, params: incoming_params).perform + end.not_to change(Contact, :count) + + expect(existing_contact.reload.name).to eq('Maria Silva') + expect(whatsapp_channel.inbox.conversations.last.contact).to eq(existing_contact) + end end end