fix(whatsapp): override webhook at phone number level (#13817)

## Description

Move WhatsApp webhook callback override from WABA level to phone number
level, allowing multiple phone numbers on the same WABA to have
independent callback URLs.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Connect a WhatsApp Cloud inbox via embedded signup
- Verify webhook setup succeeds and messages are received
- Connect a second phone number on the same WABA — both should receive
messages independently
- Delete an inbox and verify only that phone number's override is
cleared

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes

---------

Co-authored-by: tds-1 <tds-1@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tanmay Deep Sharma
2026-06-25 14:49:09 +05:30
committed by GitHub
co-authored by tds-1 Muhsin Keloth Claude Opus 4.6
parent 0849d2e070
commit 7c7459b734
8 changed files with 176 additions and 114 deletions
@@ -144,8 +144,10 @@ const showWebhookSection = computed(
() => props.healthData?.webhook_configuration !== undefined
);
// Phone-level override takes precedence over WABA-level (application), so prefer it.
const webhookUrl = computed(
() =>
props.healthData?.webhook_configuration?.phone_number ||
props.healthData?.webhook_configuration?.whatsapp_business_account ||
props.healthData?.webhook_configuration?.application
);
+35 -19
View File
@@ -1,5 +1,7 @@
class Whatsapp::FacebookApiClient
BASE_URI = 'https://graph.facebook.com'.freeze
# Base webhook fields resent on every subscribe so Meta won't reset to defaults. `calls` is added by callers only when voice is enabled.
WEBHOOK_DEFAULT_FIELDS = %w[messages smb_message_echoes].freeze
def initialize(access_token = nil)
@access_token = access_token
@@ -60,48 +62,62 @@ class Whatsapp::FacebookApiClient
data['code_verification_status'] == 'VERIFIED'
end
WEBHOOK_DEFAULT_FIELDS = %w[messages smb_message_echoes].freeze
def subscribe_phone_number_webhook(waba_id, phone_number_id, callback_url, verify_token, subscribed_fields: nil)
# Subscribe app to WABA first — Meta requires it before any callback override (issue #13097).
# subscribed_fields (incl. `calls` when voice is enabled) is declared here; the phone-level POST has no such field.
subscribe_app_to_waba(waba_id, subscribed_fields: subscribed_fields || WEBHOOK_DEFAULT_FIELDS)
def subscribe_waba_webhook(waba_id, callback_url, verify_token, subscribed_fields: WEBHOOK_DEFAULT_FIELDS)
# Step 1: Subscribe app to WABA first (required before override)
# Meta requires the app to be subscribed before using override_callback_uri
# See: https://github.com/chatwoot/chatwoot/issues/13097
subscribe_app_to_waba(waba_id)
# Step 2: Override callback URL for this specific WABA
override_waba_callback(waba_id, callback_url, verify_token, subscribed_fields: subscribed_fields)
# Phone-level override takes precedence over WABA-level, so numbers on one WABA can route to different URLs.
override_phone_number_callback(phone_number_id, callback_url, verify_token)
end
def subscribe_app_to_waba(waba_id)
def subscribe_app_to_waba(waba_id, subscribed_fields: WEBHOOK_DEFAULT_FIELDS)
response = HTTParty.post(
"#{BASE_URI}/#{@api_version}/#{waba_id}/subscribed_apps",
headers: request_headers
headers: request_headers,
body: { subscribed_fields: subscribed_fields }.to_json
)
handle_response(response, 'App subscription to WABA failed')
end
def override_waba_callback(waba_id, callback_url, verify_token, subscribed_fields: WEBHOOK_DEFAULT_FIELDS)
def override_phone_number_callback(phone_number_id, callback_url, verify_token)
response = HTTParty.post(
"#{BASE_URI}/#{@api_version}/#{waba_id}/subscribed_apps",
"#{BASE_URI}/#{@api_version}/#{phone_number_id}",
headers: request_headers,
body: {
override_callback_uri: callback_url,
verify_token: verify_token,
subscribed_fields: subscribed_fields
webhook_configuration: {
override_callback_uri: callback_url,
verify_token: verify_token
}
}.to_json
)
handle_response(response, 'Webhook callback override failed')
handle_response(response, 'Phone number webhook callback override failed')
end
def unsubscribe_waba_webhook(waba_id)
def clear_phone_number_callback_override(phone_number_id)
response = HTTParty.post(
"#{BASE_URI}/#{@api_version}/#{phone_number_id}",
headers: request_headers,
body: {
webhook_configuration: {
override_callback_uri: ''
}
}.to_json
)
handle_response(response, 'Phone number webhook callback clear failed')
end
# Fully removes this app's WABA subscription (last inbox deleted) so Meta stops delivering webhooks.
def unsubscribe_app_from_waba(waba_id)
response = HTTParty.delete(
"#{BASE_URI}/#{@api_version}/#{waba_id}/subscribed_apps",
headers: request_headers
)
handle_response(response, 'Webhook unsubscription failed')
handle_response(response, 'WABA app unsubscription failed')
end
private
@@ -27,9 +27,12 @@ class Whatsapp::ReauthorizationService
def update_channel_config(channel, access_token, phone_info)
current_config = channel.provider_config || {}
# Legacy clients may omit phone_number_id; fall back to the value just fetched from Meta.
resolved_phone_number_id = @phone_number_id.presence || phone_info[:phone_number_id]
channel.provider_config = current_config.merge(
'api_key' => access_token,
'phone_number_id' => @phone_number_id,
'phone_number_id' => resolved_phone_number_id,
'business_account_id' => @business_id,
'source' => 'embedded_signup'
)
+18 -2
View File
@@ -28,6 +28,7 @@ class Whatsapp::WebhookSetupService
raise ArgumentError, 'Channel is required' if @channel.blank?
raise ArgumentError, 'WABA ID is required' if @waba_id.blank?
raise ArgumentError, 'Access token is required' if @access_token.blank?
raise ArgumentError, 'Phone number ID is required' if @channel.provider_config['phone_number_id'].blank?
end
def register_phone_number
@@ -58,8 +59,9 @@ class Whatsapp::WebhookSetupService
def setup_webhook
callback_url = build_callback_url
verify_token = @channel.provider_config['webhook_verify_token']
phone_number_id = @channel.provider_config['phone_number_id']
@api_client.subscribe_waba_webhook(@waba_id, callback_url, verify_token, subscribed_fields: subscribed_fields)
@api_client.subscribe_phone_number_webhook(@waba_id, phone_number_id, callback_url, verify_token, subscribed_fields: subscribed_fields)
rescue StandardError => e
Rails.logger.error("[WHATSAPP] Webhook setup failed: #{e.message}")
raise "Webhook setup failed: #{e.message}"
@@ -68,10 +70,24 @@ class Whatsapp::WebhookSetupService
# Subscribe to `calls` only when voice calling is enabled on the inbox
def subscribed_fields
fields = %w[messages smb_message_echoes]
fields << 'calls' if @channel.provider_config['calling_enabled']
fields << 'calls' if calls_enabled_on_waba?
fields
end
# `subscribed_fields` is a WABA-wide app subscription, so keep `calls` whenever this inbox or
# any sibling on the same WABA has voice on — otherwise a non-calling sibling's setup would
# rewrite the shared subscription and drop calls for a calling-enabled sibling.
def calls_enabled_on_waba?
return true if @channel.provider_config['calling_enabled']
Channel::Whatsapp
.where(provider: 'whatsapp_cloud')
.where.not(id: @channel.id)
.where("provider_config->>'business_account_id' = ?", @waba_id)
.where("provider_config->>'calling_enabled' = 'true'")
.exists?
end
def build_callback_url
frontend_url = ENV.fetch('FRONTEND_URL', nil)
phone_number = @channel.phone_number
@@ -6,42 +6,53 @@ class Whatsapp::WebhookTeardownService
def perform
return unless should_teardown_webhook?
teardown_webhook
api_client = Whatsapp::FacebookApiClient.new(provider_config['api_key'])
clear_phone_number_override(api_client)
unsubscribe_app_if_last_inbox(api_client)
rescue StandardError => e
handle_webhook_teardown_error(e)
# before_destroy must never block a channel delete — log and move on.
Rails.logger.error "[WHATSAPP] Webhook teardown failed for channel #{@channel&.id}: #{e.message}"
end
private
def provider_config
@channel.provider_config || {}
end
def should_teardown_webhook?
whatsapp_cloud_provider? && embedded_signup_source? && webhook_config_present?
@channel.provider == 'whatsapp_cloud' &&
provider_config['source'] == 'embedded_signup' &&
provider_config['api_key'].present? &&
(provider_config['phone_number_id'].present? || provider_config['business_account_id'].present?)
end
def whatsapp_cloud_provider?
@channel.provider == 'whatsapp_cloud'
def clear_phone_number_override(api_client)
phone_number_id = provider_config['phone_number_id']
return if phone_number_id.blank?
api_client.clear_phone_number_callback_override(phone_number_id)
Rails.logger.info "[WHATSAPP] Phone-level webhook override cleared for channel #{@channel.id}"
rescue StandardError => e
Rails.logger.error "[WHATSAPP] Phone-level webhook clear failed for channel #{@channel.id}: #{e.message}"
end
def embedded_signup_source?
@channel.provider_config['source'] == 'embedded_signup'
# The app subscription is shared by every inbox on the WABA, so only unsubscribe when this is the last one.
def unsubscribe_app_if_last_inbox(api_client)
waba_id = provider_config['business_account_id']
return if waba_id.blank?
return if waba_sibling_exists?(waba_id)
api_client.unsubscribe_app_from_waba(waba_id)
Rails.logger.info "[WHATSAPP] WABA app subscription removed for channel #{@channel.id}"
rescue StandardError => e
Rails.logger.error "[WHATSAPP] WABA app unsubscribe failed for channel #{@channel.id}: #{e.message}"
end
def webhook_config_present?
@channel.provider_config['business_account_id'].present? &&
@channel.provider_config['api_key'].present?
end
def teardown_webhook
waba_id = @channel.provider_config['business_account_id']
access_token = @channel.provider_config['api_key']
api_client = Whatsapp::FacebookApiClient.new(access_token)
api_client.unsubscribe_waba_webhook(waba_id)
Rails.logger.info "[WHATSAPP] Webhook unsubscribed successfully for channel #{@channel.id}"
end
def handle_webhook_teardown_error(error)
Rails.logger.error "[WHATSAPP] Webhook teardown failed: #{error.message}"
# Don't raise the error to prevent channel deletion from failing
# Failed webhook teardown shouldn't block deletion
def waba_sibling_exists?(waba_id)
Channel::Whatsapp
.where.not(id: @channel.id)
.exists?(["provider_config ->> 'business_account_id' = ?", waba_id])
end
end
@@ -154,17 +154,20 @@ describe Whatsapp::FacebookApiClient do
end
end
describe '#subscribe_waba_webhook' do
describe '#subscribe_phone_number_webhook' do
let(:waba_id) { 'test_waba_id' }
let(:phone_number_id) { 'test_phone_id' }
let(:callback_url) { 'https://example.com/webhook' }
let(:verify_token) { 'test_verify_token' }
context 'when successful' do
before do
# Step 1: Subscribe app to WABA (no body)
# Step 1: Subscribe app to WABA with the default field list (`calls` is added only when voice is enabled).
# Pinning the body guards against regressions that drop a field and break delivery.
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { subscribed_fields: %w[messages smb_message_echoes] }.to_json
)
.to_return(
status: 200,
@@ -172,12 +175,11 @@ describe Whatsapp::FacebookApiClient do
headers: { 'Content-Type' => 'application/json' }
)
# Step 2: Override callback URL (with body)
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
# Step 2: Override callback at phone number level
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes] }.to_json
body: { webhook_configuration: { override_callback_uri: callback_url, verify_token: verify_token } }.to_json
)
.to_return(
status: 200,
@@ -187,7 +189,7 @@ describe Whatsapp::FacebookApiClient do
end
it 'returns success response' do
result = api_client.subscribe_waba_webhook(waba_id, callback_url, verify_token)
result = api_client.subscribe_phone_number_webhook(waba_id, phone_number_id, callback_url, verify_token)
expect(result['success']).to be(true)
end
end
@@ -202,11 +204,13 @@ describe Whatsapp::FacebookApiClient do
end
it 'raises an error' do
expect { api_client.subscribe_waba_webhook(waba_id, callback_url, verify_token) }.to raise_error(/App subscription to WABA failed/)
expect do
api_client.subscribe_phone_number_webhook(waba_id, phone_number_id, callback_url, verify_token)
end.to raise_error(/App subscription to WABA failed/)
end
end
context 'when callback override fails' do
context 'when phone number callback override fails' do
before do
# Step 1 succeeds
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
@@ -220,29 +224,31 @@ describe Whatsapp::FacebookApiClient do
)
# Step 2 fails
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes] }.to_json
body: { webhook_configuration: { override_callback_uri: callback_url, verify_token: verify_token } }.to_json
)
.to_return(status: 400, body: { error: 'Webhook callback override failed' }.to_json)
.to_return(status: 400, body: { error: 'Phone number webhook callback override failed' }.to_json)
end
it 'raises an error' do
expect { api_client.subscribe_waba_webhook(waba_id, callback_url, verify_token) }.to raise_error(/Webhook callback override failed/)
expect do
api_client.subscribe_phone_number_webhook(waba_id, phone_number_id, callback_url, verify_token)
end.to raise_error(/Phone number webhook callback override failed/)
end
end
end
describe '#unsubscribe_waba_webhook' do
let(:waba_id) { 'test_waba_id' }
describe '#clear_phone_number_callback_override' do
let(:phone_number_id) { 'test_phone_id' }
context 'when successful' do
before do
stub_request(:delete, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { webhook_configuration: { override_callback_uri: '' } }.to_json
)
.to_return(
status: 200,
@@ -252,22 +258,23 @@ describe Whatsapp::FacebookApiClient do
end
it 'returns success response' do
result = api_client.unsubscribe_waba_webhook(waba_id)
result = api_client.clear_phone_number_callback_override(phone_number_id)
expect(result['success']).to be(true)
end
end
context 'when failed' do
before do
stub_request(:delete, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { webhook_configuration: { override_callback_uri: '' } }.to_json
)
.to_return(status: 400, body: { error: 'Webhook unsubscription failed' }.to_json)
.to_return(status: 400, body: { error: 'Phone number webhook callback clear failed' }.to_json)
end
it 'raises an error' do
expect { api_client.unsubscribe_waba_webhook(waba_id) }.to raise_error(/Webhook unsubscription failed/)
expect { api_client.clear_phone_number_callback_override(phone_number_id) }.to raise_error(/Phone number webhook callback clear failed/)
end
end
end
@@ -42,17 +42,18 @@ describe Whatsapp::WebhookSetupService do
allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', anything, 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
it 'registers the phone number and sets up webhook' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
smb_message_echoes])
expect(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes])
service.perform
end
end
@@ -65,16 +66,17 @@ describe Whatsapp::WebhookSetupService do
platform_type: 'APPLICABLE',
throughput: { level: 'APPLICABLE' }
})
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', anything, 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
end
it 'does NOT register phone, but sets up webhook' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).not_to receive(:register_phone_number)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
smb_message_echoes])
expect(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes])
service.perform
end
end
@@ -89,17 +91,18 @@ describe Whatsapp::WebhookSetupService do
})
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', anything, 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
it 'registers the phone number due to pending provisioning state' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
smb_message_echoes])
expect(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes])
service.perform
end
end
@@ -114,17 +117,18 @@ describe Whatsapp::WebhookSetupService do
})
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', anything, 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
it 'registers the phone number due to throughput not applicable' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
smb_message_echoes])
expect(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes])
service.perform
end
end
@@ -139,14 +143,14 @@ describe Whatsapp::WebhookSetupService do
})
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number)
allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
it 'tries to register phone (due to verification error) and proceeds with webhook setup' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number)
expect(api_client).to receive(:subscribe_waba_webhook)
expect(api_client).to receive(:subscribe_phone_number_webhook)
expect { service.perform }.not_to raise_error
end
end
@@ -156,13 +160,13 @@ describe Whatsapp::WebhookSetupService do
before do
allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(true)
allow(health_service).to receive(:fetch_health_status).and_raise('Health API down')
allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook).and_return({ 'success' => true })
end
it 'does not register phone (conservative approach) and proceeds with webhook setup' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).not_to receive(:register_phone_number)
expect(api_client).to receive(:subscribe_waba_webhook)
expect(api_client).to receive(:subscribe_phone_number_webhook)
expect { service.perform }.not_to raise_error
end
end
@@ -173,14 +177,14 @@ describe Whatsapp::WebhookSetupService do
allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number).and_raise('Registration failed')
allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
it 'continues with webhook setup even if registration fails' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number)
expect(api_client).to receive(:subscribe_waba_webhook)
expect(api_client).to receive(:subscribe_phone_number_webhook)
expect { service.perform }.not_to raise_error
end
end
@@ -191,13 +195,13 @@ describe Whatsapp::WebhookSetupService do
allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number)
allow(api_client).to receive(:subscribe_waba_webhook).and_raise('Webhook failed')
allow(api_client).to receive(:subscribe_phone_number_webhook).and_raise('Webhook failed')
end
it 'raises an error' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number)
expect(api_client).to receive(:subscribe_waba_webhook)
expect(api_client).to receive(:subscribe_phone_number_webhook)
expect { service.perform }.to raise_error(/Webhook setup failed/)
end
end
@@ -225,7 +229,7 @@ describe Whatsapp::WebhookSetupService do
channel.provider_config['verification_pin'] = 123_456
allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
allow(api_client).to receive(:register_phone_number)
allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
@@ -241,7 +245,7 @@ describe Whatsapp::WebhookSetupService do
context 'when webhook setup fails and should trigger reauthorization' do
before do
allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(true)
allow(api_client).to receive(:subscribe_waba_webhook).and_raise('Invalid access token')
allow(api_client).to receive(:subscribe_phone_number_webhook).and_raise('Invalid access token')
end
it 'raises error with webhook setup failure message' do
@@ -282,15 +286,16 @@ describe Whatsapp::WebhookSetupService do
platform_type: 'APPLICABLE',
throughput: { level: 'APPLICABLE' }
})
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'existing_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', anything, 'existing_verify_token',
subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
end
it 'successfully reauthorizes with new access token' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).not_to receive(:register_phone_number)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'existing_verify_token',
expect(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'existing_verify_token',
subscribed_fields: %w[messages smb_message_echoes])
service_reauth.perform
end
@@ -298,8 +303,9 @@ describe Whatsapp::WebhookSetupService do
it 'uses the existing webhook verify token during reauthorization' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'existing_verify_token', subscribed_fields: %w[messages smb_message_echoes])
expect(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', anything, 'existing_verify_token',
subscribed_fields: %w[messages smb_message_echoes])
service_reauth.perform
end
end
@@ -312,8 +318,9 @@ describe Whatsapp::WebhookSetupService do
platform_type: 'APPLICABLE',
throughput: { level: 'APPLICABLE' }
})
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(api_client).to receive(:subscribe_phone_number_webhook)
.with(waba_id, '123456789', anything, 'test_verify_token',
subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
end
it 'completes successfully without errors' do
@@ -14,26 +14,26 @@ RSpec.describe Whatsapp::WebhookTeardownService do
provider: 'whatsapp_cloud',
provider_config: {
'source' => 'embedded_signup',
'business_account_id' => 'test_waba_id',
'phone_number_id' => 'test_phone_id',
'api_key' => 'test_api_key'
}
)
end
it 'calls unsubscribe_waba_webhook on Facebook API client' do
it 'calls clear_phone_number_callback_override on Facebook API client' do
api_client = instance_double(Whatsapp::FacebookApiClient)
allow(Whatsapp::FacebookApiClient).to receive(:new).with('test_api_key').and_return(api_client)
allow(api_client).to receive(:unsubscribe_waba_webhook).with('test_waba_id')
allow(api_client).to receive(:clear_phone_number_callback_override).with('test_phone_id')
service.perform
expect(api_client).to have_received(:unsubscribe_waba_webhook).with('test_waba_id')
expect(api_client).to have_received(:clear_phone_number_callback_override).with('test_phone_id')
end
it 'handles errors gracefully without raising' do
api_client = instance_double(Whatsapp::FacebookApiClient)
allow(Whatsapp::FacebookApiClient).to receive(:new).and_return(api_client)
allow(api_client).to receive(:unsubscribe_waba_webhook).and_raise(StandardError, 'API Error')
allow(api_client).to receive(:clear_phone_number_callback_override).and_raise(StandardError, 'API Error')
expect { service.perform }.not_to raise_error
end