Compare commits

...
Author SHA1 Message Date
tds-1andTanmay Deep Sharma a53c2dd687 fix: clear assignment_v2_just_enabled flag after migration enqueue 2026-03-17 18:25:10 +05:30
tds-1andTanmay Deep Sharma 5bf5c88075 fix: scope migration trigger to admin toggle and broaden inbox filter 2026-03-17 15:15:31 +05:30
Shivam Mishra 2f6da8545f test: assignment v2 migration 2026-03-17 14:17:29 +05:30
Shivam Mishra 8852e87a87 feat: move feature enabling to after create_commit 2026-03-17 14:14:48 +05:30
Shivam MishraandGitHub b0d11b5c1d chore: update labels 2026-03-17 14:04:19 +05:30
Tanmay Deep Sharma 1206cbb08c use distinct policy name to avoid colliding with user-created policies 2026-03-17 13:28:39 +05:30
Tanmay Deep Sharma aead51cbde run the migration script on assignment v2 toggle 2026-03-17 13:23:39 +05:30
Tanmay Deep Sharma 93ea9ecc9f Merge remote-tracking branch 'origin/chore/migrate-max-assignment-limit-to-policy' into chore/migrate-max-assignment-limit-to-policy 2026-03-17 12:49:33 +05:30
Tanmay Deep Sharma a36dd4bcda fix duration value review comment 2026-03-17 12:48:53 +05:30
Tanmay Deep SharmaandGitHub fc83fd39ca Merge branch 'develop' into chore/migrate-max-assignment-limit-to-policy 2026-03-17 10:35:56 +05:30
Tanmay Deep Sharma 2cacb0868c update default policy name 2026-03-16 22:25:21 +05:30
Tanmay Deep Sharma b8d35e8749 fix: guard migration against non-numeric max_assignment_limit values 2026-03-16 19:51:02 +05:30
Tanmay Deep Sharma 3a99d7fad6 fix: check phone number level webhook before waba and application level 2026-03-16 14:14:14 +05:30
Tanmay Deep Sharma d210f61855 Merge remote-tracking branch 'origin/develop' into chore/migrate-max-assignment-limit-to-policy 2026-03-16 14:05:35 +05:30
tds-1andTanmay Deep Sharma b15f60755d fix: remove default exclusion rules from agent capacity policy form 2026-03-16 14:00:34 +05:30
Tanmay Deep Sharma e5229802df fix: require migration class in rake task before invoking 2026-03-16 13:10:50 +05:30
tds-1andClaude Opus 4.6 8c0655d618 chore: bump schema version for data migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:05:39 +05:30
tds-1andClaude Opus 4.6 9f345c8d48 chore: migrate max_assignment_limit to agent capacity policies
Adds a data migration and rake task to convert inbox max_assignment_limit
settings into AgentCapacityPolicy records with per-inbox conversation
limits, preserving the exact capacity-based semantics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:57:28 +05:30
9 changed files with 107 additions and 5 deletions
@@ -20,7 +20,7 @@ const excludedLabels = defineModel('excludedLabels', {
const excludeOlderThanMinutes = defineModel('excludeOlderThanMinutes', {
type: Number,
default: 10,
default: null,
});
// Duration limits: 10 minutes to 999 days (in minutes)
@@ -32,6 +32,7 @@ const convertToMinutes = newValue => {
const transformedValue = computed({
get() {
if (duration.value == null) return null;
if (unit.value === DURATION_UNITS.MINUTES) return duration.value;
if (unit.value === DURATION_UNITS.HOURS)
return Math.floor(duration.value / 60);
@@ -41,6 +42,10 @@ const transformedValue = computed({
return 0;
},
set(newValue) {
if (newValue == null || newValue === '') {
duration.value = null;
return;
}
let minuteValue = convertToMinutes(newValue);
duration.value = Math.min(Math.max(minuteValue, props.min), props.max);
@@ -53,6 +58,7 @@ const transformedValue = computed({
// this might create some confusion, especially when saving
// this watcher fixes it by rounding the duration basically, to the nearest unit value
watch(unit, () => {
if (duration.value == null) return;
let adjustedValue = convertToMinutes(transformedValue.value);
duration.value = Math.min(Math.max(adjustedValue, props.min), props.max);
});
@@ -81,7 +81,7 @@ const formData = computed(() => ({
...(selectedPolicy.value?.exclusionRules?.excludedLabels || []),
],
excludeOlderThanHours:
selectedPolicy.value?.exclusionRules?.excludeOlderThanHours || 10,
selectedPolicy.value?.exclusionRules?.excludeOlderThanHours ?? null,
},
inboxCapacityLimits:
selectedPolicy.value?.inboxCapacityLimits?.map(limit => ({
@@ -17,7 +17,7 @@ const props = defineProps({
enabled: false,
exclusionRules: {
excludedLabels: [],
excludeOlderThanHours: 10,
excludeOlderThanHours: null,
},
inboxCapacityLimits: [],
}),
@@ -84,7 +84,7 @@ const state = reactive({
description: '',
exclusionRules: {
excludedLabels: [],
excludeOlderThanHours: 10,
excludeOlderThanHours: null,
},
inboxCapacityLimits: [],
});
@@ -120,7 +120,7 @@ const resetForm = () => {
description: '',
exclusionRules: {
excludedLabels: [],
excludeOlderThanHours: 10,
excludeOlderThanHours: null,
},
inboxCapacityLimits: [],
});
@@ -146,6 +146,7 @@ const showWebhookSection = computed(
const webhookUrl = computed(
() =>
props.healthData?.webhook_configuration?.phone_number ||
props.healthData?.webhook_configuration?.whatsapp_business_account ||
props.healthData?.webhook_configuration?.application
);
@@ -0,0 +1,45 @@
class Migration::AccountAssignmentPolicyJob < ApplicationJob
queue_as :default
INT_MAX = (2**31) - 1
def perform(account)
inboxes_with_limit = account.inboxes
.where("auto_assignment_config->>'max_assignment_limit' ~ '[1-9]'")
return if inboxes_with_limit.empty?
ActiveRecord::Base.transaction do
policy = AgentCapacityPolicy.find_or_create_by!(account: account, name: 'Auto Assignment Capacity') do |p|
p.description = 'Migrated from inbox settings'
end
create_inbox_limits(policy, inboxes_with_limit)
assign_policy_to_inbox_members(account, policy, inboxes_with_limit)
end
end
private
def create_inbox_limits(policy, inboxes)
inboxes.each do |inbox|
next if InboxCapacityLimit.exists?(agent_capacity_policy_id: policy.id, inbox_id: inbox.id)
limit = [inbox.auto_assignment_config['max_assignment_limit'].to_i, INT_MAX].min
InboxCapacityLimit.create!(
agent_capacity_policy: policy,
inbox: inbox,
conversation_limit: limit
)
end
end
def assign_policy_to_inbox_members(account, policy, inboxes)
member_user_ids = InboxMember.where(inbox_id: inboxes.select(:id)).distinct.pluck(:user_id)
account.account_users
.where(user_id: member_user_ids, agent_capacity_policy_id: nil)
.find_each { |account_user| account_user.update!(agent_capacity_policy_id: policy.id) }
end
end
@@ -5,8 +5,10 @@ module Enterprise::Account
# Auto-sync advanced_assignment with assignment_v2 when features are bulk-updated via admin UI
def selected_feature_flags=(features)
was_assignment_v2 = feature_enabled?('assignment_v2')
super
sync_assignment_features
@assignment_v2_just_enabled = !was_assignment_v2 && feature_enabled?('assignment_v2')
end
def mark_for_deletion(reason = 'manual_deletion')
@@ -19,5 +19,18 @@ module Enterprise::Concerns::Account
has_many :voice_channels, dependent: :destroy_async, class_name: '::Channel::Voice'
has_one :saml_settings, dependent: :destroy_async, class_name: 'AccountSamlSettings'
after_commit :migrate_assignment_policies, if: :assignment_v2_just_enabled?
end
private
def assignment_v2_just_enabled?
!!@assignment_v2_just_enabled
end
def migrate_assignment_policies
@assignment_v2_just_enabled = false
Migration::AccountAssignmentPolicyJob.perform_later(self)
end
end
+35
View File
@@ -222,6 +222,41 @@ RSpec.describe Account, type: :model do
end
end
describe 'after_commit :migrate_assignment_policies' do
let(:account) { create(:account) }
context 'when assignment_v2 is toggled on via admin UI' do
it 'enqueues Migration::AccountAssignmentPolicyJob after commit' do
enabled_flags = account.enabled_features.keys.map { |f| "feature_#{f}".to_sym } + [:feature_assignment_v2]
expect do
account.update!(selected_feature_flags: enabled_flags)
end.to have_enqueued_job(Migration::AccountAssignmentPolicyJob).with(account)
end
end
context 'when assignment_v2 is already enabled and another flag changes' do
it 'does not enqueue the job' do
account.enable_features!('assignment_v2')
clear_enqueued_jobs
enabled_flags = account.enabled_features.keys.map { |f| "feature_#{f}".to_sym }
expect do
account.update!(selected_feature_flags: enabled_flags)
end.not_to have_enqueued_job(Migration::AccountAssignmentPolicyJob)
end
end
context 'when assignment_v2 is enabled via enable_features!' do
it 'does not enqueue the job' do
expect do
account.enable_features!('assignment_v2')
end.not_to have_enqueued_job(Migration::AccountAssignmentPolicyJob)
end
end
end
describe 'account deletion' do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }