Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
---
.../dashboard/i18n/locale/en/inboxMgmt.json | 5 ++
.../inbox/channels/WhatsappEmbeddedSignup.vue | 26 +++++-
.../settings/inbox/channels/whatsapp/utils.js | 2 +-
app/services/whatsapp/facebook_api_client.rb | 10 +++
.../whatsapp/webhook_setup_service.rb | 12 ++-
.../super_admin/app_configs/show.html.erb | 12 ++-
config/installation_config.yml | 3 +-
.../whatsapp/webhook_setup_service_spec.rb | 83 +++++++++++++------
8 files changed, 120 insertions(+), 33 deletions(-)
diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
index 821e2ecbd..4006212a3 100644
--- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
+++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
@@ -280,6 +280,11 @@
"SECURE_AUTH": "Secure OAuth based authentication",
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
},
+ "LEARN_MORE": {
+ "TEXT": "To learn more about integrated signup, pricing, and limitations, visit",
+ "LINK_TEXT": "this link.",
+ "LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations"
+ },
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
"AUTH_PROCESSING": "Authenticating with Meta",
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue
index 384082b71..583ca2413 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue
@@ -107,7 +107,7 @@ const completeSignupFlow = async businessDataParam => {
code: authCode.value,
business_id: businessDataParam.business_id,
waba_id: businessDataParam.waba_id,
- phone_number_id: businessDataParam.phone_number_id,
+ phone_number_id: businessDataParam?.phone_number_id || '',
};
const responseData = await store.dispatch(
@@ -127,7 +127,10 @@ const completeSignupFlow = async businessDataParam => {
// Message handling
const handleEmbeddedSignupData = async data => {
- if (data.event === 'FINISH') {
+ if (
+ data.event === 'FINISH' ||
+ data.event === 'FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING'
+ ) {
const businessDataLocal = data.data;
if (isValidBusinessData(businessDataLocal)) {
@@ -262,6 +265,25 @@ onBeforeUnmount(() => {
+
+
{
override_default_response_type: true,
extras: {
setup: {},
- featureType: '',
+ featureType: 'whatsapp_business_app_onboarding',
sessionInfoVersion: '3',
},
}
diff --git a/app/services/whatsapp/facebook_api_client.rb b/app/services/whatsapp/facebook_api_client.rb
index 441e41b1a..55ce4e698 100644
--- a/app/services/whatsapp/facebook_api_client.rb
+++ b/app/services/whatsapp/facebook_api_client.rb
@@ -50,6 +50,16 @@ class Whatsapp::FacebookApiClient
handle_response(response, 'Phone registration failed')
end
+ def phone_number_verified?(phone_number_id)
+ response = HTTParty.get(
+ "#{BASE_URI}/#{@api_version}/#{phone_number_id}",
+ headers: request_headers
+ )
+
+ data = handle_response(response, 'Phone status check failed')
+ data['code_verification_status'] == 'VERIFIED'
+ end
+
def subscribe_waba_webhook(waba_id, callback_url, verify_token)
response = HTTParty.post(
"#{BASE_URI}/#{@api_version}/#{waba_id}/subscribed_apps",
diff --git a/app/services/whatsapp/webhook_setup_service.rb b/app/services/whatsapp/webhook_setup_service.rb
index 6aff531f7..a3faaaa56 100644
--- a/app/services/whatsapp/webhook_setup_service.rb
+++ b/app/services/whatsapp/webhook_setup_service.rb
@@ -8,7 +8,8 @@ class Whatsapp::WebhookSetupService
def perform
validate_parameters!
- register_phone_number
+ # Since coexistence method does not need to register, we check it
+ register_phone_number unless phone_number_verified?
setup_webhook
end
@@ -64,4 +65,13 @@ class Whatsapp::WebhookSetupService
"#{frontend_url}/webhooks/whatsapp/#{phone_number}"
end
+
+ def phone_number_verified?
+ phone_number_id = @channel.provider_config['phone_number_id']
+
+ @api_client.phone_number_verified?(phone_number_id)
+ rescue StandardError => e
+ Rails.logger.error("[WHATSAPP] Phone registration status check failed, but continuing: #{e.message}")
+ false
+ end
end
diff --git a/app/views/super_admin/app_configs/show.html.erb b/app/views/super_admin/app_configs/show.html.erb
index 6f95a6418..8bf513597 100644
--- a/app/views/super_admin/app_configs/show.html.erb
+++ b/app/views/super_admin/app_configs/show.html.erb
@@ -53,9 +53,15 @@
- <% else %>
- <%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
- <% end %>
+ <% elsif @installation_configs[key]&.dig('type') == 'select' && @installation_configs[key]&.dig('options').present? %>
+ <%= form.select "app_config[#{key}]",
+ @installation_configs[key]['options'].map { |val, label| [label, val] },
+ { selected: @app_config[key] },
+ class: "mt-2 border border-slate-100 p-1 rounded-md"
+ %>
+ <% else %>
+ <%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
+ <% end %>
<%if @installation_configs[key]&.dig('description').present? %>
<%= @installation_configs[key]&.dig('description') %>
diff --git a/config/installation_config.yml b/config/installation_config.yml
index 53251fc3c..01799e830 100644
--- a/config/installation_config.yml
+++ b/config/installation_config.yml
@@ -10,7 +10,8 @@
# locked: if you don't specify locked attribute in yaml, the default value will be true,
# which means the particular config will be locked and won't be available in `super_admin/installation_configs`
# premium: These values get overwritten unless the user is on a premium plan
-# type: The type of the config. Default is text, boolean is also supported
+# type: The type of the config. Default is text, select and boolean are also supported
+# options: For select types, its required to have options for the select in the following pattern: "option_value":"Human readable option"
# ------- Branding Related Config ------- #
- name: INSTALLATION_NAME
diff --git a/spec/services/whatsapp/webhook_setup_service_spec.rb b/spec/services/whatsapp/webhook_setup_service_spec.rb
index 89beca922..7cee115c2 100644
--- a/spec/services/whatsapp/webhook_setup_service_spec.rb
+++ b/spec/services/whatsapp/webhook_setup_service_spec.rb
@@ -24,25 +24,19 @@ describe Whatsapp::WebhookSetupService do
end
describe '#perform' do
- context 'when all operations succeed' do
+ context 'when phone number is NOT verified (should register)' do
before do
+ allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
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 })
+ .with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
- it 'registers the phone number' do
+ it 'registers the phone number and sets up webhook' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
- service.perform
- end
- end
-
- it 'sets up webhook subscription' do
- with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
service.perform
@@ -50,33 +44,71 @@ describe Whatsapp::WebhookSetupService do
end
end
- context 'when phone registration fails' do
+ context 'when phone number IS verified (should NOT register)' do
before do
- allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
- allow(api_client).to receive(:register_phone_number)
- .and_raise('Registration failed')
+ allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(true)
allow(api_client).to receive(:subscribe_waba_webhook)
- .and_return({ 'success' => true })
+ .with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
end
- it 'continues with webhook setup' do
+ it 'does NOT register phone, but sets up webhook' 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)
+ .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(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
+ 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)
expect { service.perform }.not_to raise_error
end
end
end
- context 'when webhook setup fails' do
+ context 'when phone registration fails (not blocking)' do
before do
+ allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
+ allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
+ allow(api_client).to receive(:register_phone_number).and_raise('Registration failed')
+ allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true })
+ allow(channel).to receive(:save!)
+ end
+
+ it 'continues with webhook setup even if registration fails' 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)
+ expect { service.perform }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when webhook setup fails (should raise)' do
+ before do
+ allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
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_raise('Webhook failed')
+ allow(api_client).to receive(:subscribe_waba_webhook).and_raise('Webhook failed')
end
it 'raises an error' 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)
expect { service.perform }.to raise_error(/Webhook setup failed/)
end
end
@@ -84,24 +116,25 @@ describe Whatsapp::WebhookSetupService do
context 'when required parameters are missing' do
it 'raises error when channel is nil' do
- service = described_class.new(nil, waba_id, access_token)
- expect { service.perform }.to raise_error(ArgumentError, 'Channel is required')
+ service_invalid = described_class.new(nil, waba_id, access_token)
+ expect { service_invalid.perform }.to raise_error(ArgumentError, 'Channel is required')
end
it 'raises error when waba_id is blank' do
- service = described_class.new(channel, '', access_token)
- expect { service.perform }.to raise_error(ArgumentError, 'WABA ID is required')
+ service_invalid = described_class.new(channel, '', access_token)
+ expect { service_invalid.perform }.to raise_error(ArgumentError, 'WABA ID is required')
end
it 'raises error when access_token is blank' do
- service = described_class.new(channel, waba_id, '')
- expect { service.perform }.to raise_error(ArgumentError, 'Access token is required')
+ service_invalid = described_class.new(channel, waba_id, '')
+ expect { service_invalid.perform }.to raise_error(ArgumentError, 'Access token is required')
end
end
context 'when PIN already exists' do
before do
channel.provider_config['verification_pin'] = 123_456
+ allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false)
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!)
From 8ea21eeefde1a3b2523b70d2de36be4d6e0448d6 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Mon, 11 Aug 2025 06:09:59 +0530
Subject: [PATCH 09/10] chore: Improve portal settings validation and error
handling (#12134)
# Pull Request Template
## Description
This PR includes:
1. Previously, the URL fields accepted any value starting with `https`.
Added proper URL validation to ensure valid URLs are entered.
2. Fixed an issue where form save errors displayed `[object Object]` in
the toast due to inconsistent error formatting from the backend.
Fixes https://linear.app/chatwoot/issue/CW-5389/help-center-bugs
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/63ddca2c5e2e45c99b66153ed146524f?sid=fd25331b-6b67-4722-9b36-1213496a0741
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] 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
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---
app/controllers/api/v1/accounts/portals_controller.rb | 5 ++---
.../Pages/PortalSettingsPage/PortalBaseSettings.vue | 6 +++---
app/javascript/dashboard/i18n/locale/en/helpCenter.json | 2 +-
3 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb
index 18538e842..af96441f8 100644
--- a/app/controllers/api/v1/accounts/portals_controller.rb
+++ b/app/controllers/api/v1/accounts/portals_controller.rb
@@ -26,9 +26,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain
process_attached_logo if params[:blob_id].present?
- rescue StandardError => e
- Rails.logger.error e
- render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
+ rescue ActiveRecord::RecordInvalid => e
+ render_record_invalid(e)
end
end
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
index 7995270b5..862a2cd67 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
@@ -7,8 +7,8 @@ import { useStore, useStoreGetters } from 'dashboard/composables/store';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { useVuelidate } from '@vuelidate/core';
-import { required, minLength, helpers } from '@vuelidate/validators';
-import { shouldBeUrl, isValidSlug } from 'shared/helpers/Validators';
+import { required, minLength, helpers, url } from '@vuelidate/validators';
+import { isValidSlug } from 'shared/helpers/Validators';
import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
@@ -71,7 +71,7 @@ const rules = {
isValidSlug
),
},
- homePageLink: { shouldBeUrl },
+ homePageLink: { url },
};
const v$ = useVuelidate(rules, state);
diff --git a/app/javascript/dashboard/i18n/locale/en/helpCenter.json b/app/javascript/dashboard/i18n/locale/en/helpCenter.json
index bd7fb986a..fd2b1a788 100644
--- a/app/javascript/dashboard/i18n/locale/en/helpCenter.json
+++ b/app/javascript/dashboard/i18n/locale/en/helpCenter.json
@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
- "ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
+ "ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",
From 2ebde48d15c7117b75e66b597988ba9dde66e1bd Mon Sep 17 00:00:00 2001
From: Tanmay Sharma
Date: Mon, 11 Aug 2025 08:03:01 +0530
Subject: [PATCH 10/10] add migration files for assignment v2
---
app/models/account_user.rb | 32 +++++----
...250806140000_create_assignment_policies.rb | 21 ++++++
...140001_create_inbox_assignment_policies.rb | 12 ++++
...06140002_create_agent_capacity_policies.rb | 14 ++++
...0806140003_create_inbox_capacity_limits.rb | 15 ++++
..._agent_capacity_policy_to_account_users.rb | 7 ++
db/migrate/20250806140005_create_leaves.rb | 21 ++++++
db/schema.rb | 68 ++++++++++++++++++-
8 files changed, 174 insertions(+), 16 deletions(-)
create mode 100644 db/migrate/20250806140000_create_assignment_policies.rb
create mode 100644 db/migrate/20250806140001_create_inbox_assignment_policies.rb
create mode 100644 db/migrate/20250806140002_create_agent_capacity_policies.rb
create mode 100644 db/migrate/20250806140003_create_inbox_capacity_limits.rb
create mode 100644 db/migrate/20250806140004_add_agent_capacity_policy_to_account_users.rb
create mode 100644 db/migrate/20250806140005_create_leaves.rb
diff --git a/app/models/account_user.rb b/app/models/account_user.rb
index d559ab5b4..bbcb0e010 100644
--- a/app/models/account_user.rb
+++ b/app/models/account_user.rb
@@ -2,24 +2,26 @@
#
# Table name: account_users
#
-# id :bigint not null, primary key
-# active_at :datetime
-# auto_offline :boolean default(TRUE), not null
-# availability :integer default("online"), not null
-# role :integer default("agent")
-# created_at :datetime not null
-# updated_at :datetime not null
-# account_id :bigint
-# custom_role_id :bigint
-# inviter_id :bigint
-# user_id :bigint
+# id :bigint not null, primary key
+# active_at :datetime
+# auto_offline :boolean default(TRUE), not null
+# availability :integer default("online"), not null
+# role :integer default("agent")
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint
+# agent_capacity_policy_id :bigint
+# custom_role_id :bigint
+# inviter_id :bigint
+# user_id :bigint
#
# Indexes
#
-# index_account_users_on_account_id (account_id)
-# index_account_users_on_custom_role_id (custom_role_id)
-# index_account_users_on_user_id (user_id)
-# uniq_user_id_per_account_id (account_id,user_id) UNIQUE
+# index_account_users_on_account_id (account_id)
+# index_account_users_on_agent_capacity_policy_id (agent_capacity_policy_id)
+# index_account_users_on_custom_role_id (custom_role_id)
+# index_account_users_on_user_id (user_id)
+# uniq_user_id_per_account_id (account_id,user_id) UNIQUE
#
class AccountUser < ApplicationRecord
diff --git a/db/migrate/20250806140000_create_assignment_policies.rb b/db/migrate/20250806140000_create_assignment_policies.rb
new file mode 100644
index 000000000..c02e3d6de
--- /dev/null
+++ b/db/migrate/20250806140000_create_assignment_policies.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class CreateAssignmentPolicies < ActiveRecord::Migration[7.1]
+ def change
+ create_table :assignment_policies do |t|
+ t.references :account, null: false, index: true
+ t.string :name, null: false, limit: 255
+ t.text :description
+ t.integer :assignment_order, null: false, default: 0 # 0: round_robin, 1: balanced
+ t.integer :conversation_priority, null: false, default: 0 # 0: earliest_created, 1: longest_waiting
+ t.integer :fair_distribution_limit, null: false, default: 100
+ t.integer :fair_distribution_window, null: false, default: 3600 # seconds
+ t.boolean :enabled, null: false, default: true
+
+ t.timestamps
+ end
+
+ add_index :assignment_policies, [:account_id, :name], unique: true
+ add_index :assignment_policies, :enabled
+ end
+end
diff --git a/db/migrate/20250806140001_create_inbox_assignment_policies.rb b/db/migrate/20250806140001_create_inbox_assignment_policies.rb
new file mode 100644
index 000000000..e80c67679
--- /dev/null
+++ b/db/migrate/20250806140001_create_inbox_assignment_policies.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class CreateInboxAssignmentPolicies < ActiveRecord::Migration[7.1]
+ def change
+ create_table :inbox_assignment_policies do |t|
+ t.references :inbox, null: false, index: true
+ t.references :assignment_policy, null: false, index: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20250806140002_create_agent_capacity_policies.rb b/db/migrate/20250806140002_create_agent_capacity_policies.rb
new file mode 100644
index 000000000..4fdeabc92
--- /dev/null
+++ b/db/migrate/20250806140002_create_agent_capacity_policies.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateAgentCapacityPolicies < ActiveRecord::Migration[7.1]
+ def change
+ create_table :agent_capacity_policies do |t|
+ t.references :account, null: false, index: true
+ t.string :name, null: false, limit: 255
+ t.text :description
+ t.jsonb :exclusion_rules, default: {}, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20250806140003_create_inbox_capacity_limits.rb b/db/migrate/20250806140003_create_inbox_capacity_limits.rb
new file mode 100644
index 000000000..c107ce182
--- /dev/null
+++ b/db/migrate/20250806140003_create_inbox_capacity_limits.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateInboxCapacityLimits < ActiveRecord::Migration[7.1]
+ def change
+ create_table :inbox_capacity_limits do |t|
+ t.references :agent_capacity_policy, null: false, index: true
+ t.references :inbox, null: false, index: true
+ t.integer :conversation_limit, null: false
+
+ t.timestamps
+ end
+
+ add_index :inbox_capacity_limits, [:agent_capacity_policy_id, :inbox_id], unique: true
+ end
+end
diff --git a/db/migrate/20250806140004_add_agent_capacity_policy_to_account_users.rb b/db/migrate/20250806140004_add_agent_capacity_policy_to_account_users.rb
new file mode 100644
index 000000000..53bc8e8f9
--- /dev/null
+++ b/db/migrate/20250806140004_add_agent_capacity_policy_to_account_users.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddAgentCapacityPolicyToAccountUsers < ActiveRecord::Migration[7.1]
+ def change
+ add_reference :account_users, :agent_capacity_policy, null: true, index: true
+ end
+end
diff --git a/db/migrate/20250806140005_create_leaves.rb b/db/migrate/20250806140005_create_leaves.rb
new file mode 100644
index 000000000..982ec1181
--- /dev/null
+++ b/db/migrate/20250806140005_create_leaves.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class CreateLeaves < ActiveRecord::Migration[7.1]
+ def change
+ create_table :leaves do |t|
+ t.references :account, null: false
+ t.references :user, null: false
+ t.date :start_date, null: false
+ t.date :end_date, null: false
+ t.integer :leave_type, null: false, default: 0
+ t.integer :status, null: false, default: 0
+ t.text :reason
+ t.references :approved_by
+ t.datetime :approved_at
+
+ t.timestamps
+ end
+
+ add_index :leaves, [:account_id, :status]
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c94b3618a..43996ac45 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2025_08_05_160307) do
+ActiveRecord::Schema[7.1].define(version: 2025_08_06_140005) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -39,8 +39,10 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_05_160307) do
t.integer "availability", default: 0, null: false
t.boolean "auto_offline", default: true, null: false
t.bigint "custom_role_id"
+ t.bigint "agent_capacity_policy_id"
t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true
t.index ["account_id"], name: "index_account_users_on_account_id"
+ t.index ["agent_capacity_policy_id"], name: "index_account_users_on_agent_capacity_policy_id"
t.index ["custom_role_id"], name: "index_account_users_on_custom_role_id"
t.index ["user_id"], name: "index_account_users_on_user_id"
end
@@ -120,6 +122,16 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_05_160307) do
t.index ["account_id"], name: "index_agent_bots_on_account_id"
end
+ create_table "agent_capacity_policies", force: :cascade do |t|
+ t.bigint "account_id", null: false
+ t.string "name", limit: 255, null: false
+ t.text "description"
+ t.jsonb "exclusion_rules", default: {}, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_agent_capacity_policies_on_account_id"
+ end
+
create_table "applied_slas", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "sla_policy_id", null: false
@@ -169,6 +181,22 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_05_160307) do
t.index ["views"], name: "index_articles_on_views"
end
+ create_table "assignment_policies", force: :cascade do |t|
+ t.bigint "account_id", null: false
+ t.string "name", limit: 255, null: false
+ t.text "description"
+ t.integer "assignment_order", default: 0, null: false
+ t.integer "conversation_priority", default: 0, null: false
+ t.integer "fair_distribution_limit", default: 100, null: false
+ t.integer "fair_distribution_window", default: 3600, null: false
+ t.boolean "enabled", default: true, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id", "name"], name: "index_assignment_policies_on_account_id_and_name", unique: true
+ t.index ["account_id"], name: "index_assignment_policies_on_account_id"
+ t.index ["enabled"], name: "index_assignment_policies_on_enabled"
+ end
+
create_table "attachments", id: :serial, force: :cascade do |t|
t.integer "file_type", default: 0
t.string "external_url"
@@ -728,6 +756,26 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_05_160307) do
t.datetime "updated_at", null: false
end
+ create_table "inbox_assignment_policies", force: :cascade do |t|
+ t.bigint "inbox_id", null: false
+ t.bigint "assignment_policy_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["assignment_policy_id"], name: "index_inbox_assignment_policies_on_assignment_policy_id"
+ t.index ["inbox_id"], name: "index_inbox_assignment_policies_on_inbox_id"
+ end
+
+ create_table "inbox_capacity_limits", force: :cascade do |t|
+ t.bigint "agent_capacity_policy_id", null: false
+ t.bigint "inbox_id", null: false
+ t.integer "conversation_limit", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["agent_capacity_policy_id", "inbox_id"], name: "idx_on_agent_capacity_policy_id_inbox_id_71c7ec4caf", unique: true
+ t.index ["agent_capacity_policy_id"], name: "index_inbox_capacity_limits_on_agent_capacity_policy_id"
+ t.index ["inbox_id"], name: "index_inbox_capacity_limits_on_inbox_id"
+ end
+
create_table "inbox_members", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "inbox_id", null: false
@@ -800,6 +848,24 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_05_160307) do
t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true
end
+ create_table "leaves", force: :cascade do |t|
+ t.bigint "account_id", null: false
+ t.bigint "user_id", null: false
+ t.date "start_date", null: false
+ t.date "end_date", null: false
+ t.integer "leave_type", default: 0, null: false
+ t.integer "status", default: 0, null: false
+ t.text "reason"
+ t.bigint "approved_by_id"
+ t.datetime "approved_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id", "status"], name: "index_leaves_on_account_id_and_status"
+ t.index ["account_id"], name: "index_leaves_on_account_id"
+ t.index ["approved_by_id"], name: "index_leaves_on_approved_by_id"
+ t.index ["user_id"], name: "index_leaves_on_user_id"
+ end
+
create_table "macros", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "name", null: false