fix(whatsapp): run provisioning health check after async webhook setup and recover from enqueue failures
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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) }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user