diff --git a/app/jobs/channels/whatsapp/webhook_setup_job.rb b/app/jobs/channels/whatsapp/webhook_setup_job.rb index 481b27103..f605aa989 100644 --- a/app/jobs/channels/whatsapp/webhook_setup_job.rb +++ b/app/jobs/channels/whatsapp/webhook_setup_job.rb @@ -5,7 +5,10 @@ class Channels::Whatsapp::WebhookSetupJob < ApplicationJob # thread. Inline, these Graph API calls can exceed the 15s Rack::Timeout and, since # RequestTimeoutException bypasses setup_webhooks' rescue, abort inbox creation and # roll it back — leaving the number connected on Meta but no inbox in Chatwoot. - def perform(whatsapp_channel) + def perform(whatsapp_channel, run_health_check: false) whatsapp_channel.setup_webhooks + # Health check runs only after registration so a freshly provisioned number + # isn't flagged as pending before setup_webhooks has a chance to register it. + whatsapp_channel.check_provisioning_health if run_health_check end end diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index 034206d6e..c4f1b5550 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -129,13 +129,40 @@ class Channel::Whatsapp < ApplicationRecord prompt_reauthorization! end - # Enqueue on the same channel record so GlobalID resolves it in the job. - def enqueue_webhook_setup - Channels::Whatsapp::WebhookSetupJob.perform_later(self) + # Enqueue on the same channel record so GlobalID resolves it in the job. If the queue + # is unavailable, fall back to prompting reauthorization so the inbox has a visible + # recovery path instead of silently committing without its webhook registered. + def enqueue_webhook_setup(run_health_check: false) + Channels::Whatsapp::WebhookSetupJob.perform_later(self, run_health_check: run_health_check) + rescue StandardError => e + Rails.logger.error "[WHATSAPP] Failed to enqueue webhook setup: #{e.message}" + prompt_reauthorization! + end + + # Runs after webhook registration (inside WebhookSetupJob) so it observes the + # post-registration provisioning state; prompts reauthorization if Meta still reports + # the number as not provisioned. Only used for new embedded-signup channels — running + # it before registration would spuriously flag freshly created numbers as pending. + def check_provisioning_health + health_data = Whatsapp::HealthService.new(self).fetch_health_status + return unless health_data + + if provisioning_pending?(health_data) + prompt_reauthorization! + else + Rails.logger.info "[WHATSAPP] Channel #{phone_number} health check passed" + end + rescue StandardError => e + Rails.logger.error "[WHATSAPP] Health check failed for channel #{phone_number}: #{e.message}" end private + def provisioning_pending?(health_data) + health_data[:platform_type] == 'NOT_APPLICABLE' || + health_data.dig(:throughput, 'level') == 'NOT_APPLICABLE' + end + def ensure_webhook_verify_token provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider == 'whatsapp_cloud' end diff --git a/app/services/whatsapp/embedded_signup_service.rb b/app/services/whatsapp/embedded_signup_service.rb index 3acbb75b5..2f83682c2 100644 --- a/app/services/whatsapp/embedded_signup_service.rb +++ b/app/services/whatsapp/embedded_signup_service.rb @@ -18,13 +18,11 @@ class Whatsapp::EmbeddedSignupService # Enqueue webhook setup explicitly instead of relying on the after_commit callback because: # 1. Reauthorization flow updates an existing channel (not a create), so after_commit on: :create won't trigger # 2. The channel is marked with source: 'embedded_signup' to skip the after_commit callback - # Run it in a job so Meta's slow phone-registration/subscription calls can't trip Rack::Timeout - # and roll back the just-created channel. - Channels::Whatsapp::WebhookSetupJob.perform_later(channel) - # Skip health check during reauthorization — phone numbers in pending provisioning state - # (platform_type: NOT_APPLICABLE) would incorrectly trigger a disconnect email right after - # a successful reauth. Only run health check for new channel creation. - check_channel_health_and_prompt_reauth(channel) if @inbox_id.blank? + # The job runs Meta's slow phone-registration/subscription calls off the request thread so they + # can't trip Rack::Timeout and roll back the just-created channel. The provisioning health check + # runs inside the job, after registration — and only for new channels, since a reauthorized + # number in a pending state would otherwise trigger a spurious disconnect right after reauth. + channel.enqueue_webhook_setup(run_health_check: @inbox_id.blank?) channel rescue StandardError => e @@ -56,24 +54,6 @@ class Whatsapp::EmbeddedSignupService end end - def check_channel_health_and_prompt_reauth(channel) - health_data = Whatsapp::HealthService.new(channel).fetch_health_status - return unless health_data - - if channel_in_pending_state?(health_data) - channel.prompt_reauthorization! - else - Rails.logger.info "[WHATSAPP] Channel #{channel.phone_number} health check passed" - end - rescue StandardError => e - Rails.logger.error "[WHATSAPP] Health check failed for channel #{channel.phone_number}: #{e.message}" - end - - def channel_in_pending_state?(health_data) - health_data[:platform_type] == 'NOT_APPLICABLE' || - health_data.dig(:throughput, 'level') == 'NOT_APPLICABLE' - end - def validate_parameters! missing_params = [] missing_params << 'code' if @code.blank? diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index 2bc344568..a9d7341d0 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -158,6 +158,60 @@ RSpec.describe Channel::Whatsapp do end end + describe '#enqueue_webhook_setup' do + let(:channel) do + create(:channel_whatsapp, account: create(:account), + validate_provider_config: false, sync_templates: false) + end + + it 'enqueues the setup job with the health-check flag' do + expect { channel.enqueue_webhook_setup(run_health_check: true) } + .to have_enqueued_job(Channels::Whatsapp::WebhookSetupJob).with(channel, run_health_check: true) + end + + it 'prompts reauthorization when the queue is unavailable' do + allow(Channels::Whatsapp::WebhookSetupJob).to receive(:perform_later).and_raise(StandardError, 'redis down') + + expect(channel.reauthorization_required?).to be false + channel.enqueue_webhook_setup + expect(channel.reauthorization_required?).to be true + end + end + + describe '#check_provisioning_health' do + let(:channel) do + create(:channel_whatsapp, account: create(:account), + validate_provider_config: false, sync_templates: false) + end + let(:health_service) { instance_double(Whatsapp::HealthService) } + + before { allow(Whatsapp::HealthService).to receive(:new).with(channel).and_return(health_service) } + + it 'prompts reauthorization when platform_type is NOT_APPLICABLE' do + allow(health_service).to receive(:fetch_health_status) + .and_return(platform_type: 'NOT_APPLICABLE', throughput: { 'level' => 'STANDARD' }) + + channel.check_provisioning_health + expect(channel.reauthorization_required?).to be true + end + + it 'prompts reauthorization when throughput level is NOT_APPLICABLE' do + allow(health_service).to receive(:fetch_health_status) + .and_return(platform_type: 'CLOUD_API', throughput: { 'level' => 'NOT_APPLICABLE' }) + + channel.check_provisioning_health + expect(channel.reauthorization_required?).to be true + end + + it 'does not prompt reauthorization for a healthy number' do + allow(health_service).to receive(:fetch_health_status) + .and_return(platform_type: 'CLOUD_API', throughput: { 'level' => 'STANDARD' }) + + channel.check_provisioning_health + expect(channel.reauthorization_required?).to be false + end + end + describe '#teardown_webhooks' do let(:account) { create(:account) } diff --git a/spec/services/whatsapp/embedded_signup_service_spec.rb b/spec/services/whatsapp/embedded_signup_service_spec.rb index 9a7fc4c7a..e106b894f 100644 --- a/spec/services/whatsapp/embedded_signup_service_spec.rb +++ b/spec/services/whatsapp/embedded_signup_service_spec.rb @@ -44,61 +44,11 @@ describe Whatsapp::EmbeddedSignupService do .with(account, { waba_id: params[:waba_id], business_name: 'Test Business' }, phone_info, access_token) .and_return(channel_creation) allow(channel_creation).to receive(:perform).and_return(channel) - - health_service = instance_double(Whatsapp::HealthService) - allow(Whatsapp::HealthService).to receive(:new).and_return(health_service) - allow(health_service).to receive(:fetch_health_status).and_return({ - platform_type: 'CLOUD_API', - throughput: { 'level' => 'STANDARD' }, - messaging_limit_tier: 'TIER_1000' - }) end - it 'creates channel and enqueues webhook setup' do - expect { service.perform }.to have_enqueued_job(Channels::Whatsapp::WebhookSetupJob).with(channel) - end - - it 'checks health status after channel creation' do - health_service = instance_double(Whatsapp::HealthService) - allow(Whatsapp::HealthService).to receive(:new).and_return(health_service) - expect(health_service).to receive(:fetch_health_status) - - service.perform - end - - context 'when channel is in pending state' do - it 'prompts reauthorization for pending channel' do - health_service = instance_double(Whatsapp::HealthService) - allow(Whatsapp::HealthService).to receive(:new).and_return(health_service) - allow(health_service).to receive(:fetch_health_status).and_return({ - platform_type: 'NOT_APPLICABLE', - throughput: { 'level' => 'STANDARD' }, - messaging_limit_tier: 'TIER_1000' - }) - - expect(channel).to receive(:prompt_reauthorization!) - service.perform - end - - it 'prompts reauthorization when throughput level is NOT_APPLICABLE' do - health_service = instance_double(Whatsapp::HealthService) - allow(Whatsapp::HealthService).to receive(:new).and_return(health_service) - allow(health_service).to receive(:fetch_health_status).and_return({ - platform_type: 'CLOUD_API', - throughput: { 'level' => 'NOT_APPLICABLE' }, - messaging_limit_tier: 'TIER_1000' - }) - - expect(channel).to receive(:prompt_reauthorization!) - service.perform - end - end - - context 'when channel is healthy' do - it 'does not prompt reauthorization for healthy channel' do - expect(channel).not_to receive(:prompt_reauthorization!) - service.perform - end + it 'creates the channel and enqueues webhook setup with the health check' do + expect { service.perform } + .to have_enqueued_job(Channels::Whatsapp::WebhookSetupJob).with(channel, run_health_check: true) end context 'when parameters are invalid' do @@ -156,10 +106,11 @@ describe Whatsapp::EmbeddedSignupService do }) end - it 'uses ReauthorizationService and enqueues webhook setup' do + it 'uses ReauthorizationService and enqueues webhook setup without the health check' do expect(reauth_service).to receive(:perform) - expect { service_with_inbox.perform }.to have_enqueued_job(Channels::Whatsapp::WebhookSetupJob).with(channel) + expect { service_with_inbox.perform } + .to have_enqueued_job(Channels::Whatsapp::WebhookSetupJob).with(channel, run_health_check: false) end context 'with real channel requiring reauthorization' do