From a3e74c314acffdad0f2fa914a05e894983856301 Mon Sep 17 00:00:00 2001 From: Tanmay Sharma Date: Mon, 11 Aug 2025 16:56:50 +0530 Subject: [PATCH] segregate enterprise and base --- app/models/account.rb | 1 + app/models/assignment_policy.rb | 40 +++----------- app/models/inbox_assignment_policy.rb | 2 - app/models/notification.rb | 1 + .../assignment_v2/assignment_service.rb | 8 ++- config/features.yml | 3 ++ config/routes.rb | 7 +++ .../models/enterprise/assignment_policy.rb | 27 ++++++++++ .../assignment_v2/assignment_service.rb | 10 ++++ .../assignment_v2/balanced_selector.rb | 54 +++++++++++++++++++ 10 files changed, 113 insertions(+), 40 deletions(-) create mode 100644 enterprise/app/models/enterprise/assignment_policy.rb create mode 100644 enterprise/app/services/enterprise/assignment_v2/assignment_service.rb create mode 100644 enterprise/app/services/enterprise/assignment_v2/balanced_selector.rb diff --git a/app/models/account.rb b/app/models/account.rb index f8eb998f0..b84d7f526 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -61,6 +61,7 @@ class Account < ApplicationRecord has_many :agent_bots, dependent: :destroy_async has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api' has_many :articles, dependent: :destroy_async, class_name: '::Article' + has_many :assignment_policies, dependent: :destroy_async has_many :automation_rules, dependent: :destroy_async has_many :macros, dependent: :destroy_async has_many :campaigns, dependent: :destroy_async diff --git a/app/models/assignment_policy.rb b/app/models/assignment_policy.rb index b7a898569..4f1d6eab9 100644 --- a/app/models/assignment_policy.rb +++ b/app/models/assignment_policy.rb @@ -9,7 +9,7 @@ # conversation_priority :integer default("earliest_created"), not null # description :text # enabled :boolean default(TRUE), not null -# fair_distribution_limit :integer default(10), not null +# fair_distribution_limit :integer default(100), not null # fair_distribution_window :integer default(3600), not null # name :string(255) not null # created_at :datetime not null @@ -18,16 +18,14 @@ # # Indexes # -# index_assignment_policies_on_account_id (account_id) -# index_assignment_policies_on_enabled (enabled) -# unique_assignment_policy_name_per_account (account_id,name) UNIQUE +# index_assignment_policies_on_account_id (account_id) +# index_assignment_policies_on_account_id_and_name (account_id,name) UNIQUE +# index_assignment_policies_on_enabled (enabled) # class AssignmentPolicy < ApplicationRecord - include AccountCacheRevalidator - # Enums - enum assignment_order: { round_robin: 0, balanced: 1 } + enum assignment_order: { round_robin: 0 } enum conversation_priority: { earliest_created: 0, longest_waiting: 1 } # Associations @@ -44,12 +42,6 @@ class AssignmentPolicy < ApplicationRecord validates :assignment_order, inclusion: { in: assignment_orders.keys } validates :conversation_priority, inclusion: { in: conversation_priorities.keys } - # Validate balanced assignment is only available for enterprise - validate :validate_balanced_assignment_enterprise_only - - # Server-side validation to prevent bypass - before_save :enforce_enterprise_features - # Scopes scope :enabled, -> { where(enabled: true) } scope :disabled, -> { where(enabled: false) } @@ -58,10 +50,6 @@ class AssignmentPolicy < ApplicationRecord after_update_commit :clear_assignment_caches after_destroy :clear_assignment_caches - def can_use_balanced_assignment? - account.feature_enabled?(:enterprise_agent_capacity) if account.respond_to?(:feature_enabled?) - end - def webhook_data { id: id, @@ -77,22 +65,6 @@ class AssignmentPolicy < ApplicationRecord private - def validate_balanced_assignment_enterprise_only - return unless balanced? - return if can_use_balanced_assignment? - - errors.add(:assignment_order, 'Balanced assignment is only available for enterprise accounts') - end - - def enforce_enterprise_features - # Server-side enforcement to prevent API bypass - return unless balanced? && !can_use_balanced_assignment? - - # Force to round_robin if enterprise not available - self.assignment_order = 'round_robin' - Rails.logger.warn("Assignment V2: Forced assignment_order to round_robin for non-enterprise account #{account_id}") - end - def clear_assignment_caches # Clear Redis caches when policy is updated Rails.cache.delete("assignment_v2:policy:#{id}") @@ -101,3 +73,5 @@ class AssignmentPolicy < ApplicationRecord end end end + +AssignmentPolicy.prepend_mod_with('AssignmentPolicy') diff --git a/app/models/inbox_assignment_policy.rb b/app/models/inbox_assignment_policy.rb index 2540fadf1..d0293cd47 100644 --- a/app/models/inbox_assignment_policy.rb +++ b/app/models/inbox_assignment_policy.rb @@ -14,7 +14,6 @@ # # index_inbox_assignment_policies_on_assignment_policy_id (assignment_policy_id) # index_inbox_assignment_policies_on_inbox_id (inbox_id) -# unique_inbox_assignment_policy (inbox_id) UNIQUE # class InboxAssignmentPolicy < ApplicationRecord @@ -25,7 +24,6 @@ class InboxAssignmentPolicy < ApplicationRecord belongs_to :assignment_policy # Validations - validates :inbox_id, uniqueness: true validate :inbox_belongs_to_same_account # Delegations diff --git a/app/models/notification.rb b/app/models/notification.rb index 8c4162702..db07e3679 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -19,6 +19,7 @@ # # Indexes # +# idx_notifications_performance (user_id,account_id,snoozed_until,read_at) # index_notifications_on_account_id (account_id) # index_notifications_on_last_activity_at (last_activity_at) # index_notifications_on_user_id (user_id) diff --git a/app/services/assignment_v2/assignment_service.rb b/app/services/assignment_v2/assignment_service.rb index bb7260593..99efd28b8 100644 --- a/app/services/assignment_v2/assignment_service.rb +++ b/app/services/assignment_v2/assignment_service.rb @@ -53,11 +53,7 @@ class AssignmentV2::AssignmentService end def selector_service - @selector_service ||= if policy.assignment_order == 'balanced' && enterprise_enabled? && policy.can_use_balanced_assignment? - ::Enterprise::AssignmentV2::BalancedSelector.new(inbox: inbox) - else - AssignmentV2::RoundRobinSelector.new(inbox: inbox) - end + @selector_service ||= AssignmentV2::RoundRobinSelector.new(inbox: inbox) end def unassigned_conversations(limit) @@ -110,3 +106,5 @@ class AssignmentV2::AssignmentService Rails.logger.error "AssignmentV2: Failed to record assignment in rate limiter: #{e.message}" end end + +AssignmentV2::AssignmentService.prepend_mod_with('AssignmentV2::AssignmentService') diff --git a/config/features.yml b/config/features.yml index db2d46700..38cd04682 100644 --- a/config/features.yml +++ b/config/features.yml @@ -191,3 +191,6 @@ display_name: CRM V2 enabled: false chatwoot_internal: true +- name: assignment_v2 + display_name: Assignment V2 + enabled: false diff --git a/config/routes.rb b/config/routes.rb index 7fb348084..59de0367d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -217,6 +217,13 @@ Rails.application.routes.draw do end end + # Assignment V2 Routes + resources :assignment_policies + + resources :inboxes, only: [] do + resource :assignment_policy, only: [:show, :create, :destroy], controller: 'inbox_assignment_policies' + end + namespace :twitter do resource :authorization, only: [:create] end diff --git a/enterprise/app/models/enterprise/assignment_policy.rb b/enterprise/app/models/enterprise/assignment_policy.rb new file mode 100644 index 000000000..b99b78231 --- /dev/null +++ b/enterprise/app/models/enterprise/assignment_policy.rb @@ -0,0 +1,27 @@ +module Enterprise::AssignmentPolicy + # In enterprise, we extend the enum to include balanced + # However, since Rails enums are frozen after definition, + # we need to handle this differently + + # Override assignment_order= to accept 'balanced' + def assignment_order=(value) + if value.to_s == 'balanced' + write_attribute(:assignment_order, 1) + else + super + end + end + + # Override assignment_order getter to return 'balanced' for value 1 + def assignment_order + value = read_attribute(:assignment_order) + return 'balanced' if value == 1 + + super + end + + # Define balanced? method + def balanced? + self[:assignment_order] == 1 + end +end diff --git a/enterprise/app/services/enterprise/assignment_v2/assignment_service.rb b/enterprise/app/services/enterprise/assignment_v2/assignment_service.rb new file mode 100644 index 000000000..ecbf72845 --- /dev/null +++ b/enterprise/app/services/enterprise/assignment_v2/assignment_service.rb @@ -0,0 +1,10 @@ +module Enterprise::AssignmentV2::AssignmentService + # Override selector_service to use BalancedSelector when appropriate + def selector_service + @selector_service ||= if policy&.balanced? + Enterprise::AssignmentV2::BalancedSelector.new(inbox: inbox) + else + super + end + end +end diff --git a/enterprise/app/services/enterprise/assignment_v2/balanced_selector.rb b/enterprise/app/services/enterprise/assignment_v2/balanced_selector.rb new file mode 100644 index 000000000..0dfce0310 --- /dev/null +++ b/enterprise/app/services/enterprise/assignment_v2/balanced_selector.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Enterprise::AssignmentV2::BalancedSelector + pattr_initialize [:inbox!] + + def select_agent(available_agents) + return nil if available_agents.empty? + + # Get current assignment counts for all available agents + agent_users = available_agents.map(&:user) + assignment_counts = fetch_assignment_counts(agent_users) + + # Find the agent with the least assignments + selected_agent = agent_users.min_by { |user| assignment_counts[user.id] || 0 } + + # Log the selection for debugging + Rails.logger.info "BalancedSelector: Selected agent #{selected_agent.id} with #{assignment_counts[selected_agent.id] || 0} assignments" + + selected_agent + end + + def add_agent_to_queue(user_id) + # No-op for balanced assignment - we don't maintain a queue + end + + def remove_agent_from_queue(user_id) + # No-op for balanced assignment - we don't maintain a queue + end + + def reset_queue + # No-op for balanced assignment - we don't maintain a queue + end + + private + + def fetch_assignment_counts(users) + # Get open conversation counts for each user + user_ids = users.map(&:id) + + # Count open conversations assigned to each user in this inbox + counts = inbox.conversations + .open + .where(assignee_id: user_ids) + .group(:assignee_id) + .count + + # Convert to hash with default value of 0 + Hash.new(0).merge(counts) + end + + def account + @account ||= inbox.account + end +end