@@ -211,6 +239,55 @@ const getStatusTextColor = status => STATUS_COLORS[status] || 'text-n-slate-12';
}}
+
+
+
+
+
+ {{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.TITLE') }}
+
+
+
+
+
+
+ {{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.CONFIGURED_SUCCESS') }}
+
+
+
+ {{
+ webhookUrlMismatch
+ ? t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.URL_MISMATCH')
+ : t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.ACTION_REQUIRED')
+ }}
+
+
+ {{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.REGISTER_BUTTON') }}
+
+
+
diff --git a/app/services/whatsapp/health_service.rb b/app/services/whatsapp/health_service.rb
index 94789ef79..428fb4e6b 100644
--- a/app/services/whatsapp/health_service.rb
+++ b/app/services/whatsapp/health_service.rb
@@ -39,11 +39,11 @@ class Whatsapp::HealthService
def health_fields
%w[
+ id
quality_rating
messaging_limit_tier
code_verification_status
account_mode
- id
display_phone_number
name_status
verified_name
@@ -68,6 +68,7 @@ class Whatsapp::HealthService
def format_health_response(response)
{
+ id: response['id'],
display_phone_number: response['display_phone_number'],
verified_name: response['verified_name'],
name_status: response['name_status'],
@@ -75,10 +76,20 @@ class Whatsapp::HealthService
messaging_limit_tier: response['messaging_limit_tier'],
account_mode: response['account_mode'],
code_verification_status: response['code_verification_status'],
+ webhook_configuration: response['webhook_configuration'],
+ expected_webhook_url: build_expected_webhook_url,
throughput: response['throughput'],
last_onboarded_time: response['last_onboarded_time'],
platform_type: response['platform_type'],
+ certificate: response['certificate'],
business_id: @channel.provider_config['business_account_id']
}
end
+
+ def build_expected_webhook_url
+ frontend_url = ENV.fetch('FRONTEND_URL', nil)
+ return nil if frontend_url.blank?
+
+ "#{frontend_url}/webhooks/whatsapp/#{@channel.phone_number}"
+ end
end
diff --git a/app/services/whatsapp/webhook_setup_service.rb b/app/services/whatsapp/webhook_setup_service.rb
index 63fed1e93..97a53eb9a 100644
--- a/app/services/whatsapp/webhook_setup_service.rb
+++ b/app/services/whatsapp/webhook_setup_service.rb
@@ -1,9 +1,9 @@
class Whatsapp::WebhookSetupService
- def initialize(channel, waba_id, access_token)
+ def initialize(channel, waba_id = nil, access_token = nil)
@channel = channel
- @waba_id = waba_id
- @access_token = access_token
- @api_client = Whatsapp::FacebookApiClient.new(access_token)
+ @waba_id = waba_id || channel.provider_config['business_account_id']
+ @access_token = access_token || channel.provider_config['api_key']
+ @api_client = Whatsapp::FacebookApiClient.new(@access_token)
end
def perform
@@ -17,6 +17,11 @@ class Whatsapp::WebhookSetupService
setup_webhook
end
+ def register_callback
+ validate_parameters!
+ setup_webhook
+ end
+
private
def validate_parameters!
@@ -33,8 +38,6 @@ class Whatsapp::WebhookSetupService
store_pin(pin)
rescue StandardError => e
Rails.logger.warn("[WHATSAPP] Phone registration failed but continuing: #{e.message}")
- # Continue with webhook setup even if registration fails
- # This is just a warning, not a blocking error
end
def fetch_or_create_pin
diff --git a/config/routes.rb b/config/routes.rb
index 0992d21a2..5fe70bdca 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -218,6 +218,7 @@ Rails.application.routes.draw do
delete :avatar, on: :member
post :sync_templates, on: :member
get :health, on: :member
+ post :register_webhook, on: :member
if ChatwootApp.enterprise?
resource :conference, only: %i[create destroy], controller: 'conference' do
get :token, on: :member
From a5c50354fcd32fb61a16a62908c88ecab28808c2 Mon Sep 17 00:00:00 2001
From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
Date: Mon, 16 Mar 2026 13:13:37 +0530
Subject: [PATCH 2/2] feat: trigger assignment on resolve (#13780)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Description
When an agent resolves or snoozes a conversation, their capacity frees
up — but under Assignment V2, no new conversation was assigned until the
next event triggered the assignment job. This meant agents could sit
idle despite having queued conversations waiting. This change triggers
the AssignmentJob immediately on resolve/snooze so freed capacity is
utilized right away.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Enable auto assignment v2 on an inbox
- Create a conversation (gets auto-assigned to an available agent)
- Resolve the conversation
- Verify that another unassigned conversation in the inbox gets assigned
to the freed-up agent
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: tds-1
---
app/models/concerns/auto_assignment_handler.rb | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/app/models/concerns/auto_assignment_handler.rb b/app/models/concerns/auto_assignment_handler.rb
index dca154842..6be7a8d85 100644
--- a/app/models/concerns/auto_assignment_handler.rb
+++ b/app/models/concerns/auto_assignment_handler.rb
@@ -9,9 +9,9 @@ module AutoAssignmentHandler
private
def run_auto_assignment
- # Round robin kicks in on conversation create & update
- # run it only when conversation status changes to open
- return unless conversation_status_changed_to_open?
+ # Assignment V2: Also trigger assignment when conversation is resolved or snoozed,
+ # bypassing the open-only condition so the AssignmentJob can redistribute capacity.
+ return unless conversation_status_changed_to_open? || conversation_status_changed_to_resolved_or_snoozed?
return unless should_run_auto_assignment?
if inbox.auto_assignment_v2_enabled?
@@ -25,6 +25,10 @@ module AutoAssignmentHandler
end
end
+ def conversation_status_changed_to_resolved_or_snoozed?
+ inbox.auto_assignment_v2_enabled? && saved_change_to_status? && (resolved? || snoozed?)
+ end
+
def team_member_ids_with_capacity
return [] if team.blank? || team.allow_auto_assign.blank?
@@ -33,6 +37,9 @@ module AutoAssignmentHandler
def should_run_auto_assignment?
return false unless inbox.enable_auto_assignment?
+ # Assignment V2: Resolved/snoozed conversations still have an assignee, so bypass the
+ # assignee-blank check below. The AssignmentJob needs to run to rebalance assignments.
+ return true if conversation_status_changed_to_resolved_or_snoozed?
# run only if assignee is blank or doesn't have access to inbox
assignee.blank? || inbox.members.exclude?(assignee)