diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue index 04c54ff0d..948f69812 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue @@ -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 ); diff --git a/app/services/whatsapp/facebook_api_client.rb b/app/services/whatsapp/facebook_api_client.rb index 22e75aac0..7e74e8ac6 100644 --- a/app/services/whatsapp/facebook_api_client.rb +++ b/app/services/whatsapp/facebook_api_client.rb @@ -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 diff --git a/app/services/whatsapp/reauthorization_service.rb b/app/services/whatsapp/reauthorization_service.rb index aeb6dfbef..141417886 100644 --- a/app/services/whatsapp/reauthorization_service.rb +++ b/app/services/whatsapp/reauthorization_service.rb @@ -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' ) diff --git a/app/services/whatsapp/webhook_setup_service.rb b/app/services/whatsapp/webhook_setup_service.rb index 2abf113da..7bf93c62d 100644 --- a/app/services/whatsapp/webhook_setup_service.rb +++ b/app/services/whatsapp/webhook_setup_service.rb @@ -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 diff --git a/app/services/whatsapp/webhook_teardown_service.rb b/app/services/whatsapp/webhook_teardown_service.rb index c4a39a5eb..948d84f04 100644 --- a/app/services/whatsapp/webhook_teardown_service.rb +++ b/app/services/whatsapp/webhook_teardown_service.rb @@ -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 diff --git a/spec/services/whatsapp/facebook_api_client_spec.rb b/spec/services/whatsapp/facebook_api_client_spec.rb index 74fb2f6e2..5dda2aaeb 100644 --- a/spec/services/whatsapp/facebook_api_client_spec.rb +++ b/spec/services/whatsapp/facebook_api_client_spec.rb @@ -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 diff --git a/spec/services/whatsapp/webhook_setup_service_spec.rb b/spec/services/whatsapp/webhook_setup_service_spec.rb index e80036f32..15d32efaf 100644 --- a/spec/services/whatsapp/webhook_setup_service_spec.rb +++ b/spec/services/whatsapp/webhook_setup_service_spec.rb @@ -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 diff --git a/spec/services/whatsapp/webhook_teardown_service_spec.rb b/spec/services/whatsapp/webhook_teardown_service_spec.rb index 2a7ba9fd0..be94f3c44 100644 --- a/spec/services/whatsapp/webhook_teardown_service_spec.rb +++ b/spec/services/whatsapp/webhook_teardown_service_spec.rb @@ -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