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:
co-authored by
tds-1
Muhsin Keloth
Claude Opus 4.6
parent
0849d2e070
commit
7c7459b734
@@ -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
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user