From 7e4d93f64994ac87cf850f1fc975667654839b07 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 19 Jan 2026 14:12:36 +0400 Subject: [PATCH] fix: Setup webhooks for manual WhatsApp Cloud channel creation (#13278) Fixes https://github.com/chatwoot/chatwoot/issues/13097 ### Problem The PR #12176 removed the `before_save :setup_webhooks` callback to fix a race condition where Meta's webhook verification request arrived before the channel was saved to the database. This change broke manual WhatsApp Cloud channel setup. While embedded signup explicitly calls `channel.setup_webhooks` in `EmbeddedSignupService`, manual setup had no equivalent call - meaning the `subscribed_apps` endpoint was never invoked and Meta never sent webhook events to Chatwoot. ### Solution Added an `after_commit` callback that triggers webhook setup for manual WhatsApp Cloud channels --- app/models/channel/whatsapp.rb | 7 +++ .../whatsapp/embedded_signup_service.rb | 4 ++ spec/factories/channel/channel_whatsapp.rb | 12 ++++- spec/models/channel/whatsapp_spec.rb | 49 +++++++++++++++---- .../whatsapp/webhook_setup_service_spec.rb | 6 ++- 5 files changed, 65 insertions(+), 13 deletions(-) diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index 7318cd978..5905c54f7 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -34,6 +34,7 @@ class Channel::Whatsapp < ApplicationRecord after_create :sync_templates before_destroy :teardown_webhooks + after_commit :setup_webhooks, on: :create, if: :should_auto_setup_webhooks? def name 'Whatsapp' @@ -86,4 +87,10 @@ class Channel::Whatsapp < ApplicationRecord def teardown_webhooks Whatsapp::WebhookTeardownService.new(self).perform end + + def should_auto_setup_webhooks? + # Only auto-setup webhooks for whatsapp_cloud provider with manual setup + # Embedded signup calls setup_webhooks explicitly in EmbeddedSignupService + provider == 'whatsapp_cloud' && provider_config['source'] != 'embedded_signup' + end end diff --git a/app/services/whatsapp/embedded_signup_service.rb b/app/services/whatsapp/embedded_signup_service.rb index 4379d0b74..52273bc5d 100644 --- a/app/services/whatsapp/embedded_signup_service.rb +++ b/app/services/whatsapp/embedded_signup_service.rb @@ -16,6 +16,10 @@ class Whatsapp::EmbeddedSignupService validate_token_access(access_token) channel = create_or_reauthorize_channel(access_token, phone_info) + # NOTE: We call setup_webhooks explicitly here instead of relying on after_commit callback because: + # 1. Reauthorization flow updates an existing channel (not a create), so after_commit on: :create won't trigger + # 2. We need to run check_channel_health_and_prompt_reauth after webhook setup completes + # 3. The channel is marked with source: 'embedded_signup' to skip the after_commit callback channel.setup_webhooks check_channel_health_and_prompt_reauth(channel) channel diff --git a/spec/factories/channel/channel_whatsapp.rb b/spec/factories/channel/channel_whatsapp.rb index dae7eb04f..4282a374d 100644 --- a/spec/factories/channel/channel_whatsapp.rb +++ b/spec/factories/channel/channel_whatsapp.rb @@ -96,8 +96,16 @@ FactoryBot.define do channel_whatsapp.define_singleton_method(:sync_templates) { nil } unless options.sync_templates channel_whatsapp.define_singleton_method(:validate_provider_config) { nil } unless options.validate_provider_config if channel_whatsapp.provider == 'whatsapp_cloud' - channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'phone_number_id' => '123456789', - 'business_account_id' => '123456789' }) + # Add 'source' => 'embedded_signup' to skip after_commit :setup_webhooks callback in tests + # The callback is for manual setup flow; embedded signup handles webhook setup explicitly + # Only set source if not already provided (allows tests to override) + default_config = { + 'api_key' => 'test_key', + 'phone_number_id' => '123456789', + 'business_account_id' => '123456789' + } + default_config['source'] = 'embedded_signup' unless channel_whatsapp.provider_config.key?('source') + channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge(default_config) end end diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index b46c984d1..dcc010d88 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -47,16 +47,39 @@ RSpec.describe Channel::Whatsapp do end describe 'webhook_verify_token' do + before do + # Stub webhook setup to prevent HTTP calls during channel creation + setup_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(setup_service) + allow(setup_service).to receive(:perform) + end + it 'generates webhook_verify_token if not present' do - channel = create(:channel_whatsapp, provider_config: { webhook_verify_token: nil }, provider: 'whatsapp_cloud', account: create(:account), - validate_provider_config: false, sync_templates: false) + channel = create(:channel_whatsapp, + provider_config: { + 'webhook_verify_token' => nil, + 'api_key' => 'test_key', + 'business_account_id' => '123456789' + }, + provider: 'whatsapp_cloud', + account: create(:account), + validate_provider_config: false, + sync_templates: false) expect(channel.provider_config['webhook_verify_token']).not_to be_nil end it 'does not generate webhook_verify_token if present' do - channel = create(:channel_whatsapp, provider: 'whatsapp_cloud', provider_config: { webhook_verify_token: '123' }, account: create(:account), - validate_provider_config: false, sync_templates: false) + channel = create(:channel_whatsapp, + provider: 'whatsapp_cloud', + provider_config: { + 'webhook_verify_token' => '123', + 'api_key' => 'test_key', + 'business_account_id' => '123456789' + }, + account: create(:account), + validate_provider_config: false, + sync_templates: false) expect(channel.provider_config['webhook_verify_token']).to eq '123' end @@ -91,15 +114,18 @@ RSpec.describe Channel::Whatsapp do end context 'when channel is created through manual setup' do - it 'does not setup webhooks' do - expect(Whatsapp::WebhookSetupService).not_to receive(:new) + it 'setups webhooks via after_commit callback' do + expect(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + expect(webhook_service).to receive(:perform) + # Explicitly set source to nil to test manual setup behavior (not embedded_signup) create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', provider_config: { 'business_account_id' => 'test_waba_id', - 'api_key' => 'test_access_token' + 'api_key' => 'test_access_token', + 'source' => nil }, validate_provider_config: false, sync_templates: false) @@ -157,12 +183,17 @@ RSpec.describe Channel::Whatsapp do end context 'when channel is not embedded_signup' do - it 'does not call WebhookTeardownService on destroy' do + it 'calls WebhookTeardownService on destroy' do + # Mock the setup service to prevent HTTP calls during creation + setup_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(setup_service) + allow(setup_service).to receive(:perform) + channel = create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', provider_config: { - 'source' => 'manual', + 'business_account_id' => 'test_waba_id', 'api_key' => 'test_access_token' }, validate_provider_config: false, diff --git a/spec/services/whatsapp/webhook_setup_service_spec.rb b/spec/services/whatsapp/webhook_setup_service_spec.rb index 38856e252..d35d14cb9 100644 --- a/spec/services/whatsapp/webhook_setup_service_spec.rb +++ b/spec/services/whatsapp/webhook_setup_service_spec.rb @@ -6,7 +6,8 @@ describe Whatsapp::WebhookSetupService do phone_number: '+1234567890', provider_config: { 'phone_number_id' => '123456789', - 'webhook_verify_token' => 'test_verify_token' + 'webhook_verify_token' => 'test_verify_token', + 'source' => 'embedded_signup' }, provider: 'whatsapp_cloud', sync_templates: false, @@ -261,7 +262,8 @@ describe Whatsapp::WebhookSetupService do 'phone_number_id' => '123456789', 'webhook_verify_token' => 'existing_verify_token', 'business_id' => 'existing_business_id', - 'waba_id' => 'existing_waba_id' + 'waba_id' => 'existing_waba_id', + 'source' => 'embedded_signup' }, provider: 'whatsapp_cloud', sync_templates: false,