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>
This commit is contained in:
Muhsin Keloth
2025-09-25 11:23:43 +05:30
committed by GitHub
parent 47bdb6d2bb
commit f44e47a624
4 changed files with 85 additions and 29 deletions

View File

@@ -47,36 +47,8 @@ module Whatsapp::IncomingMessageServiceHelpers
%w[reaction ephemeral unsupported request_welcome].include?(message_type) %w[reaction ephemeral unsupported request_welcome].include?(message_type)
end 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) def processed_waid(waid)
# in case of Brazil, we need to do additional processing Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid)
# 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
end end
def error_webhook_event?(message) def error_webhook_event?(message)

View File

@@ -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

View File

@@ -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

View File

@@ -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