+
@@ -856,6 +917,9 @@ export default {
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/Reauthorize.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/Reauthorize.vue
index 08ddcaded..229b62f69 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/Reauthorize.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/Reauthorize.vue
@@ -16,6 +16,10 @@ const props = defineProps({
type: Object,
required: true,
},
+ whatsappRegistrationIncomplete: {
+ type: Boolean,
+ default: false,
+ },
});
const { t } = useI18n();
@@ -28,6 +32,20 @@ const whatsappConfigurationId = computed(
() => window.chatwootConfig.whatsappConfigurationId
);
+const actionLabel = computed(() => {
+ if (props.whatsappRegistrationIncomplete) {
+ return t('INBOX_MGMT.COMPLETE_REGISTRATION');
+ }
+ return '';
+});
+
+const description = computed(() => {
+ if (props.whatsappRegistrationIncomplete) {
+ return t('INBOX_MGMT.WHATSAPP_REGISTRATION_INCOMPLETE');
+ }
+ return '';
+});
+
const reauthorizeWhatsApp = async params => {
isRequestingAuthorization.value = true;
@@ -185,6 +203,8 @@ defineExpose({
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue
new file mode 100644
index 000000000..026c8ef69
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+ {{ t('INBOX_MGMT.ACCOUNT_HEALTH.TITLE') }}
+
+
+ {{ t('INBOX_MGMT.ACCOUNT_HEALTH.DESCRIPTION') }}
+
+
+
+ {{ t('INBOX_MGMT.ACCOUNT_HEALTH.GO_TO_SETTINGS') }}
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+ {{ item.value }}
+
+
+ {{ formatStatusDisplay(item.value) }}
+
+
+ {{ formatModeDisplay(item.value) }}
+
+
+ {{ formatTierDisplay(item.value) }}
+
+ {{
+ item.value
+ }}
+
+
+
+
+
+
+
+
+
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.NO_DATA') }}
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/InboxReconnectionRequired.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/InboxReconnectionRequired.vue
index ac84065e9..109272973 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/InboxReconnectionRequired.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/InboxReconnectionRequired.vue
@@ -1,15 +1,26 @@
- {{ $t('INBOX_MGMT.RECONNECTION_REQUIRED') }}
+ {{ description || $t('INBOX_MGMT.RECONNECTION_REQUIRED') }}
diff --git a/app/policies/inbox_policy.rb b/app/policies/inbox_policy.rb
index 8a6f81484..cace85e5e 100644
--- a/app/policies/inbox_policy.rb
+++ b/app/policies/inbox_policy.rb
@@ -61,4 +61,8 @@ class InboxPolicy < ApplicationPolicy
def sync_templates?
@account_user.administrator?
end
+
+ def health?
+ @account_user.administrator?
+ end
end
diff --git a/app/services/whatsapp/embedded_signup_service.rb b/app/services/whatsapp/embedded_signup_service.rb
index 1b882b1f1..4379d0b74 100644
--- a/app/services/whatsapp/embedded_signup_service.rb
+++ b/app/services/whatsapp/embedded_signup_service.rb
@@ -17,6 +17,7 @@ class Whatsapp::EmbeddedSignupService
channel = create_or_reauthorize_channel(access_token, phone_info)
channel.setup_webhooks
+ check_channel_health_and_prompt_reauth(channel)
channel
rescue StandardError => e
@@ -52,6 +53,24 @@ 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/app/services/whatsapp/health_service.rb b/app/services/whatsapp/health_service.rb
new file mode 100644
index 000000000..94789ef79
--- /dev/null
+++ b/app/services/whatsapp/health_service.rb
@@ -0,0 +1,84 @@
+class Whatsapp::HealthService
+ BASE_URI = 'https://graph.facebook.com'.freeze
+
+ def initialize(channel)
+ @channel = channel
+ @access_token = channel.provider_config['api_key']
+ @api_version = GlobalConfigService.load('WHATSAPP_API_VERSION', 'v22.0')
+ end
+
+ def fetch_health_status
+ validate_channel!
+ fetch_phone_health_data
+ end
+
+ private
+
+ def validate_channel!
+ raise ArgumentError, 'Channel is required' if @channel.blank?
+ raise ArgumentError, 'API key is missing' if @access_token.blank?
+ raise ArgumentError, 'Phone number ID is missing' if @channel.provider_config['phone_number_id'].blank?
+ end
+
+ def fetch_phone_health_data
+ phone_number_id = @channel.provider_config['phone_number_id']
+
+ response = HTTParty.get(
+ "#{BASE_URI}/#{@api_version}/#{phone_number_id}",
+ query: {
+ fields: health_fields,
+ access_token: @access_token
+ }
+ )
+
+ handle_response(response)
+ rescue StandardError => e
+ Rails.logger.error "[WHATSAPP HEALTH] Error fetching health data: #{e.message}"
+ raise e
+ end
+
+ def health_fields
+ %w[
+ quality_rating
+ messaging_limit_tier
+ code_verification_status
+ account_mode
+ id
+ display_phone_number
+ name_status
+ verified_name
+ webhook_configuration
+ throughput
+ last_onboarded_time
+ platform_type
+ certificate
+ ].join(',')
+ end
+
+ def handle_response(response)
+ unless response.success?
+ error_message = "WhatsApp API request failed: #{response.code} - #{response.body}"
+ Rails.logger.error "[WHATSAPP HEALTH] #{error_message}"
+ raise error_message
+ end
+
+ data = response.parsed_response
+ format_health_response(data)
+ end
+
+ def format_health_response(response)
+ {
+ display_phone_number: response['display_phone_number'],
+ verified_name: response['verified_name'],
+ name_status: response['name_status'],
+ quality_rating: response['quality_rating'],
+ messaging_limit_tier: response['messaging_limit_tier'],
+ account_mode: response['account_mode'],
+ code_verification_status: response['code_verification_status'],
+ throughput: response['throughput'],
+ last_onboarded_time: response['last_onboarded_time'],
+ platform_type: response['platform_type'],
+ business_id: @channel.provider_config['business_account_id']
+ }
+ end
+end
diff --git a/app/services/whatsapp/webhook_setup_service.rb b/app/services/whatsapp/webhook_setup_service.rb
index a3faaaa56..63fed1e93 100644
--- a/app/services/whatsapp/webhook_setup_service.rb
+++ b/app/services/whatsapp/webhook_setup_service.rb
@@ -8,8 +8,12 @@ class Whatsapp::WebhookSetupService
def perform
validate_parameters!
- # Since coexistence method does not need to register, we check it
- register_phone_number unless phone_number_verified?
+
+ # Register phone number if either condition is met:
+ # 1. Phone number is not verified (code_verification_status != 'VERIFIED')
+ # 2. Phone number needs registration (pending provisioning state)
+ register_phone_number if !phone_number_verified? || phone_number_needs_registration?
+
setup_webhook
end
@@ -69,9 +73,44 @@ class Whatsapp::WebhookSetupService
def phone_number_verified?
phone_number_id = @channel.provider_config['phone_number_id']
- @api_client.phone_number_verified?(phone_number_id)
+ # Check with WhatsApp API if the phone number code verification is complete
+ # This checks code_verification_status == 'VERIFIED'
+ verified = @api_client.phone_number_verified?(phone_number_id)
+ Rails.logger.info("[WHATSAPP] Phone number #{phone_number_id} code verification status: #{verified}")
+
+ verified
rescue StandardError => e
- Rails.logger.error("[WHATSAPP] Phone registration status check failed, but continuing: #{e.message}")
+ # If verification check fails, assume not verified to be safe
+ Rails.logger.error("[WHATSAPP] Phone verification status check failed: #{e.message}")
+ false
+ end
+
+ def phone_number_needs_registration?
+ # Check if phone is in pending provisioning state based on health data
+ # This is a separate check from phone_number_verified? which only checks code verification
+
+ phone_number_in_pending_state?
+
+ rescue StandardError => e
+ Rails.logger.error("[WHATSAPP] Phone registration check failed: #{e.message}")
+ # Conservative approach: don't register if we can't determine the state
+ false
+ end
+
+ def phone_number_in_pending_state?
+ health_service = Whatsapp::HealthService.new(@channel)
+ health_data = health_service.fetch_health_status
+
+ # Check if phone number is in "not provisioned" state based on health indicators
+ # These conditions indicate the number is pending and needs registration:
+ # - platform_type: "NOT_APPLICABLE" means not fully set up
+ # - throughput.level: "NOT_APPLICABLE" means no messaging capacity assigned
+ health_data[:platform_type] == 'NOT_APPLICABLE' ||
+ health_data.dig(:throughput, :level) == 'NOT_APPLICABLE'
+
+ rescue StandardError => e
+ Rails.logger.error("[WHATSAPP] Health status check failed: #{e.message}")
+ # If health check fails, assume registration is not needed to avoid errors
false
end
end
diff --git a/config/routes.rb b/config/routes.rb
index 6a484b380..bf455949c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -193,6 +193,7 @@ Rails.application.routes.draw do
post :set_agent_bot, on: :member
delete :avatar, on: :member
post :sync_templates, on: :member
+ get :health, on: :member
end
resources :inbox_members, only: [:create, :show], param: :inbox_id do
collection do
diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
index cc235cada..a44f60391 100644
--- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
@@ -980,4 +980,153 @@ RSpec.describe 'Inboxes API', type: :request do
end
end
end
+
+ describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/health' do
+ let(:whatsapp_channel) do
+ create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false)
+ end
+ let(:whatsapp_inbox) { create(:inbox, account: account, channel: whatsapp_channel) }
+ let(:non_whatsapp_inbox) { create(:inbox, account: account) }
+ let(:health_service) { instance_double(Whatsapp::HealthService) }
+ let(:health_data) do
+ {
+ display_phone_number: '+1234567890',
+ verified_name: 'Test Business',
+ name_status: 'APPROVED',
+ quality_rating: 'GREEN',
+ messaging_limit_tier: 'TIER_1000',
+ account_mode: 'LIVE',
+ business_id: 'business123'
+ }
+ end
+
+ before do
+ allow(Whatsapp::HealthService).to receive(:new).and_return(health_service)
+ allow(health_service).to receive(:fetch_health_status).and_return(health_data)
+ end
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/health"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ context 'with WhatsApp inbox' do
+ it 'returns health data for administrator' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/health",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = response.parsed_body
+ expect(json_response).to include(
+ 'display_phone_number' => '+1234567890',
+ 'verified_name' => 'Test Business',
+ 'name_status' => 'APPROVED',
+ 'quality_rating' => 'GREEN',
+ 'messaging_limit_tier' => 'TIER_1000',
+ 'account_mode' => 'LIVE',
+ 'business_id' => 'business123'
+ )
+ end
+
+ it 'returns health data for agent with inbox access' do
+ create(:inbox_member, user: agent, inbox: whatsapp_inbox)
+
+ get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/health",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = response.parsed_body
+ expect(json_response['display_phone_number']).to eq('+1234567890')
+ end
+
+ it 'returns unauthorized for agent without inbox access' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/health",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+
+ it 'calls the health service with correct channel' do
+ expect(Whatsapp::HealthService).to receive(:new).with(whatsapp_channel).and_return(health_service)
+ expect(health_service).to receive(:fetch_health_status)
+
+ get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/health",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'handles service errors gracefully' do
+ allow(health_service).to receive(:fetch_health_status).and_raise(StandardError, 'API Error')
+
+ get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/health",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ json_response = response.parsed_body
+ expect(json_response['error']).to include('API Error')
+ end
+ end
+
+ context 'with non-WhatsApp inbox' do
+ it 'returns bad request error for administrator' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{non_whatsapp_inbox.id}/health",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:bad_request)
+ json_response = response.parsed_body
+ expect(json_response['error']).to eq('Health data only available for WhatsApp Cloud API channels')
+ end
+
+ it 'returns bad request error for agent' do
+ create(:inbox_member, user: agent, inbox: non_whatsapp_inbox)
+
+ get "/api/v1/accounts/#{account.id}/inboxes/#{non_whatsapp_inbox.id}/health",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:bad_request)
+ json_response = response.parsed_body
+ expect(json_response['error']).to eq('Health data only available for WhatsApp Cloud API channels')
+ end
+ end
+
+ context 'with WhatsApp non-cloud inbox' do
+ let(:whatsapp_default_channel) do
+ create(:channel_whatsapp, account: account, provider: 'default', sync_templates: false, validate_provider_config: false)
+ end
+ let(:whatsapp_default_inbox) { create(:inbox, account: account, channel: whatsapp_default_channel) }
+
+ it 'returns bad request error for non-cloud provider' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_default_inbox.id}/health",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:bad_request)
+ json_response = response.parsed_body
+ expect(json_response['error']).to eq('Health data only available for WhatsApp Cloud API channels')
+ end
+ end
+
+ context 'with non-existent inbox' do
+ it 'returns not found error' do
+ get "/api/v1/accounts/#{account.id}/inboxes/999999/health",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/whatsapp/embedded_signup_service_spec.rb b/spec/services/whatsapp/embedded_signup_service_spec.rb
index 1db94928e..a20e36ac8 100644
--- a/spec/services/whatsapp/embedded_signup_service_spec.rb
+++ b/spec/services/whatsapp/embedded_signup_service_spec.rb
@@ -48,6 +48,15 @@ describe Whatsapp::EmbeddedSignupService do
allow(channel_creation).to receive(:perform).and_return(channel)
allow(channel).to receive(:setup_webhooks)
+ allow(channel).to receive(:phone_number).and_return('+1234567890')
+
+ 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 sets up webhooks' do
@@ -57,6 +66,49 @@ describe Whatsapp::EmbeddedSignupService do
expect(result).to eq(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
+ end
+
context 'when parameters are invalid' do
it 'raises ArgumentError for missing parameters' do
invalid_service = described_class.new(account: account, params: { code: '', business_id: '', waba_id: '' })
@@ -114,6 +166,16 @@ describe Whatsapp::EmbeddedSignupService do
business_id: params[:business_id]
).and_return(reauth_service)
allow(reauth_service).to receive(:perform).with(access_token, phone_info).and_return(channel)
+
+ allow(channel).to receive(:phone_number).and_return('+1234567890')
+
+ 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 'uses ReauthorizationService and sets up webhooks' do
@@ -124,36 +186,57 @@ describe Whatsapp::EmbeddedSignupService do
expect(result).to eq(channel)
end
- it 'clears reauthorization flag' do
- inbox = create(:inbox, account: account)
- whatsapp_channel = create(:channel_whatsapp, account: account, phone_number: '+1234567890',
- validate_provider_config: false, sync_templates: false)
- inbox.update!(channel: whatsapp_channel)
- whatsapp_channel.prompt_reauthorization!
+ context 'with real channel requiring reauthorization' do
+ let(:inbox) { create(:inbox, account: account) }
+ let(:whatsapp_channel) do
+ create(:channel_whatsapp, account: account, phone_number: '+1234567890',
+ validate_provider_config: false, sync_templates: false)
+ end
+ let(:service_with_real_inbox) { described_class.new(account: account, params: params, inbox_id: inbox.id) }
- service_with_real_inbox = described_class.new(account: account, params: params, inbox_id: inbox.id)
+ before do
+ inbox.update!(channel: whatsapp_channel)
+ whatsapp_channel.prompt_reauthorization!
- # Mock the ReauthorizationService to return our test channel
- reauth_service = instance_double(Whatsapp::ReauthorizationService)
- allow(Whatsapp::ReauthorizationService).to receive(:new).with(
- account: account,
- inbox_id: inbox.id,
- phone_number_id: params[:phone_number_id],
- business_id: params[:business_id]
- ).and_return(reauth_service)
-
- # Perform the reauthorization and clear the flag
- allow(reauth_service).to receive(:perform) do
- whatsapp_channel.reauthorized!
- whatsapp_channel
+ setup_reauthorization_mocks
+ setup_health_service_mock
end
- allow(whatsapp_channel).to receive(:setup_webhooks).and_return(true)
+ it 'clears reauthorization flag when reauthorization completes' do
+ expect(whatsapp_channel.reauthorization_required?).to be true
+ result = service_with_real_inbox.perform
+ expect(result).to eq(whatsapp_channel)
+ expect(whatsapp_channel.reauthorization_required?).to be false
+ end
- expect(whatsapp_channel.reauthorization_required?).to be true
- result = service_with_real_inbox.perform
- expect(result).to eq(whatsapp_channel)
- expect(whatsapp_channel.reauthorization_required?).to be false
+ private
+
+ def setup_reauthorization_mocks
+ reauth_service = instance_double(Whatsapp::ReauthorizationService)
+ allow(Whatsapp::ReauthorizationService).to receive(:new).with(
+ account: account,
+ inbox_id: inbox.id,
+ phone_number_id: params[:phone_number_id],
+ business_id: params[:business_id]
+ ).and_return(reauth_service)
+
+ allow(reauth_service).to receive(:perform) do
+ whatsapp_channel.reauthorized!
+ whatsapp_channel
+ end
+
+ allow(whatsapp_channel).to receive(:setup_webhooks).and_return(true)
+ end
+
+ def setup_health_service_mock
+ 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
end
end
end
diff --git a/spec/services/whatsapp/webhook_setup_service_spec.rb b/spec/services/whatsapp/webhook_setup_service_spec.rb
index e6a246e5d..38856e252 100644
--- a/spec/services/whatsapp/webhook_setup_service_spec.rb
+++ b/spec/services/whatsapp/webhook_setup_service_spec.rb
@@ -16,6 +16,7 @@ describe Whatsapp::WebhookSetupService do
let(:access_token) { 'test_access_token' }
let(:service) { described_class.new(channel, waba_id, access_token) }
let(:api_client) { instance_double(Whatsapp::FacebookApiClient) }
+ let(:health_service) { instance_double(Whatsapp::HealthService) }
before do
# Stub webhook teardown to prevent HTTP calls during cleanup
@@ -24,8 +25,14 @@ describe Whatsapp::WebhookSetupService do
# Clean up any existing channels to avoid phone number conflicts
Channel::Whatsapp.destroy_all
allow(Whatsapp::FacebookApiClient).to receive(:new).and_return(api_client)
- # Default stub for phone_number_verified? with any argument
+ allow(Whatsapp::HealthService).to receive(:new).and_return(health_service)
+
+ # Default stubs for phone_number_verified? and health service
allow(api_client).to receive(:phone_number_verified?).and_return(false)
+ allow(health_service).to receive(:fetch_health_status).and_return({
+ platform_type: 'APPLICABLE',
+ throughput: { level: 'APPLICABLE' }
+ })
end
describe '#perform' do
@@ -49,9 +56,13 @@ describe Whatsapp::WebhookSetupService do
end
end
- context 'when phone number IS verified (should NOT register)' do
+ context 'when phone number IS verified AND fully provisioned (should NOT register)' 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_return({
+ platform_type: 'APPLICABLE',
+ throughput: { level: 'APPLICABLE' }
+ })
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
end
@@ -66,16 +77,68 @@ describe Whatsapp::WebhookSetupService do
end
end
+ context 'when phone number IS verified BUT needs registration (pending provisioning)' 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_return({
+ platform_type: 'NOT_APPLICABLE',
+ throughput: { level: 'APPLICABLE' }
+ })
+ 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').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')
+ service.perform
+ end
+ end
+ end
+
+ context 'when phone number needs registration due to throughput level' 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_return({
+ platform_type: 'APPLICABLE',
+ throughput: { level: 'NOT_APPLICABLE' }
+ })
+ 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').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')
+ service.perform
+ end
+ end
+ end
+
context 'when phone_number_verified? raises error' do
before do
allow(api_client).to receive(:phone_number_verified?).with('123456789').and_raise('API down')
+ allow(health_service).to receive(:fetch_health_status).and_return({
+ platform_type: 'APPLICABLE',
+ throughput: { level: 'APPLICABLE' }
+ })
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(channel).to receive(:save!)
end
- it 'tries to register phone and proceeds with webhook setup' do
+ 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)
@@ -84,6 +147,22 @@ describe Whatsapp::WebhookSetupService do
end
end
+ context 'when health service raises error' 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 })
+ 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 { service.perform }.not_to raise_error
+ end
+ end
+ end
+
context 'when phone registration fails (not blocking)' do
before do
allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
@@ -193,6 +272,10 @@ 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_return({
+ platform_type: 'APPLICABLE',
+ throughput: { level: 'APPLICABLE' }
+ })
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'existing_verify_token').and_return({ 'success' => true })
end
@@ -218,6 +301,10 @@ describe Whatsapp::WebhookSetupService do
context 'when webhook setup is successful in creation flow' 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_return({
+ platform_type: 'APPLICABLE',
+ throughput: { level: 'APPLICABLE' }
+ })
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
end