feat: Add WhatsApp health monitoring and self-service registration completion (#12556)
Fixes https://linear.app/chatwoot/issue/CW-5692/whatsapp-es-numbers-stuck-in-pending-due-to-premature-registration ### Problem Multiple customers reported that their WhatsApp numbers remain stuck in **Pending** in WhatsApp Manager even after successful onboarding. - Our system triggers a **registration call** (`/<PHONE_NUMBER_ID>/register`) as soon as the number is OTP verified. - In many cases, Meta hasn’t finished **display name review/provisioning**, so the call fails with: ``` code: 100, error_subcode: 2388001 error_user_title: "Cannot Create Certificate" error_user_msg: "Your display name could not be processed. Please edit your display name and try again." ``` - This leaves the number stuck in Pending, no messaging can start until we manually retry registration. - Some customers have reported being stuck in this state for **7+ days**. ### Root cause - We only check `code_verification_status = VERIFIED` before attempting registration. - We **don’t wait** for display name provisioning (`name_status` / `platform_type`) to be complete. - As a result, registration fails prematurely and the number never transitions out of Pending. ### Solution #### 1. Health Status Monitoring - Build a backend service to fetch **real-time health data** from Graph API: - `code_verification_status` - `name_status` / `display_name_status` - `platform_type` - `throughput.level` - `messaging_limit_tier` - `quality_rating` - Expose health data via API (`/api/v1/accounts/:account_id/inboxes/:id/health`). - Display this in the UI as an **Account Health tab** with clear badges and direct links to WhatsApp Manager. #### 2. Smarter Registration Logic - Update `WebhookSetupService` to include a **dual-condition check**: - Register if: 1. Phone is **not verified**, OR 2. Phone is **verified but provisioning incomplete** (`platform_type = NOT_APPLICABLE`, `throughput.level = NOT_APPLICABLE`). - Skip registration if number is already provisioned. - Retry registration automatically when stuck. - Provide a UI banner with complete registration button so customers can retry without manual support. ### Screenshot <img width="2292" height="1344" alt="CleanShot 2025-09-30 at 16 01 03@2x" src="https://github.com/user-attachments/assets/1c417d2a-b11c-475e-b092-3c5671ee59a7" /> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user