mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	 66cfef9298
			
		
	
	66cfef9298
	
	
	
		
			
			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>
		
			
				
	
	
		
			216 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			216 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
 | |
|   include Api::V1::InboxesHelper
 | |
|   before_action :fetch_inbox, except: [:index, :create]
 | |
|   before_action :fetch_agent_bot, only: [:set_agent_bot]
 | |
|   before_action :validate_limit, only: [:create]
 | |
|   # we are already handling the authorization in fetch inbox
 | |
|   before_action :check_authorization, except: [:show, :health]
 | |
|   before_action :validate_whatsapp_cloud_channel, only: [:health]
 | |
| 
 | |
|   def index
 | |
|     @inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
 | |
|   end
 | |
| 
 | |
|   def show; end
 | |
| 
 | |
|   # Deprecated: This API will be removed in 2.7.0
 | |
|   def assignable_agents
 | |
|     @assignable_agents = @inbox.assignable_agents
 | |
|   end
 | |
| 
 | |
|   def campaigns
 | |
|     @campaigns = @inbox.campaigns
 | |
|   end
 | |
| 
 | |
|   def avatar
 | |
|     @inbox.avatar.attachment.destroy! if @inbox.avatar.attached?
 | |
|     head :ok
 | |
|   end
 | |
| 
 | |
|   def create
 | |
|     ActiveRecord::Base.transaction do
 | |
|       channel = create_channel
 | |
|       @inbox = Current.account.inboxes.build(
 | |
|         {
 | |
|           name: inbox_name(channel),
 | |
|           channel: channel
 | |
|         }.merge(
 | |
|           permitted_params.except(:channel)
 | |
|         )
 | |
|       )
 | |
|       @inbox.save!
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def update
 | |
|     inbox_params = permitted_params.except(:channel, :csat_config)
 | |
|     inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) if permitted_params[:csat_config].present?
 | |
|     @inbox.update!(inbox_params)
 | |
|     update_inbox_working_hours
 | |
|     update_channel if channel_update_required?
 | |
|   end
 | |
| 
 | |
|   def agent_bot
 | |
|     @agent_bot = @inbox.agent_bot
 | |
|   end
 | |
| 
 | |
|   def set_agent_bot
 | |
|     if @agent_bot
 | |
|       agent_bot_inbox = @inbox.agent_bot_inbox || AgentBotInbox.new(inbox: @inbox)
 | |
|       agent_bot_inbox.agent_bot = @agent_bot
 | |
|       agent_bot_inbox.save!
 | |
|     elsif @inbox.agent_bot_inbox.present?
 | |
|       @inbox.agent_bot_inbox.destroy!
 | |
|     end
 | |
|     head :ok
 | |
|   end
 | |
| 
 | |
|   def destroy
 | |
|     ::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
 | |
|     render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
 | |
|   end
 | |
| 
 | |
|   def sync_templates
 | |
|     return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel?
 | |
| 
 | |
|     trigger_template_sync
 | |
|     render status: :ok, json: { message: 'Template sync initiated successfully' }
 | |
|   rescue StandardError => e
 | |
|     render status: :internal_server_error, json: { error: e.message }
 | |
|   end
 | |
| 
 | |
|   def health
 | |
|     health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
 | |
|     render json: health_data
 | |
|   rescue StandardError => e
 | |
|     Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
 | |
|     render json: { error: e.message }, status: :unprocessable_entity
 | |
|   end
 | |
| 
 | |
|   private
 | |
| 
 | |
|   def fetch_inbox
 | |
|     @inbox = Current.account.inboxes.find(params[:id])
 | |
|     authorize @inbox, :show?
 | |
|   end
 | |
| 
 | |
|   def fetch_agent_bot
 | |
|     @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
 | |
|   end
 | |
| 
 | |
|   def validate_whatsapp_cloud_channel
 | |
|     return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
 | |
| 
 | |
|     render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
 | |
|   end
 | |
| 
 | |
|   def create_channel
 | |
|     return unless allowed_channel_types.include?(permitted_params[:channel][:type])
 | |
| 
 | |
|     account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
 | |
|   end
 | |
| 
 | |
|   def allowed_channel_types
 | |
|     %w[web_widget api email line telegram whatsapp sms]
 | |
|   end
 | |
| 
 | |
|   def update_inbox_working_hours
 | |
|     @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
 | |
|   end
 | |
| 
 | |
|   def update_channel
 | |
|     channel_attributes = get_channel_attributes(@inbox.channel_type)
 | |
|     return if permitted_params(channel_attributes)[:channel].blank?
 | |
| 
 | |
|     validate_and_update_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
 | |
| 
 | |
|     reauthorize_and_update_channel(channel_attributes)
 | |
|     update_channel_feature_flags
 | |
|   end
 | |
| 
 | |
|   def channel_update_required?
 | |
|     permitted_params(get_channel_attributes(@inbox.channel_type))[:channel].present?
 | |
|   end
 | |
| 
 | |
|   def validate_and_update_email_channel(channel_attributes)
 | |
|     validate_email_channel(channel_attributes)
 | |
|   rescue StandardError => e
 | |
|     render json: { message: e }, status: :unprocessable_entity and return
 | |
|   end
 | |
| 
 | |
|   def reauthorize_and_update_channel(channel_attributes)
 | |
|     @inbox.channel.reauthorized! if @inbox.channel.respond_to?(:reauthorized!)
 | |
|     @inbox.channel.update!(permitted_params(channel_attributes)[:channel])
 | |
|   end
 | |
| 
 | |
|   def update_channel_feature_flags
 | |
|     return unless @inbox.web_widget?
 | |
|     return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
 | |
| 
 | |
|     @inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags]
 | |
|     @inbox.channel.save!
 | |
|   end
 | |
| 
 | |
|   def format_csat_config(config)
 | |
|     {
 | |
|       display_type: config['display_type'] || 'emoji',
 | |
|       message: config['message'] || '',
 | |
|       survey_rules: {
 | |
|         operator: config.dig('survey_rules', 'operator') || 'contains',
 | |
|         values: config.dig('survey_rules', 'values') || []
 | |
|       }
 | |
|     }
 | |
|   end
 | |
| 
 | |
|   def inbox_attributes
 | |
|     [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
 | |
|      :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
 | |
|      :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name,
 | |
|      { csat_config: [:display_type, :message, { survey_rules: [:operator, { values: [] }] }] }]
 | |
|   end
 | |
| 
 | |
|   def permitted_params(channel_attributes = [])
 | |
|     # We will remove this line after fixing https://linear.app/chatwoot/issue/CW-1567/null-value-passed-as-null-string-to-backend
 | |
|     params.each { |k, v| params[k] = params[k] == 'null' ? nil : v }
 | |
| 
 | |
|     params.permit(
 | |
|       *inbox_attributes,
 | |
|       channel: [:type, *channel_attributes]
 | |
|     )
 | |
|   end
 | |
| 
 | |
|   def channel_type_from_params
 | |
|     {
 | |
|       'web_widget' => Channel::WebWidget,
 | |
|       'api' => Channel::Api,
 | |
|       'email' => Channel::Email,
 | |
|       'line' => Channel::Line,
 | |
|       'telegram' => Channel::Telegram,
 | |
|       'whatsapp' => Channel::Whatsapp,
 | |
|       'sms' => Channel::Sms
 | |
|     }[permitted_params[:channel][:type]]
 | |
|   end
 | |
| 
 | |
|   def get_channel_attributes(channel_type)
 | |
|     if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
 | |
|       channel_type.constantize::EDITABLE_ATTRS.presence
 | |
|     else
 | |
|       []
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def whatsapp_channel?
 | |
|     @inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?)
 | |
|   end
 | |
| 
 | |
|   def trigger_template_sync
 | |
|     if @inbox.whatsapp?
 | |
|       Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
 | |
|     elsif @inbox.twilio? && @inbox.channel.whatsapp?
 | |
|       Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel)
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
 |