From b8df75a5ab9b771a208ed068ca1c7233320749cb Mon Sep 17 00:00:00 2001 From: Muhsin <12408980+muhsin-k@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:44:18 +0400 Subject: [PATCH] fix(whatsapp): normalize Mexico phone numbers for contact matching WhatsApp/Twilio may send Mexican numbers with an extra 1 after the country code (521XXXXXXXXXX) while the contact is stored without it (52XXXXXXXXXX), creating duplicate contacts and conversations. Add a Mexico normalizer alongside the existing Brazil/Argentina ones and register it in the normalization service so both the WhatsApp Cloud and Twilio inbound paths match the existing contact. Reopens #14174 (original author unavailable). Closes #14048. --- .../mexico_phone_normalizer.rb | 23 +++++++++ .../phone_number_normalization_service.rb | 5 +- .../twilio/incoming_message_service_spec.rb | 51 +++++++++++++++++++ ...ing_message_whatsapp_cloud_service_spec.rb | 43 ++++++++++++++++ .../mexico_phone_normalizer_spec.rb | 23 +++++++++ 5 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 app/services/whatsapp/phone_normalizers/mexico_phone_normalizer.rb create mode 100644 spec/services/whatsapp/phone_normalizers/mexico_phone_normalizer_spec.rb diff --git a/app/services/whatsapp/phone_normalizers/mexico_phone_normalizer.rb b/app/services/whatsapp/phone_normalizers/mexico_phone_normalizer.rb new file mode 100644 index 000000000..00a2df1aa --- /dev/null +++ b/app/services/whatsapp/phone_normalizers/mexico_phone_normalizer.rb @@ -0,0 +1,23 @@ +# Handles Mexico phone number normalization +# +# WhatsApp may include a "1" after Mexico's country code (521), while contacts +# can already be stored without it (52). Normalize to the stored WAID (digits-only) +# format; provider formatting (whatsapp:+...) happens in the normalization service. +# The mobile "1" only appears on the 13-digit form (52 + 1 + 10 digits), so guard +# on length to avoid stripping a legitimate "1" from a shorter national number. +class Whatsapp::PhoneNormalizers::MexicoPhoneNormalizer < Whatsapp::PhoneNormalizers::BasePhoneNormalizer + COUNTRY_CODE_WITH_MOBILE_LENGTH = 13 + + def normalize(waid) + return waid unless handles_country?(waid) + return waid unless waid.length == COUNTRY_CODE_WITH_MOBILE_LENGTH + + waid.sub(/^521/, '52') + end + + private + + def country_code_pattern + /^52/ + end +end diff --git a/app/services/whatsapp/phone_number_normalization_service.rb b/app/services/whatsapp/phone_number_normalization_service.rb index 1e52d9b02..853c7d24e 100644 --- a/app/services/whatsapp/phone_number_normalization_service.rb +++ b/app/services/whatsapp/phone_number_normalization_service.rb @@ -1,5 +1,5 @@ # Service to handle phone number normalization for WhatsApp messages -# Currently supports Brazil and Argentina phone number format variations +# Currently supports Brazil, Argentina, and Mexico phone number format variations # Supports both WhatsApp Cloud API and Twilio WhatsApp providers class Whatsapp::PhoneNumberNormalizationService def initialize(inbox) @@ -64,6 +64,7 @@ class Whatsapp::PhoneNumberNormalizationService NORMALIZERS = [ Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer, - Whatsapp::PhoneNormalizers::ArgentinaPhoneNormalizer + Whatsapp::PhoneNormalizers::ArgentinaPhoneNormalizer, + Whatsapp::PhoneNormalizers::MexicoPhoneNormalizer ].freeze end diff --git a/spec/services/twilio/incoming_message_service_spec.rb b/spec/services/twilio/incoming_message_service_spec.rb index dc30f6e62..642425fd5 100644 --- a/spec/services/twilio/incoming_message_service_spec.rb +++ b/spec/services/twilio/incoming_message_service_spec.rb @@ -788,6 +788,57 @@ describe Twilio::IncomingMessageService do expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+541123456789') end end + + describe 'When the incoming number is a Mexican WhatsApp number with 1 after country code' do + let!(:whatsapp_twilio_channel) do + create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx', + inbox: create(:inbox, account: account, greeting_enabled: false)) + end + + it 'appends to existing contact when contact inbox exists without the extra 1' do + contact = create(:contact, account: account, phone_number: '+525512345678') + contact_inbox = create(:contact_inbox, + source_id: 'whatsapp:+525512345678', + contact: contact, + inbox: whatsapp_twilio_channel.inbox) + last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox) + + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+5215512345678', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Mexico', + ProfileName: 'Maria Lopez' + } + + described_class.new(params: params).perform + + expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1) + expect(last_conversation.messages.last.content).to eq('Test message from Mexico') + expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id) + .to eq('whatsapp:+525512345678') + end + + it 'creates contact inbox with incoming number when no existing contact matches' do + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+5215512345678', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Mexico', + ProfileName: 'Maria Lopez' + } + + described_class.new(params: params).perform + + expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0) + expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('Maria Lopez') + expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Mexico') + expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id) + .to eq('whatsapp:+5215512345678') + end + end end end end 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 60b153775..ef1aba3fc 100644 --- a/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb @@ -224,6 +224,49 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do end end + describe 'when the incoming waid is a Mexican number with 1 after country code' do + let(:mexico_params) do + { + phone_number: whatsapp_channel.phone_number, + object: 'whatsapp_business_account', + entry: [{ + changes: [{ + value: { + contacts: [{ profile: { name: 'Maria Lopez' }, wa_id: '5215512345678' }], + messages: [{ + from: '5215512345678', + id: 'wamid.cloud-mexico-message', + text: { body: 'Test message from Mexico' }, + timestamp: '1664799904', + type: 'text' + }] + } + }] + }] + }.with_indifferent_access + end + + it 'appends to existing contact when a contact inbox exists without the extra 1' do + contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: '525512345678') + last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox) + + described_class.new(inbox: whatsapp_channel.inbox, params: mexico_params).perform + + expect(whatsapp_channel.inbox.conversations.count).to eq(1) + expect(last_conversation.messages.last.content).to eq('Test message from Mexico') + expect(whatsapp_channel.inbox.contact_inboxes.first.source_id).to eq('525512345678') + end + + it 'creates a contact inbox with the incoming waid when no existing contact matches' do + described_class.new(inbox: whatsapp_channel.inbox, params: mexico_params).perform + + expect(whatsapp_channel.inbox.conversations.count).not_to eq(0) + expect(whatsapp_channel.inbox.contacts.first.name).to eq('Maria Lopez') + expect(whatsapp_channel.inbox.messages.first.content).to eq('Test message from Mexico') + expect(whatsapp_channel.inbox.contact_inboxes.first.source_id).to eq('5215512345678') + end + end + context 'when invalid params' do it 'will not throw error' do described_class.new(inbox: whatsapp_channel.inbox, params: { phone_number: whatsapp_channel.phone_number, diff --git a/spec/services/whatsapp/phone_normalizers/mexico_phone_normalizer_spec.rb b/spec/services/whatsapp/phone_normalizers/mexico_phone_normalizer_spec.rb new file mode 100644 index 000000000..ad5216afb --- /dev/null +++ b/spec/services/whatsapp/phone_normalizers/mexico_phone_normalizer_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe Whatsapp::PhoneNormalizers::MexicoPhoneNormalizer do + subject(:normalizer) { described_class.new } + + describe '#normalize' do + it 'strips the mobile "1" from the 13-digit form (521 + 10 digits)' do + expect(normalizer.normalize('5215512345678')).to eq('525512345678') + end + + it 'leaves the already-normalized 12-digit form untouched' do + expect(normalizer.normalize('525512345678')).to eq('525512345678') + end + + it 'does not strip a legitimate leading "1" from a 12-digit national number' do + expect(normalizer.normalize('521512345678')).to eq('521512345678') + end + + it 'returns non-Mexico numbers unchanged' do + expect(normalizer.normalize('5511999998888')).to eq('5511999998888') + end + end +end