diff --git a/app/services/whatsapp/providers/base_service.rb b/app/services/whatsapp/providers/base_service.rb index 9fd1f6267..4f8a6e489 100644 --- a/app/services/whatsapp/providers/base_service.rb +++ b/app/services/whatsapp/providers/base_service.rb @@ -54,6 +54,20 @@ class Whatsapp::Providers::BaseService message.save! end + # WhatsApp coexistence / username migration: a contact may become addressable only by a Business-Scoped + # User ID (BSUID, e.g. "BR.123..."), with no phone number available. The Cloud API requires a BSUID to be + # passed in the `recipient` field (with recipient_type: individual), NOT in `to`. Passing a BSUID in `to` + # returns HTTP 200 with a message id but the message is silently dropped: the "CC." prefix is stripped and + # the remainder is treated as a phone number (wa_id), which never resolves. Phone numbers keep using `to`. + # See: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids/ + def recipient_params(identifier) + if identifier.to_s.match?(RegexHelper::WHATSAPP_BSUID_REGEX) + { recipient_type: 'individual', recipient: identifier } + else + { to: identifier } + end + end + def create_buttons(items) buttons = [] items.each do |item| diff --git a/app/services/whatsapp/providers/whatsapp_cloud_service.rb b/app/services/whatsapp/providers/whatsapp_cloud_service.rb index d65c6cc62..09270a588 100644 --- a/app/services/whatsapp/providers/whatsapp_cloud_service.rb +++ b/app/services/whatsapp/providers/whatsapp_cloud_service.rb @@ -17,7 +17,8 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi request_body = { messaging_product: 'whatsapp', recipient_type: 'individual', # Only individual messages supported (not group messages) - to: phone_number, + # BSUID -> `recipient`; phone number -> `to` (see recipient_params in the base provider). + **recipient_params(phone_number), type: 'template', template: template_body } @@ -128,7 +129,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi body: { messaging_product: 'whatsapp', context: whatsapp_reply_context(message), - to: phone_number, + **recipient_params(phone_number), text: { body: message.outgoing_content }, type: 'text' }.to_json @@ -148,7 +149,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi body: { :messaging_product => 'whatsapp', :context => whatsapp_reply_context(message), - 'to' => phone_number, + **recipient_params(phone_number), 'type' => type, type.to_s => type_content }.to_json @@ -243,7 +244,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi headers: api_headers, body: { messaging_product: 'whatsapp', - to: phone_number, + **recipient_params(phone_number), interactive: payload, type: 'interactive' }.to_json diff --git a/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb index a94ba2e44..c409c9a69 100644 --- a/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb @@ -227,6 +227,59 @@ describe Whatsapp::Providers::WhatsappCloudService do end end + describe 'when the recipient is a Business-Scoped User ID (BSUID)' do + # Meta requires a BSUID to be sent in the `recipient` field (with recipient_type: individual), not `to`. + let(:bsuid) { 'BR.13491208655302741918' } + + it 'sends a text message via the recipient field instead of to' do + stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages') + .with( + body: { + messaging_product: 'whatsapp', + context: nil, + recipient_type: 'individual', + recipient: bsuid, + text: { body: message.content }, + type: 'text' + }.to_json + ) + .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers) + + expect(service.send_message(bsuid, message)).to eq 'message_id' + end + + it 'sends a template via the recipient field instead of to' do + template_info = { name: 'test_template', namespace: 'test_namespace', lang_code: 'en_US', parameters: [] } + stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages') + .with(body: hash_including({ messaging_product: 'whatsapp', recipient_type: 'individual', recipient: bsuid, type: 'template' })) + .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers) + + expect(service.send_template(bsuid, template_info, message)).to eq 'message_id' + end + + it 'sends an interactive message via the recipient field instead of to' do + interactive_message = create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox, + content_type: 'input_select', + content_attributes: { items: [{ title: 'Burito', value: 'Burito' }] }) + stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages') + .with(body: hash_including({ messaging_product: 'whatsapp', recipient_type: 'individual', recipient: bsuid, type: 'interactive' })) + .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers) + + expect(service.send_message(bsuid, interactive_message)).to eq 'message_id' + end + + it 'sends an attachment via the recipient field instead of to' do + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + + stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages') + .with(body: hash_including({ messaging_product: 'whatsapp', recipient_type: 'individual', recipient: bsuid, type: 'image' })) + .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers) + + expect(service.send_message(bsuid, message)).to eq 'message_id' + end + end + describe '#sync_templates' do context 'when called' do it 'updated the message templates' do