fix(whatsapp): normalize contact phone numbers to prevent Brazil duplicates

WhatsApp Coexistence echo messages from the native WhatsApp app deliver
Brazilian numbers without the mobile 9th digit. The contact-resolution
layer only deduplicated at the contact_inbox source_id level, so when no
matching contact_inbox existed it fell back to an exact phone_number match
using the un-normalized number, missing the existing contact stored with
the 9th digit and creating a duplicate.

Normalize the phone number used for contact lookup/creation via the
existing Brazil/Argentina normalizers so both formats resolve to the same
contact. Non-affected countries are returned unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Muhsin
2026-06-05 13:03:27 +04:00
co-authored by Claude Opus 4.8
parent 73ba0b26e5
commit fe4ba71cdf
3 changed files with 51 additions and 1 deletions
@@ -76,11 +76,17 @@ module Whatsapp::IncomingMessageIdentifierHelper
phone_number = whatsapp_phone_number(phone_identifier)
return { name: name } if phone_number.blank?
formatted_phone_number = "+#{phone_number}"
# 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)}"
display_name = name == phone_identifier ? formatted_phone_number : name
{ name: display_name, phone_number: formatted_phone_number }
end
def normalized_phone_number(phone_number)
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_number(phone_number)
end
def update_whatsapp_identifiers(source_ids: [], username: nil, phone_number: nil)
Whatsapp::IdentifierSyncService.new(contact_inbox: @contact_inbox, contact: @contact).perform(source_ids: source_ids, username: username,
phone_number: phone_number)
@@ -29,6 +29,15 @@ class Whatsapp::PhoneNumberNormalizationService
existing_contact_inbox&.source_id || raw_number
end
# @param clean_number [String] Phone number in clean cloud format, e.g. "5541988887777"
# @return [String] Country-normalized number, or the original when no normalizer applies
def normalize_number(clean_number)
normalizer = find_normalizer_for_country(clean_number)
return clean_number unless normalizer
normalizer.normalize(clean_number)
end
private
attr_reader :inbox
@@ -257,6 +257,41 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
end
end
end
context 'when an echo (coexistence) message arrives for a Brazil number missing the 9th digit' do
let(:echo_params) do
{
object: 'whatsapp_business_account',
entry: [{
changes: [{
field: 'smb_message_echoes',
value: {
message_echoes: [{
from: whatsapp_channel.phone_number.delete('+'),
to: '551112345678',
id: 'wamid.ECHO_MESSAGE_ID',
timestamp: '1664799904',
text: { body: 'Reply from the WhatsApp app' },
type: 'text'
}]
}
}]
}]
}.with_indifferent_access
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)
expect do
described_class.new(inbox: whatsapp_channel.inbox, params: echo_params, outgoing_echo: true).perform
end.not_to change(Contact, :count)
conversation = whatsapp_channel.inbox.conversations.last
expect(conversation.contact).to eq(existing_contact)
expect(conversation.messages.last.content).to eq('Reply from the WhatsApp app')
end
end
end
# Métodos auxiliares para reduzir o tamanho do exemplo