From 58f5ae61579950debece2101748da41bbb009b53 Mon Sep 17 00:00:00 2001 From: Rodrigo Borba Date: Thu, 26 Feb 2026 10:10:09 -0300 Subject: [PATCH] Fix Wuzapi webhook handling --- .../webhooks/whatsapp_controller.rb | 20 ++++++++++-- app/jobs/webhooks/whatsapp_events_job.rb | 14 ++++++-- .../webhooks/whatsapp_controller_spec.rb | 10 ++++++ .../jobs/webhooks/whatsapp_events_job_spec.rb | 32 +++++++++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/app/controllers/webhooks/whatsapp_controller.rb b/app/controllers/webhooks/whatsapp_controller.rb index 088677556..59459fe8b 100644 --- a/app/controllers/webhooks/whatsapp_controller.rb +++ b/app/controllers/webhooks/whatsapp_controller.rb @@ -30,19 +30,33 @@ class Webhooks::WhatsappController < ActionController::API end def valid_token?(token) - channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number]) + channel = find_channel_by_phone_number(params[:phone_number]) whatsapp_webhook_verify_token = channel.provider_config['webhook_verify_token'] if channel.present? token == whatsapp_webhook_verify_token if whatsapp_webhook_verify_token.present? end def inactive_whatsapp_number? - phone_number = params[:phone_number] + phone_number = normalize_phone(params[:phone_number]) return false if phone_number.blank? inactive_numbers = GlobalConfig.get_value('INACTIVE_WHATSAPP_NUMBERS').to_s return false if inactive_numbers.blank? inactive_numbers_array = inactive_numbers.split(',').map(&:strip) - inactive_numbers_array.include?(phone_number) + inactive_numbers_array.map { |number| normalize_phone(number) }.include?(phone_number) + end + + def find_channel_by_phone_number(phone_number) + raw_phone = phone_number.to_s.strip + digits_only = normalize_phone(raw_phone) + return if raw_phone.blank? && digits_only.blank? + + Channel::Whatsapp.find_by(phone_number: raw_phone) || + Channel::Whatsapp.find_by(phone_number: "+#{digits_only}") || + Channel::Whatsapp.where("regexp_replace(phone_number, '[^0-9]', '', 'g') = ?", digits_only).first + end + + def normalize_phone(phone_number) + phone_number.to_s.gsub(/\D/, '') end end diff --git a/app/jobs/webhooks/whatsapp_events_job.rb b/app/jobs/webhooks/whatsapp_events_job.rb index be94301a4..a7dc0de9e 100644 --- a/app/jobs/webhooks/whatsapp_events_job.rb +++ b/app/jobs/webhooks/whatsapp_events_job.rb @@ -79,7 +79,7 @@ class Webhooks::WhatsappEventsJob < ApplicationJob return unless params[:phone_number] - Channel::Whatsapp.find_by(phone_number: params[:phone_number]) + find_channel_by_phone_number(params[:phone_number]) end def channel_is_inactive?(channel) @@ -96,8 +96,18 @@ class Webhooks::WhatsappEventsJob < ApplicationJob # we will give priority to the phone_number in the payload phone_number = "+#{params[:entry].first[:changes].first.dig(:value, :metadata, :display_phone_number)}" phone_number_id = params[:entry].first[:changes].first.dig(:value, :metadata, :phone_number_id) - channel = Channel::Whatsapp.find_by(phone_number: phone_number) + channel = find_channel_by_phone_number(phone_number) # validate to ensure the phone number id matches the whatsapp channel channel if channel && channel.provider_config['phone_number_id'] == phone_number_id end + + def find_channel_by_phone_number(phone_number) + raw_phone = phone_number.to_s.strip + digits_only = raw_phone.gsub(/\D/, '') + return if raw_phone.blank? && digits_only.blank? + + Channel::Whatsapp.find_by(phone_number: raw_phone) || + Channel::Whatsapp.find_by(phone_number: "+#{digits_only}") || + Channel::Whatsapp.where("regexp_replace(phone_number, '[^0-9]', '', 'g') = ?", digits_only).first + end end diff --git a/spec/controllers/webhooks/whatsapp_controller_spec.rb b/spec/controllers/webhooks/whatsapp_controller_spec.rb index 59541dd60..ad2e73c95 100644 --- a/spec/controllers/webhooks/whatsapp_controller_spec.rb +++ b/spec/controllers/webhooks/whatsapp_controller_spec.rb @@ -46,6 +46,16 @@ RSpec.describe 'Webhooks::WhatsappController', type: :request do expect(response).to have_http_status(:unprocessable_entity) expect(response.parsed_body['error']).to eq('Inactive WhatsApp number') end + + it 'returns service unavailable even when incoming phone has different formatting' do + allow(Rails.logger).to receive(:warn) + allow(GlobalConfig).to receive(:get_value).with('INACTIVE_WHATSAPP_NUMBERS').and_return('+55 (61) 91234-5678') + + post '/webhooks/whatsapp/5561912345678', params: { content: 'hello' } + + expect(Rails.logger).to have_received(:warn).with('Rejected webhook for inactive WhatsApp number: 5561912345678') + expect(response).to have_http_status(:unprocessable_entity) + end end context 'when INACTIVE_WHATSAPP_NUMBERS config is not set' do diff --git a/spec/jobs/webhooks/whatsapp_events_job_spec.rb b/spec/jobs/webhooks/whatsapp_events_job_spec.rb index 2fe314d93..5ae760a81 100644 --- a/spec/jobs/webhooks/whatsapp_events_job_spec.rb +++ b/spec/jobs/webhooks/whatsapp_events_job_spec.rb @@ -109,6 +109,38 @@ RSpec.describe Webhooks::WhatsappEventsJob do end end + context 'when phone number format differs in webhook payload' do + it 'finds channel by normalized phone number and processes event' do + wuzapi_channel = create( + :channel_whatsapp, + provider: 'wuzapi', + phone_number: '+55 (61) 99186-8492', + sync_templates: false, + validate_provider_config: false + ) + wuzapi_params = { + phone_number: '5561991868492', + type: 'Message', + event: { + Info: { + ID: '3A721AC0A215F23ACFA9', + IsFromMe: false, + IsGroup: false, + Type: 'text', + Sender: '556182098580@s.whatsapp.net', + Timestamp: Time.current.iso8601 + }, + Message: { conversation: 'Ola terse' } + } + } + + allow(Whatsapp::IncomingMessageWuzapiService).to receive(:new).and_return(process_service) + expect(Whatsapp::IncomingMessageWuzapiService).to receive(:new).with(inbox: wuzapi_channel.inbox, params: wuzapi_params) + + job.perform_now(wuzapi_params) + end + end + context 'when whatsapp business params' do it 'enqueue Whatsapp::IncomingMessageWhatsappCloudService based on the number in payload' do other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,