From f44e47a624a19802cb5de9ec950ac624dbbca5b9 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 25 Sep 2025 11:23:43 +0530 Subject: [PATCH] feat: Extract Brazil phone number normalization into generic service (#12492) This PR refactors existing Brazil phone number normalization logic into a generic, extensible service while maintaining backward compatibility. Also extracts it into a dedicated service designed for expansion to support additional countries. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../incoming_message_service_helpers.rb | 30 +------------- .../base_phone_normalizer.rb | 19 +++++++++ .../brazil_phone_normalizer.rb | 26 +++++++++++++ .../phone_number_normalization_service.rb | 39 +++++++++++++++++++ 4 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 app/services/whatsapp/phone_normalizers/base_phone_normalizer.rb create mode 100644 app/services/whatsapp/phone_normalizers/brazil_phone_normalizer.rb create mode 100644 app/services/whatsapp/phone_number_normalization_service.rb diff --git a/app/services/whatsapp/incoming_message_service_helpers.rb b/app/services/whatsapp/incoming_message_service_helpers.rb index c5474314b..e40dc408f 100644 --- a/app/services/whatsapp/incoming_message_service_helpers.rb +++ b/app/services/whatsapp/incoming_message_service_helpers.rb @@ -47,36 +47,8 @@ module Whatsapp::IncomingMessageServiceHelpers %w[reaction ephemeral unsupported request_welcome].include?(message_type) end - def brazil_phone_number?(phone_number) - phone_number.match(/^55/) - end - - # ref: https://github.com/chatwoot/chatwoot/issues/5840 - def normalised_brazil_mobile_number(phone_number) - # DDD : Area codes in Brazil are popularly known as "DDD codes" (códigos DDD) or simply "DDD", from the initials of "direct distance dialing" - # https://en.wikipedia.org/wiki/Telephone_numbers_in_Brazil - ddd = phone_number[2, 2] - # Remove country code and DDD to obtain the number - number = phone_number[4, phone_number.length - 4] - normalised_number = "55#{ddd}#{number}" - # insert 9 to convert the number to the new mobile number format - normalised_number = "55#{ddd}9#{number}" if normalised_number.length != 13 - normalised_number - end - def processed_waid(waid) - # in case of Brazil, we need to do additional processing - # https://github.com/chatwoot/chatwoot/issues/5840 - if brazil_phone_number?(waid) - # check if there is an existing contact inbox with the normalised waid - # We will create conversation against it - contact_inbox = inbox.contact_inboxes.find_by(source_id: normalised_brazil_mobile_number(waid)) - - # if there is no contact inbox with the waid without 9, - # We will create contact inboxes and contacts with the number 9 added - waid = contact_inbox.source_id if contact_inbox.present? - end - waid + Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid) end def error_webhook_event?(message) diff --git a/app/services/whatsapp/phone_normalizers/base_phone_normalizer.rb b/app/services/whatsapp/phone_normalizers/base_phone_normalizer.rb new file mode 100644 index 000000000..91882b8e5 --- /dev/null +++ b/app/services/whatsapp/phone_normalizers/base_phone_normalizer.rb @@ -0,0 +1,19 @@ +# Base class for country-specific phone number normalizers +# Each country normalizer should inherit from this class and implement: +# - country_code_pattern: regex to identify the country code +# - normalize: logic to convert phone number to normalized format for contact lookup +class Whatsapp::PhoneNormalizers::BasePhoneNormalizer + def handles_country?(waid) + waid.match(country_code_pattern) + end + + def normalize(waid) + raise NotImplementedError, 'Subclasses must implement #normalize' + end + + private + + def country_code_pattern + raise NotImplementedError, 'Subclasses must implement #country_code_pattern' + end +end diff --git a/app/services/whatsapp/phone_normalizers/brazil_phone_normalizer.rb b/app/services/whatsapp/phone_normalizers/brazil_phone_normalizer.rb new file mode 100644 index 000000000..24a1c406e --- /dev/null +++ b/app/services/whatsapp/phone_normalizers/brazil_phone_normalizer.rb @@ -0,0 +1,26 @@ +# Handles Brazil phone number normalization +# ref: https://github.com/chatwoot/chatwoot/issues/5840 +# +# 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 +class Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer < Whatsapp::PhoneNormalizers::BasePhoneNormalizer + COUNTRY_CODE_LENGTH = 2 + DDD_LENGTH = 2 + + 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 + end + + private + + def country_code_pattern + /^55/ + end +end diff --git a/app/services/whatsapp/phone_number_normalization_service.rb b/app/services/whatsapp/phone_number_normalization_service.rb new file mode 100644 index 000000000..b8e416794 --- /dev/null +++ b/app/services/whatsapp/phone_number_normalization_service.rb @@ -0,0 +1,39 @@ +# Service to handle phone number normalization for WhatsApp messages +# Currently supports Brazil phone number format variations +# Designed to be extensible for additional countries in future PRs +# +# Usage: Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid) +class Whatsapp::PhoneNumberNormalizationService + def initialize(inbox) + @inbox = inbox + end + + # Main entry point for phone number normalization + # Returns the source_id of an existing contact if found, otherwise returns original waid + def normalize_and_find_contact(waid) + normalizer = find_normalizer_for_country(waid) + return waid unless normalizer + + normalized_waid = normalizer.normalize(waid) + existing_contact_inbox = find_existing_contact_inbox(normalized_waid) + + existing_contact_inbox&.source_id || waid + end + + private + + attr_reader :inbox + + def find_normalizer_for_country(waid) + NORMALIZERS.map(&:new) + .find { |normalizer| normalizer.handles_country?(waid) } + end + + def find_existing_contact_inbox(normalized_waid) + inbox.contact_inboxes.find_by(source_id: normalized_waid) + end + + NORMALIZERS = [ + Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer + ].freeze +end