From d644c207f021cac7e692bac8a1c32492af350846 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 24 Jul 2026 12:00:37 +0400 Subject: [PATCH] feat: monitor whatsapp phone number health (#15100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds scheduled WhatsApp Cloud API phone-number health synchronization and expands the Account Health page with phone, capacity, business-account, webhook, coexistence, and recovery details. Authorization failures now guide administrators to the appropriate Configuration flow without exposing technical Meta error codes. Fixes [CW-7621](https://linear.app/chatwoot/issue/CW-7621/store-whatsapp-phone-number-health-status) **Preview** CleanShot 2026-07-22 at 10 55
01@2x CleanShot 2026-07-22 at 10 55
08@2x CleanShot 2026-07-22 at 11 10
05@2x ### How to test 1. Open **Settings โ†’ Inboxes โ†’ a WhatsApp Cloud API inbox โ†’ Account Health**. 2. Confirm the page shows separate Phone number, Health and capacity, Business account, and Webhook configuration sections. 3. Confirm the configured webhook URL can be copied and the expected URL appears only when it differs. 4. For a coexistence number, confirm **Coexistence ยท Active** appears; confirm it is hidden for standard Cloud API numbers. 5. With an invalid Embedded Signup token, confirm the page asks to refresh the WhatsApp connection and **Go to Configuration** opens the Configuration tab. 6. With an invalid manually configured token, confirm the page asks the administrator to verify or replace the access token. 7. Confirm authorization states do not display Meta error codes or the manual-migration recommendation. ### What changed - Persists the latest successful phone health snapshot, check time, and most recent error while retaining the last successful data after a failed refresh. - Refreshes stale active Cloud API channels every six hours through low-priority jobs. - Fetches phone, WABA, business portfolio, webhook, and coexistence details. - Uses Meta's current `whatsapp_business_manager_messaging_limit` field while preserving the existing UI response key. - Classifies authorization failures for setup-specific recovery guidance and logs new risky quality/status transitions. - Adds focused service, scheduler, job, trigger, and API coverage. Internal alerts, throttling, automatic inbox disablement, and customer notifications remain outside this PR and are tracked separately in CW-7622. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> --- .../concerns/whatsapp_health_management.rb | 13 +- .../dashboard/i18n/locale/en/inboxMgmt.json | 88 ++- .../dashboard/settings/inbox/Settings.vue | 21 +- .../inbox/components/AccountHealth.vue | 528 ++++++++++++++---- .../components/specs/AccountHealth.spec.js | 85 +++ app/jobs/channels/whatsapp/health_sync_job.rb | 9 + .../whatsapp/health_sync_scheduler_job.rb | 15 + .../trigger_hourly_scheduled_items_job.rb | 4 +- app/models/channel/whatsapp.rb | 6 +- app/services/whatsapp/health_service.rb | 200 +++++-- ...phone_number_health_to_channel_whatsapp.rb | 8 + db/schema.rb | 6 +- .../v1/accounts/inboxes_controller_spec.rb | 43 +- .../channels/whatsapp/health_sync_job_spec.rb | 48 ++ .../health_sync_scheduler_job_spec.rb | 85 +++ ...trigger_hourly_scheduled_items_job_spec.rb | 16 + spec/jobs/trigger_scheduled_items_job_spec.rb | 6 + spec/models/email_template_spec.rb | 4 +- spec/services/whatsapp/health_service_spec.rb | 262 +++++++++ 19 files changed, 1282 insertions(+), 165 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/components/specs/AccountHealth.spec.js create mode 100644 app/jobs/channels/whatsapp/health_sync_job.rb create mode 100644 app/jobs/channels/whatsapp/health_sync_scheduler_job.rb create mode 100644 db/migrate/20260718000000_add_phone_number_health_to_channel_whatsapp.rb create mode 100644 spec/jobs/channels/whatsapp/health_sync_job_spec.rb create mode 100644 spec/jobs/channels/whatsapp/health_sync_scheduler_job_spec.rb create mode 100644 spec/jobs/internal/trigger_hourly_scheduled_items_job_spec.rb create mode 100644 spec/services/whatsapp/health_service_spec.rb diff --git a/app/controllers/api/v1/accounts/concerns/whatsapp_health_management.rb b/app/controllers/api/v1/accounts/concerns/whatsapp_health_management.rb index 795d7f2a9..32e672779 100644 --- a/app/controllers/api/v1/accounts/concerns/whatsapp_health_management.rb +++ b/app/controllers/api/v1/accounts/concerns/whatsapp_health_management.rb @@ -17,8 +17,19 @@ module Api::V1::Accounts::Concerns::WhatsappHealthManagement end def health - health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status + health_data = Whatsapp::HealthService.new(@inbox.channel).sync_health_status! render json: health_data + rescue Whatsapp::HealthService::ApiError => e + Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}" + render json: { + error: { + type: e.authorization_error? ? 'authorization' : 'api', + message: e.message, + http_status: e.http_status, + code: e.code, + subcode: e.subcode + }.compact + }, status: :unprocessable_entity rescue StandardError => e Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}" render json: { error: e.message }, status: :unprocessable_entity diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index dfc371a56..dcc28f177 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -684,40 +684,98 @@ "WIDGET_FEATURES": "Widget features", "ACCOUNT_HEALTH": { "TITLE": "Manage your WhatsApp account", - "DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed", + "DESCRIPTION": "Review your WhatsApp phone number health, messaging capacity, and linked Meta business details.", "GO_TO_SETTINGS": "Go to Meta Business Manager", "NO_DATA": "Health data is not available", + "ERRORS": { + "AUTHORIZATION": { + "EMBEDDED_TITLE": "WhatsApp connection needs to be refreshed", + "EMBEDDED_DESCRIPTION": "Meta could not validate the connection for this inbox. Go to Configuration to reconfigure it using Embedded Signup.", + "MANUAL_TITLE": "WhatsApp access token needs attention", + "MANUAL_DESCRIPTION": "Meta could not validate the access token for this inbox. Go to Configuration to verify or replace it." + }, + "GENERIC_TITLE": "WhatsApp health data is unavailable", + "GENERIC_DESCRIPTION": "Meta could not return the current health details. Review the error below and try again later.", + "GO_TO_CONFIGURATION": "Go to Configuration" + }, + "SECTIONS": { + "PHONE_NUMBER": "Phone number", + "HEALTH_AND_CAPACITY": "Health and capacity", + "BUSINESS_ACCOUNT": "Business account" + }, "FIELDS": { "DISPLAY_PHONE_NUMBER": { "LABEL": "Display phone number", "TOOLTIP": "Phone number displayed to customers" }, "VERIFIED_NAME": { - "LABEL": "Business name", - "TOOLTIP": "Business name verified by WhatsApp" + "LABEL": "WhatsApp display name", + "TOOLTIP": "Display name associated with this phone number on WhatsApp" + }, + "PHONE_NUMBER_ID": { + "LABEL": "Phone number ID", + "TOOLTIP": "Meta identifier for this WhatsApp phone number" + }, + "COEXISTENCE": { + "LABEL": "Coexistence", + "TOOLTIP": "This number is connected to both the WhatsApp Business app and Cloud API" }, "DISPLAY_NAME_STATUS": { "LABEL": "Display name status", - "TOOLTIP": "Status of your business name verification" + "TOOLTIP": "Review status of the WhatsApp display name" + }, + "LAST_ONBOARDED_TIME": { + "LABEL": "Last onboarded", + "TOOLTIP": "Most recent time Meta onboarded this phone number" + }, + "PHONE_NUMBER_STATUS": { + "LABEL": "Phone number status", + "TOOLTIP": "Current connection status of this phone number on WhatsApp" }, "QUALITY_RATING": { "LABEL": "Quality rating", - "TOOLTIP": "WhatsApp quality rating for your account" + "TOOLTIP": "Quality rating Meta assigns to this phone number based on recent messaging signals" }, "MESSAGING_LIMIT_TIER": { "LABEL": "Messaging limit tier", - "TOOLTIP": "Daily messaging limit for your account" + "TOOLTIP": "Number of customers this phone number can initiate conversations with in 24 hours" }, "ACCOUNT_MODE": { "LABEL": "Account mode", - "TOOLTIP": "Current operating mode of your WhatsApp account" + "TOOLTIP": "Current operating mode of this WhatsApp phone number" + }, + "CODE_VERIFICATION_STATUS": { + "LABEL": "Code verification", + "TOOLTIP": "Verification-code status reported by Meta for this phone number" + }, + "THROUGHPUT_LEVEL": { + "LABEL": "Throughput", + "TOOLTIP": "Message throughput level assigned to this phone number" + }, + "BUSINESS_ACCOUNT_NAME": { + "LABEL": "WhatsApp Business Account name", + "TOOLTIP": "Name of the WhatsApp Business Account linked to this phone number" + }, + "BUSINESS_ACCOUNT_ID": { + "LABEL": "WhatsApp Business Account ID", + "TOOLTIP": "Meta identifier for the linked WhatsApp Business Account" + }, + "BUSINESS_PORTFOLIO_NAME": { + "LABEL": "Business portfolio name", + "TOOLTIP": "Name of the Meta business portfolio that owns the WhatsApp Business Account" + }, + "BUSINESS_PORTFOLIO_ID": { + "LABEL": "Business portfolio ID", + "TOOLTIP": "Meta identifier for the business portfolio that owns the WhatsApp Business Account" } }, "VALUES": { + "NOT_AVAILABLE": "Not available", "TIERS": { "TIER_250": "250 customers per 24h", "TIER_1000": "1K customers per 24h", "TIER_1K": "1K customers per 24h", + "TIER_2K": "2K customers per 24h", "TIER_10K": "10K customers per 24h", "TIER_100K": "100K customers per 24h", "TIER_UNLIMITED": "Unlimited customers per 24h", @@ -725,6 +783,16 @@ }, "STATUSES": { "APPROVED": "Approved", + "CONNECTED": "Connected", + "VERIFIED": "Verified", + "STANDARD": "Standard", + "ACTIVE": "Active", + "EXPIRED": "Expired", + "FLAGGED": "Flagged", + "RESTRICTED": "Restricted", + "BANNED": "Banned", + "DISCONNECTED": "Disconnected", + "UNKNOWN": "Not available", "PENDING_REVIEW": "Pending Review", "AVAILABLE_WITHOUT_REVIEW": "Available Without Review", "REJECTED": "Rejected", @@ -744,7 +812,11 @@ "REGISTER_SUCCESS": "Webhook registered successfully", "REGISTER_ERROR": "Failed to register webhook. Please try again.", "CONFIGURED_SUCCESS": "Webhook configured successfully", - "URL_MISMATCH": "Webhook URL mismatch" + "URL_MISMATCH": "Webhook URL mismatch", + "CONFIGURED_URL": "Configured URL", + "EXPECTED_URL": "Expected URL", + "COPY_URL": "Copy webhook URL", + "COPY_SUCCESS": "Webhook URL copied to clipboard" } }, "SETTINGS": "Settings", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 93a21c868..226e90735 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -409,6 +409,7 @@ export default { return ( this.isAWhatsAppCloudChannel && this.isEmbeddedSignupWhatsApp && + this.healthError?.type !== 'authorization' && this.isFeatureEnabledonAccount( this.accountId, FEATURE_FLAGS.WHATSAPP_MANUAL_TRANSFER @@ -578,11 +579,26 @@ export default { const response = await InboxHealthAPI.getHealthStatus(this.inbox.id); this.healthData = response.data; } catch (error) { - this.healthError = error.message || 'Failed to fetch health data'; + const apiError = error.response?.data?.error; + this.healthError = + typeof apiError === 'object' + ? apiError + : { + type: 'generic', + message: apiError || error.message, + }; } finally { this.isLoadingHealth = false; } }, + goToWhatsAppConfiguration() { + const configurationTabIndex = this.tabs.findIndex( + tab => tab.key === 'configuration' + ); + if (configurationTabIndex !== -1) { + this.onTabChange(configurationTabIndex); + } + }, async registerWebhook() { if (!this.inbox) return; @@ -1377,8 +1393,11 @@ export default {
import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; +import { copyTextToClipboard } from 'shared/helpers/clipboard'; import ButtonV4 from 'next/button/Button.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; +import { useAlert } from 'dashboard/composables'; const props = defineProps({ healthData: { type: Object, default: null, }, + healthError: { + type: Object, + default: null, + }, + isEmbeddedSignup: { + type: Boolean, + default: false, + }, isRegisteringWebhook: { type: Boolean, default: false, }, }); -const emit = defineEmits(['registerWebhook']); +const emit = defineEmits(['registerWebhook', 'goToConfiguration']); -const { t } = useI18n(); +const { t, te, locale } = useI18n(); const QUALITY_COLORS = { GREEN: 'text-n-teal-11', @@ -29,8 +39,17 @@ const QUALITY_COLORS = { const STATUS_COLORS = { APPROVED: 'text-n-teal-11', + CONNECTED: 'text-n-teal-11', + VERIFIED: 'text-n-teal-11', + STANDARD: 'text-n-teal-11', + ACTIVE: 'text-n-teal-11', PENDING_REVIEW: 'text-n-amber-11', AVAILABLE_WITHOUT_REVIEW: 'text-n-teal-11', + EXPIRED: 'text-n-amber-11', + FLAGGED: 'text-n-amber-11', + RESTRICTED: 'text-n-ruby-9', + BANNED: 'text-n-ruby-9', + DISCONNECTED: 'text-n-ruby-9', REJECTED: 'text-n-ruby-9', DECLINED: 'text-n-ruby-9', }; @@ -40,85 +59,274 @@ const MODE_COLORS = { SANDBOX: 'text-n-slate-11', }; -const healthItems = computed(() => { +const formatStatusDisplay = status => { + const translationKey = `INBOX_MGMT.ACCOUNT_HEALTH.VALUES.STATUSES.${status}`; + + if (te(translationKey)) { + return t(translationKey); + } + + return status + .toLowerCase() + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +}; + +const formatDateTime = value => { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + + return new Intl.DateTimeFormat(locale.value, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(date); +}; + +const healthSections = computed(() => { if (!props.healthData) { return []; } const { + id: phoneNumberId, display_phone_number: displayPhoneNumber, verified_name: verifiedName, name_status: nameStatus, quality_rating: qualityRating, messaging_limit_tier: messagingLimitTier, account_mode: accountMode, + status: phoneNumberStatus, + code_verification_status: codeVerificationStatus, + throughput_level: throughputLevel, + last_onboarded_time: lastOnboardedTime, + is_on_biz_app: isOnBizApp, + platform_type: platformType, + business_account_id: businessAccountId, + business_account_name: businessAccountName, + business_portfolio_id: businessPortfolioId, + business_portfolio_name: businessPortfolioName, } = props.healthData; + const notAvailable = t('INBOX_MGMT.ACCOUNT_HEALTH.VALUES.NOT_AVAILABLE'); + return [ { - key: 'displayPhoneNumber', - label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.LABEL'), - value: displayPhoneNumber || 'N/A', - tooltip: t( - 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.TOOLTIP' - ), - show: true, + key: 'phoneNumber', + title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.PHONE_NUMBER'), + items: [ + { + key: 'displayPhoneNumber', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.LABEL' + ), + value: displayPhoneNumber || notAvailable, + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.TOOLTIP' + ), + }, + { + key: 'verifiedName', + label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.LABEL'), + value: verifiedName || notAvailable, + tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.TOOLTIP'), + }, + { + key: 'phoneNumberId', + label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_ID.LABEL'), + value: phoneNumberId || notAvailable, + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_ID.TOOLTIP' + ), + }, + { + key: 'coexistence', + label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.COEXISTENCE.LABEL'), + value: 'ACTIVE', + tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.COEXISTENCE.TOOLTIP'), + type: 'status', + show: isOnBizApp === true && platformType === 'CLOUD_API', + }, + { + key: 'displayNameStatus', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.LABEL' + ), + value: nameStatus || 'UNKNOWN', + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.TOOLTIP' + ), + type: 'status', + }, + { + key: 'lastOnboardedTime', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.LAST_ONBOARDED_TIME.LABEL' + ), + value: lastOnboardedTime + ? formatDateTime(lastOnboardedTime) + : notAvailable, + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.LAST_ONBOARDED_TIME.TOOLTIP' + ), + show: Boolean(lastOnboardedTime), + }, + ], }, { - key: 'verifiedName', - label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.LABEL'), - value: verifiedName || 'N/A', - tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.TOOLTIP'), - show: true, + key: 'healthAndCapacity', + title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.HEALTH_AND_CAPACITY'), + items: [ + { + key: 'phoneNumberStatus', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_STATUS.LABEL' + ), + value: phoneNumberStatus || 'UNKNOWN', + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_STATUS.TOOLTIP' + ), + type: 'status', + }, + { + key: 'qualityRating', + label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.LABEL'), + value: qualityRating || 'UNKNOWN', + tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.TOOLTIP'), + type: 'quality', + }, + { + key: 'messagingLimitTier', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.LABEL' + ), + value: messagingLimitTier || 'UNKNOWN', + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.TOOLTIP' + ), + type: 'tier', + }, + { + key: 'accountMode', + label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.LABEL'), + value: accountMode || 'UNKNOWN', + tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.TOOLTIP'), + type: 'mode', + }, + { + key: 'codeVerificationStatus', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.CODE_VERIFICATION_STATUS.LABEL' + ), + value: codeVerificationStatus || 'UNKNOWN', + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.CODE_VERIFICATION_STATUS.TOOLTIP' + ), + type: 'status', + }, + { + key: 'throughputLevel', + label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.THROUGHPUT_LEVEL.LABEL'), + value: throughputLevel || 'UNKNOWN', + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.THROUGHPUT_LEVEL.TOOLTIP' + ), + type: 'status', + }, + ], }, { - key: 'displayNameStatus', - label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.LABEL'), - value: nameStatus || 'UNKNOWN', - tooltip: t( - 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.TOOLTIP' - ), - show: true, - type: 'status', + key: 'businessAccount', + title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.BUSINESS_ACCOUNT'), + items: [ + { + key: 'businessAccountName', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_NAME.LABEL' + ), + value: businessAccountName || notAvailable, + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_NAME.TOOLTIP' + ), + }, + { + key: 'businessAccountId', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_ID.LABEL' + ), + value: businessAccountId || notAvailable, + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_ID.TOOLTIP' + ), + }, + { + key: 'businessPortfolioName', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_NAME.LABEL' + ), + value: businessPortfolioName || notAvailable, + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_NAME.TOOLTIP' + ), + }, + { + key: 'businessPortfolioId', + label: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_ID.LABEL' + ), + value: businessPortfolioId || notAvailable, + tooltip: t( + 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_ID.TOOLTIP' + ), + }, + ], }, - { - key: 'qualityRating', - label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.LABEL'), - value: qualityRating || 'UNKNOWN', - tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.TOOLTIP'), - show: true, - type: 'quality', - }, - { - key: 'messagingLimitTier', - label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.LABEL'), - value: messagingLimitTier || 'UNKNOWN', - tooltip: t( - 'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.TOOLTIP' - ), - show: true, - type: 'tier', - }, - { - key: 'accountMode', - label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.LABEL'), - value: accountMode || 'UNKNOWN', - tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.TOOLTIP'), - show: true, - type: 'mode', - }, - ]; + ].map(section => ({ + ...section, + items: section.items.filter(item => item.show !== false), + })); +}); + +const errorState = computed(() => { + if (!props.healthError) return null; + + if (props.healthError.type === 'authorization') { + return { + title: props.isEmbeddedSignup + ? t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.EMBEDDED_TITLE') + : t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.MANUAL_TITLE'), + description: props.isEmbeddedSignup + ? t( + 'INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.EMBEDDED_DESCRIPTION' + ) + : t( + 'INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.MANUAL_DESCRIPTION' + ), + showConfigurationAction: true, + }; + } + + return { + title: t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.GENERIC_TITLE'), + description: t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.GENERIC_DESCRIPTION'), + showConfigurationAction: false, + }; }); const handleGoToSettings = () => { - const { business_id: businessId } = props.healthData || {}; + const { + business_account_id: businessAccountId, + business_portfolio_id: businessPortfolioId, + } = props.healthData || {}; - if (businessId) { - // WhatsApp Business Manager URL with specific business ID and phone numbers tab - const whatsappBusinessUrl = `https://business.facebook.com/latest/whatsapp_manager/phone_numbers/?business_id=${businessId}&tab=phone-numbers`; - window.open(whatsappBusinessUrl, '_blank'); + if (businessPortfolioId && businessAccountId) { + const whatsappBusinessUrl = new URL( + 'https://business.facebook.com/latest/whatsapp_manager/phone_numbers/' + ); + whatsappBusinessUrl.searchParams.set('business_id', businessPortfolioId); + whatsappBusinessUrl.searchParams.set('asset_id', businessAccountId); + window.open(whatsappBusinessUrl.toString(), '_blank'); } else { - // Fallback to general WhatsApp Business Manager if business_id is not available const fallbackUrl = 'https://business.facebook.com/'; window.open(fallbackUrl, '_blank'); } @@ -127,14 +335,17 @@ const handleGoToSettings = () => { const getQualityRatingTextColor = rating => QUALITY_COLORS[rating] || QUALITY_COLORS.UNKNOWN; -const formatTierDisplay = tier => - t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.TIERS.${tier}`) || tier; +const formatTierDisplay = tier => { + const translationKey = `INBOX_MGMT.ACCOUNT_HEALTH.VALUES.TIERS.${tier}`; -const formatStatusDisplay = status => - t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.STATUSES.${status}`) || status; + return te(translationKey) ? t(translationKey) : formatStatusDisplay(tier); +}; -const formatModeDisplay = mode => - t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.MODES.${mode}`) || mode; +const formatModeDisplay = mode => { + const translationKey = `INBOX_MGMT.ACCOUNT_HEALTH.VALUES.MODES.${mode}`; + + return te(translationKey) ? t(translationKey) : formatStatusDisplay(mode); +}; const getModeStatusTextColor = mode => MODE_COLORS[mode] || 'text-n-slate-12'; @@ -163,6 +374,15 @@ const webhookUrlMismatch = computed( const handleRegisterWebhook = () => { emit('registerWebhook'); }; + +const handleGoToConfiguration = () => { + emit('goToConfiguration'); +}; + +const handleCopyWebhookUrl = async url => { + await copyTextToClipboard(url); + useAlert(t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_SUCCESS')); +};