diff --git a/app/javascript/v3/views/login/Index.vue b/app/javascript/v3/views/login/Index.vue
index 5faaa3e02..028af4f72 100644
--- a/app/javascript/v3/views/login/Index.vue
+++ b/app/javascript/v3/views/login/Index.vue
@@ -15,6 +15,7 @@ import SimpleDivider from '../../components/Divider/SimpleDivider.vue';
import FormInput from '../../components/Form/Input.vue';
import GoogleOAuthButton from '../../components/GoogleOauth/Button.vue';
import Spinner from 'shared/components/Spinner.vue';
+import Icon from 'dashboard/components-next/icon/Icon.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import MfaVerification from 'dashboard/components/auth/MfaVerification.vue';
@@ -33,6 +34,7 @@ export default {
NextButton,
SimpleDivider,
MfaVerification,
+ Icon,
},
props: {
ssoAuthToken: { type: String, default: '' },
@@ -260,9 +262,12 @@ export default {
-
+
{{ $t('LOGIN.SAML.LABEL') }}
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/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder
index 35b723807..014884cf1 100644
--- a/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder
+++ b/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder
@@ -3,7 +3,7 @@ json.payload do
json.applied_sla applied_sla.push_event_data
json.conversation do
conversation = applied_sla.conversation
- json.id conversation.id
+ json.id conversation.display_id
json.contact do
json.name conversation.contact.name if conversation.contact
end
diff --git a/package.json b/package.json
index 66dd84aa2..38053b2c3 100644
--- a/package.json
+++ b/package.json
@@ -110,7 +110,7 @@
"@egoist/tailwindcss-icons": "^1.8.1",
"@histoire/plugin-vue": "0.17.15",
"@iconify-json/logos": "^1.2.3",
- "@iconify-json/lucide": "^1.2.11",
+ "@iconify-json/lucide": "^1.2.68",
"@iconify-json/ph": "^1.2.1",
"@iconify-json/ri": "^1.2.3",
"@iconify-json/teenyicons": "^1.2.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 71279b7e5..f4ae568c5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -246,8 +246,8 @@ importers:
specifier: ^1.2.3
version: 1.2.3
'@iconify-json/lucide':
- specifier: ^1.2.11
- version: 1.2.11
+ specifier: ^1.2.68
+ version: 1.2.68
'@iconify-json/ph':
specifier: ^1.2.1
version: 1.2.1
@@ -884,8 +884,8 @@ packages:
'@iconify-json/logos@1.2.3':
resolution: {integrity: sha512-JLHS5hgZP1b55EONAWNeqBUuriRfRNKWXK4cqYx0PpVaJfIIMiiMxFfvoQiX/bkE9XgkLhcKmDUqL3LXPdXPwQ==}
- '@iconify-json/lucide@1.2.11':
- resolution: {integrity: sha512-dqpbV7+g1qqxtZOHCZKwdKhtYYqEUjFhYiOg/+PcADbjtapoL+bwa1Brn12gAHq5r2K7Mf29xRHOTmZ3UHHOrw==}
+ '@iconify-json/lucide@1.2.68':
+ resolution: {integrity: sha512-lR5xNJdn2CT0iR7lM25G4SewBO4G2hbr3fTWOc3AE9BspflEcneh02E3l9TBaCU/JOHozTJevWLrxBGypD7Tng==}
'@iconify-json/material-symbols@1.2.10':
resolution: {integrity: sha512-GcZxhOFStM7Dk/oZvJSaW0tR/k6NwTq+KDzYgCNBDg52ktZuRa/gkjRiYooJm/8PAe9NBYxIx8XjS/wi4sasdQ==}
@@ -5265,7 +5265,7 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
- '@iconify-json/lucide@1.2.11':
+ '@iconify-json/lucide@1.2.68':
dependencies:
'@iconify/types': 2.0.0
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/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
index 423f65711..299b8ac7a 100644
--- a/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
+++ b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
@@ -152,7 +152,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
body = JSON.parse(response.body)
expect(body['payload'].size).to eq(1)
expect(body['payload'].first).to include('applied_sla')
- expect(body['payload'].first['conversation']['id']).to eq(conversation2.id)
+ expect(body['payload'].first['conversation']['id']).to eq(conversation2.display_id)
expect(body['meta']).to include('count' => 1)
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