diff --git a/db/migrate/20250729140000_create_assignment_policies.rb b/db/migrate/20250729140000_create_assignment_policies.rb index 4bc8a6884..d336fd484 100644 --- a/db/migrate/20250729140000_create_assignment_policies.rb +++ b/db/migrate/20250729140000_create_assignment_policies.rb @@ -18,4 +18,4 @@ class CreateAssignmentPolicies < ActiveRecord::Migration[7.1] add_index :assignment_policies, [:account_id, :name], unique: true, name: 'unique_assignment_policy_name_per_account' add_index :assignment_policies, :enabled, name: 'index_assignment_policies_on_enabled' end -end \ No newline at end of file +end diff --git a/db/migrate/20250729140001_create_inbox_assignment_policies.rb b/db/migrate/20250729140001_create_inbox_assignment_policies.rb index 372440fff..62e14caa0 100644 --- a/db/migrate/20250729140001_create_inbox_assignment_policies.rb +++ b/db/migrate/20250729140001_create_inbox_assignment_policies.rb @@ -11,4 +11,4 @@ class CreateInboxAssignmentPolicies < ActiveRecord::Migration[7.1] add_index :inbox_assignment_policies, :inbox_id, unique: true, name: 'unique_inbox_assignment_policy' end -end \ No newline at end of file +end diff --git a/db/migrate/20250729140002_create_enterprise_agent_capacity_policies.rb b/db/migrate/20250729140002_create_enterprise_agent_capacity_policies.rb index a14556f36..3a1c16903 100644 --- a/db/migrate/20250729140002_create_enterprise_agent_capacity_policies.rb +++ b/db/migrate/20250729140002_create_enterprise_agent_capacity_policies.rb @@ -14,4 +14,4 @@ class CreateEnterpriseAgentCapacityPolicies < ActiveRecord::Migration[7.1] add_index :enterprise_agent_capacity_policies, [:account_id, :name], unique: true, name: 'unique_capacity_policy_name_per_account' add_index :enterprise_agent_capacity_policies, :exclusion_rules, using: :gin, name: 'index_capacity_policies_on_exclusion_rules' end -end \ No newline at end of file +end diff --git a/db/migrate/20250729140003_create_enterprise_inbox_capacity_limits.rb b/db/migrate/20250729140003_create_enterprise_inbox_capacity_limits.rb index 067ef3b31..7b45f80d0 100644 --- a/db/migrate/20250729140003_create_enterprise_inbox_capacity_limits.rb +++ b/db/migrate/20250729140003_create_enterprise_inbox_capacity_limits.rb @@ -12,4 +12,4 @@ class CreateEnterpriseInboxCapacityLimits < ActiveRecord::Migration[7.1] add_index :enterprise_inbox_capacity_limits, [:agent_capacity_policy_id, :inbox_id], unique: true, name: 'unique_policy_inbox_limit' end -end \ No newline at end of file +end diff --git a/db/migrate/20250729140004_add_agent_capacity_policy_to_account_users.rb b/db/migrate/20250729140004_add_agent_capacity_policy_to_account_users.rb index 57c2c4dac..53bc8e8f9 100644 --- a/db/migrate/20250729140004_add_agent_capacity_policy_to_account_users.rb +++ b/db/migrate/20250729140004_add_agent_capacity_policy_to_account_users.rb @@ -4,4 +4,4 @@ class AddAgentCapacityPolicyToAccountUsers < ActiveRecord::Migration[7.1] def change add_reference :account_users, :agent_capacity_policy, null: true, index: true end -end \ No newline at end of file +end diff --git a/db/migrate/20250730000000_create_leaves.rb b/db/migrate/20250730000000_create_leaves.rb index c394e3b6f..64f636ea2 100644 --- a/db/migrate/20250730000000_create_leaves.rb +++ b/db/migrate/20250730000000_create_leaves.rb @@ -12,7 +12,7 @@ class CreateLeaves < ActiveRecord::Migration[7.1] t.text :reason t.references :approved_by t.datetime :approved_at - + t.timestamps end @@ -22,4 +22,4 @@ class CreateLeaves < ActiveRecord::Migration[7.1] add_index :leaves, [:account_user_id, :start_date, :end_date], name: 'index_leaves_on_account_user_and_dates' add_index :leaves, [:account_id, :status], name: 'index_leaves_on_account_and_status' end -end \ No newline at end of file +end diff --git a/enterprise/app/models/enterprise/agent_capacity_policy.rb b/enterprise/app/models/enterprise/agent_capacity_policy.rb index ef772f029..fce610ae4 100644 --- a/enterprise/app/models/enterprise/agent_capacity_policy.rb +++ b/enterprise/app/models/enterprise/agent_capacity_policy.rb @@ -22,116 +22,116 @@ # fk_rails_... (account_id => accounts.id) # -module Enterprise - class AgentCapacityPolicy < ::ApplicationRecord - include AccountCacheRevalidator +class Enterprise::AgentCapacityPolicy < ApplicationRecord + include AccountCacheRevalidator - self.table_name = 'enterprise_agent_capacity_policies' + self.table_name = 'enterprise_agent_capacity_policies' - # Associations - belongs_to :account, class_name: '::Account' - has_many :account_users, class_name: '::AccountUser', dependent: :nullify - has_many :users, through: :account_users, source: :user, class_name: '::User' - has_many :inbox_capacity_limits, dependent: :destroy, class_name: 'Enterprise::InboxCapacityLimit' - has_many :inboxes, through: :inbox_capacity_limits + # Associations + belongs_to :account, class_name: '::Account' + has_many :account_users, class_name: '::AccountUser', dependent: :nullify + has_many :users, through: :account_users, source: :user, class_name: '::User' + has_many :inbox_capacity_limits, dependent: :destroy, class_name: 'Enterprise::InboxCapacityLimit' + has_many :inboxes, through: :inbox_capacity_limits - # Validations - validates :name, presence: true, uniqueness: { scope: :account_id } - validates :name, length: { maximum: 255 } - validates :description, length: { maximum: 1000 } - validate :validate_exclusion_rules_schema + # Validations + validates :name, presence: true, uniqueness: { scope: :account_id } + validates :name, length: { maximum: 255 } + validates :description, length: { maximum: 1000 } + validate :validate_exclusion_rules_schema - # Callbacks - before_save :validate_inbox_access - after_update_commit :invalidate_capacity_caches - after_destroy :invalidate_capacity_caches + # Callbacks + before_save :validate_inbox_access + after_update_commit :invalidate_capacity_caches + after_destroy :invalidate_capacity_caches - # Scopes - scope :with_users, -> { joins(:account_users) } - scope :for_inbox, ->(inbox) { joins(:inbox_capacity_limits).where(enterprise_inbox_capacity_limits: { inbox: inbox }) } + # Scopes + scope :with_users, -> { joins(:account_users) } + scope :for_inbox, ->(inbox) { joins(:inbox_capacity_limits).where(enterprise_inbox_capacity_limits: { inbox: inbox }) } - def add_user(user) - # Find the account_user for this account and user - account_user = account.account_users.find_by!(user: user) + def add_user(user) + # Find the account_user for this account and user + account_user = account.account_users.find_by!(user: user) - # Update the capacity policy reference - account_user.update!(agent_capacity_policy_id: id) - invalidate_user_capacity_cache(user) + # Update the capacity policy reference + account_user.update!(agent_capacity_policy_id: id) + invalidate_user_capacity_cache(user) + end + + def remove_user(user) + account_user = account.account_users.find_by(user: user, agent_capacity_policy_id: id) + account_user&.update!(agent_capacity_policy_id: nil) + invalidate_user_capacity_cache(user) + end + + def set_inbox_limit(inbox, limit) + inbox_capacity_limit = inbox_capacity_limits.find_or_initialize_by(inbox: inbox) + inbox_capacity_limit.conversation_limit = limit + inbox_capacity_limit.save! + + invalidate_inbox_capacity_cache(inbox) + end + + def remove_inbox_limit(inbox) + inbox_capacity_limits.where(inbox: inbox).destroy_all + invalidate_inbox_capacity_cache(inbox) + end + + def get_inbox_limit(inbox) + inbox_capacity_limits.find_by(inbox: inbox)&.conversation_limit + end + + def webhook_data + { + id: id, + name: name, + description: description, + exclusion_rules: exclusion_rules, + users_count: users.count, + inboxes_count: inboxes.count + } + end + + private + + def validate_exclusion_rules_schema + return if exclusion_rules.blank? + + schema = self.class.exclusion_rules_schema + schemer = JSONSchemer.schema(schema) + validation_errors = schemer.validate(exclusion_rules) + + validation_errors.each do |error| + errors.add(:exclusion_rules, error['error']) end + end - def remove_user(user) - account_user = account.account_users.find_by(user: user, agent_capacity_policy_id: id) - account_user&.update!(agent_capacity_policy_id: nil) - invalidate_user_capacity_cache(user) - end + def validate_inbox_access + # Ensure all specified inboxes belong to the same account + invalid_inboxes = inbox_capacity_limits.joins(:inbox) + .where.not(inboxes: { account_id: account_id }) - def set_inbox_limit(inbox, limit) - inbox_capacity_limit = inbox_capacity_limits.find_or_initialize_by(inbox: inbox) - inbox_capacity_limit.conversation_limit = limit - inbox_capacity_limit.save! + return unless invalid_inboxes.exists? - invalidate_inbox_capacity_cache(inbox) - end + errors.add(:inbox_capacity_limits, 'contains inboxes from different accounts') + throw :abort + end - def remove_inbox_limit(inbox) - inbox_capacity_limits.where(inbox: inbox).destroy_all - invalidate_inbox_capacity_cache(inbox) - end + def invalidate_capacity_caches + users.find_each { |user| invalidate_user_capacity_cache(user) } + inboxes.find_each { |inbox| invalidate_inbox_capacity_cache(inbox) } + end - def get_inbox_limit(inbox) - inbox_capacity_limits.find_by(inbox: inbox)&.conversation_limit - end + def invalidate_user_capacity_cache(user) + Rails.cache.delete_matched("assignment_v2:capacity:#{user.id}:*") + end - def webhook_data - { - id: id, - name: name, - description: description, - exclusion_rules: exclusion_rules, - users_count: users.count, - inboxes_count: inboxes.count - } - end + def invalidate_inbox_capacity_cache(inbox) + Rails.cache.delete_matched("assignment_v2:capacity:*:#{inbox.id}") + end - private - - def validate_exclusion_rules_schema - return if exclusion_rules.blank? - - schema = self.class.exclusion_rules_schema - schemer = JSONSchemer.schema(schema) - validation_errors = schemer.validate(exclusion_rules) - - validation_errors.each do |error| - errors.add(:exclusion_rules, error['error']) - end - end - - def validate_inbox_access - # Ensure all specified inboxes belong to the same account - invalid_inboxes = inbox_capacity_limits.joins(:inbox) - .where.not(inboxes: { account_id: account_id }) - - return unless invalid_inboxes.exists? - - errors.add(:inbox_capacity_limits, 'contains inboxes from different accounts') - throw :abort - end - - def invalidate_capacity_caches - users.find_each { |user| invalidate_user_capacity_cache(user) } - inboxes.find_each { |inbox| invalidate_inbox_capacity_cache(inbox) } - end - - def invalidate_user_capacity_cache(user) - Rails.cache.delete_matched("assignment_v2:capacity:#{user.id}:*") - end - - def invalidate_inbox_capacity_cache(inbox) - Rails.cache.delete_matched("assignment_v2:capacity:*:#{inbox.id}") - end - - def self.exclusion_rules_schema + class << self + def exclusion_rules_schema { type: 'object', properties: { diff --git a/enterprise/app/models/enterprise/agent_capacity_policy_user.rb b/enterprise/app/models/enterprise/agent_capacity_policy_user.rb index 586dce94e..a6f9251a3 100644 --- a/enterprise/app/models/enterprise/agent_capacity_policy_user.rb +++ b/enterprise/app/models/enterprise/agent_capacity_policy_user.rb @@ -20,28 +20,25 @@ # fk_rails_... (user_id => users.id) # -module Enterprise - class AgentCapacityPolicyUser < ::ApplicationRecord - self.table_name = 'enterprise_agent_capacity_policy_users' +class Enterprise::AgentCapacityPolicyUser < ApplicationRecord + self.table_name = 'enterprise_agent_capacity_policy_users' - # Associations - belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy' - belongs_to :user, class_name: '::User' + # Associations + belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy' + belongs_to :user, class_name: '::User' - # Validations - validates :user_id, uniqueness: true + # Validations + validates :user_id, uniqueness: true - # Delegations - delegate :account, to: :agent_capacity_policy + # Delegations + delegate :account, to: :agent_capacity_policy - # Callbacks - after_create_commit :invalidate_user_cache - after_destroy_commit :invalidate_user_cache + # Callbacks + after_commit :invalidate_user_cache - private + private - def invalidate_user_cache - Rails.cache.delete_matched("assignment_v2:capacity:#{user_id}:*") - end + def invalidate_user_cache + Rails.cache.delete_matched("assignment_v2:capacity:#{user_id}:*") end end diff --git a/enterprise/app/models/enterprise/inbox_capacity_limit.rb b/enterprise/app/models/enterprise/inbox_capacity_limit.rb index 61938fb3a..ae12ebed6 100644 --- a/enterprise/app/models/enterprise/inbox_capacity_limit.rb +++ b/enterprise/app/models/enterprise/inbox_capacity_limit.rb @@ -23,48 +23,44 @@ # fk_rails_... (inbox_id => inboxes.id) # -module Enterprise - class InboxCapacityLimit < ::ApplicationRecord - include AccountCacheRevalidator +class Enterprise::InboxCapacityLimit < ApplicationRecord + include AccountCacheRevalidator - self.table_name = 'enterprise_inbox_capacity_limits' + self.table_name = 'enterprise_inbox_capacity_limits' - # Associations - belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy' - belongs_to :inbox, class_name: '::Inbox' + # Associations + belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy' + belongs_to :inbox, class_name: '::Inbox' - # Validations - validates :agent_capacity_policy_id, uniqueness: { scope: :inbox_id } - validates :conversation_limit, presence: true, numericality: { greater_than: 0, less_than_or_equal_to: 1000 } + # Validations + validates :agent_capacity_policy_id, uniqueness: { scope: :inbox_id } + validates :conversation_limit, presence: true, numericality: { greater_than: 0, less_than_or_equal_to: 1000 } - # Delegations - delegate :account, to: :agent_capacity_policy - delegate :name, :description, :exclusion_rules, to: :agent_capacity_policy, prefix: :policy + # Delegations + delegate :account, to: :agent_capacity_policy + delegate :name, :description, :exclusion_rules, to: :agent_capacity_policy, prefix: :policy - # Callbacks - after_create_commit :invalidate_inbox_cache - after_update_commit :invalidate_inbox_cache - after_destroy_commit :invalidate_inbox_cache + # Callbacks + after_commit :invalidate_inbox_cache - # Scopes - scope :for_inbox, ->(inbox) { where(inbox: inbox) } - scope :for_policy, ->(policy) { where(agent_capacity_policy: policy) } + # Scopes + scope :for_inbox, ->(inbox) { where(inbox: inbox) } + scope :for_policy, ->(policy) { where(agent_capacity_policy: policy) } - def webhook_data - { - id: id, - inbox_id: inbox_id, - agent_capacity_policy_id: agent_capacity_policy_id, - conversation_limit: conversation_limit, - policy: agent_capacity_policy.webhook_data - } - end + def webhook_data + { + id: id, + inbox_id: inbox_id, + agent_capacity_policy_id: agent_capacity_policy_id, + conversation_limit: conversation_limit, + policy: agent_capacity_policy.webhook_data + } + end - private + private - def invalidate_inbox_cache - Rails.cache.delete_matched("assignment_v2:capacity:*:#{inbox_id}") - update_account_cache - end + def invalidate_inbox_cache + Rails.cache.delete_matched("assignment_v2:capacity:*:#{inbox_id}") + update_account_cache end end diff --git a/enterprise/app/policies/enterprise/agent_capacity_policy_policy.rb b/enterprise/app/policies/enterprise/agent_capacity_policy_policy.rb index 5fe4650e3..f87d29afe 100644 --- a/enterprise/app/policies/enterprise/agent_capacity_policy_policy.rb +++ b/enterprise/app/policies/enterprise/agent_capacity_policy_policy.rb @@ -1,45 +1,43 @@ # frozen_string_literal: true -module Enterprise - class AgentCapacityPolicyPolicy < ApplicationPolicy - def index? - @account_user.administrator? - end - - def show? - @account_user.administrator? - end - - def create? - @account_user.administrator? - end - - def update? - @account_user.administrator? - end - - def destroy? - @account_user.administrator? - end - - def set_inbox_limit? - @account_user.administrator? - end - - def remove_inbox_limit? - @account_user.administrator? - end - - def assign_user? - @account_user.administrator? - end - - def remove_user? - @account_user.administrator? - end - - def agent_capacity? - @account_user.administrator? || @account_user.agent? - end +class Enterprise::AgentCapacityPolicyPolicy < ApplicationPolicy + def index? + @account_user.administrator? end -end \ No newline at end of file + + def show? + @account_user.administrator? + end + + def create? + @account_user.administrator? + end + + def update? + @account_user.administrator? + end + + def destroy? + @account_user.administrator? + end + + def set_inbox_limit? + @account_user.administrator? + end + + def remove_inbox_limit? + @account_user.administrator? + end + + def assign_user? + @account_user.administrator? + end + + def remove_user? + @account_user.administrator? + end + + def agent_capacity? + @account_user.administrator? || @account_user.agent? + end +end diff --git a/enterprise/app/services/assignment_v2/balanced_selector.rb b/enterprise/app/services/assignment_v2/balanced_selector.rb index f7cd656b1..3fce1cbf2 100644 --- a/enterprise/app/services/assignment_v2/balanced_selector.rb +++ b/enterprise/app/services/assignment_v2/balanced_selector.rb @@ -1,42 +1,40 @@ # frozen_string_literal: true -module Enterprise - class AssignmentV2::BalancedSelector - pattr_initialize [:inbox!] +class Enterprise::AssignmentV2::BalancedSelector + pattr_initialize [:inbox!] - def select_agent(available_agents) - return nil if available_agents.empty? + def select_agent(available_agents) + return nil if available_agents.empty? - # Since agents are already filtered by capacity, we can compute workload distribution - agents_with_workload = compute_agent_workloads(available_agents) - return nil if agents_with_workload.empty? + # Since agents are already filtered by capacity, we can compute workload distribution + agents_with_workload = compute_agent_workloads(available_agents) + return nil if agents_with_workload.empty? - # Select agent with lowest current workload - best_agent_data = agents_with_workload.min_by { |data| data[:current_assignments] } - best_agent_data[:agent] - rescue StandardError => e - Rails.logger.error "AssignmentV2: Balanced selection failed: #{e.message}" - # Fallback to simple selection - available_agents.first&.user - end + # Select agent with lowest current workload + best_agent_data = agents_with_workload.min_by { |data| data[:current_assignments] } + best_agent_data[:agent] + rescue StandardError => e + Rails.logger.error "AssignmentV2: Balanced selection failed: #{e.message}" + # Fallback to simple selection + available_agents.first&.user + end - private + private - def compute_agent_workloads(available_agents) - available_agents.map do |inbox_member| - agent = inbox_member.user - - # Count current assignments - current_assignments = agent.assigned_conversations - .where(inbox: inbox) - .open - .count + def compute_agent_workloads(available_agents) + available_agents.map do |inbox_member| + agent = inbox_member.user - { - agent: agent, - current_assignments: current_assignments - } - end + # Count current assignments + current_assignments = agent.assigned_conversations + .where(inbox: inbox) + .open + .count + + { + agent: agent, + current_assignments: current_assignments + } end end -end \ No newline at end of file +end diff --git a/enterprise/app/services/assignment_v2/capacity_service.rb b/enterprise/app/services/assignment_v2/capacity_service.rb index d8bfbbffd..d3756e431 100644 --- a/enterprise/app/services/assignment_v2/capacity_service.rb +++ b/enterprise/app/services/assignment_v2/capacity_service.rb @@ -1,55 +1,58 @@ # frozen_string_literal: true -module Enterprise - class AssignmentV2::CapacityService - pattr_initialize [:inbox!] +class Enterprise::AssignmentV2::CapacityService + pattr_initialize [:inbox!] - def filter_agents_by_capacity(inbox_members) - inbox_members.select do |inbox_member| - agent = inbox_member.user - has_available_capacity?(agent) - end - end - - def get_agent_capacity(agent) - account_user = agent.account_users.find_by(account: inbox.account) - policy = account_user&.agent_capacity_policy - - unless policy - return { - total_capacity: Float::INFINITY, - current_assignments: 0, - available_capacity: Float::INFINITY - } - end - - inbox_limit = policy.inbox_capacity_limits.find_by(inbox: inbox) - - unless inbox_limit - return { - total_capacity: Float::INFINITY, - current_assignments: 0, - available_capacity: Float::INFINITY - } - end - - current_count = agent.assigned_conversations - .where(inbox: inbox) - .open - .count - - { - total_capacity: inbox_limit.conversation_limit, - current_assignments: current_count, - available_capacity: inbox_limit.conversation_limit - current_count - } - end - - private - - def has_available_capacity?(agent) - capacity_data = get_agent_capacity(agent) - capacity_data[:available_capacity].positive? + def filter_agents_by_capacity(inbox_members) + inbox_members.select do |inbox_member| + agent = inbox_member.user + available_capacity?(agent) end end -end \ No newline at end of file + + def get_agent_capacity(agent) + inbox_limit = get_inbox_limit_for_agent(agent) + return unlimited_capacity unless inbox_limit + + current_count = count_current_assignments(agent) + build_capacity_data(inbox_limit, current_count) + end + + private + + def get_inbox_limit_for_agent(agent) + account_user = agent.account_users.find_by(account: inbox.account) + policy = account_user&.agent_capacity_policy + return nil unless policy + + policy.inbox_capacity_limits.find_by(inbox: inbox) + end + + def count_current_assignments(agent) + agent.assigned_conversations + .where(inbox: inbox) + .open + .count + end + + def build_capacity_data(inbox_limit, current_count) + { + total_capacity: inbox_limit.conversation_limit, + current_assignments: current_count, + available_capacity: inbox_limit.conversation_limit - current_count + } + end + + def unlimited_capacity + { + total_capacity: Float::INFINITY, + current_assignments: 0, + available_capacity: Float::INFINITY + } + end + + def available_capacity?(agent) + capacity_data = get_agent_capacity(agent) + capacity_data[:available_capacity].positive? + end +end diff --git a/enterprise/app/services/enterprise/assignment_v2/capacity_service.rb b/enterprise/app/services/enterprise/assignment_v2/capacity_service.rb index dbf848c48..c2863f40f 100644 --- a/enterprise/app/services/enterprise/assignment_v2/capacity_service.rb +++ b/enterprise/app/services/enterprise/assignment_v2/capacity_service.rb @@ -1,175 +1,187 @@ # frozen_string_literal: true -module Enterprise - module AssignmentV2 - class CapacityService - def initialize - @cache_ttl = 5.minutes - end +class Enterprise::AssignmentV2::CapacityService + def initialize + @cache_ttl = 5.minutes + end - # Get agent's current capacity status for specific inbox - def get_agent_capacity(agent, inbox) - cache_key = capacity_cache_key(agent, inbox) + # Get agent's current capacity status for specific inbox + def get_agent_capacity(agent, inbox) + cache_key = capacity_cache_key(agent, inbox) - cached = Redis::Alfred.hgetall(cache_key) - return parse_cached_capacity(cached) if cached.present? + cached = Redis::Alfred.hgetall(cache_key) + return parse_cached_capacity(cached) if cached.present? - # Cache miss - compute from database - capacity = compute_agent_capacity(agent, inbox) - cache_capacity_data(cache_key, capacity) - capacity - end + # Cache miss - compute from database + capacity = compute_agent_capacity(agent, inbox) + cache_capacity_data(cache_key, capacity) + capacity + end - # Get agent's overall capacity across all inboxes - def get_agent_overall_capacity(agent) - account = agent.accounts.first # Assuming we're working within account context - policy = get_agent_capacity_policy(agent, account) - - return unlimited_capacity_summary unless policy + # Get agent's overall capacity across all inboxes + def get_agent_overall_capacity(agent) + account = agent.accounts.first # Assuming we're working within account context + policy = get_agent_capacity_policy(agent, account) - inboxes_data = [] - total_current = 0 - total_limit = 0 + return unlimited_capacity_summary unless policy - policy.inbox_capacity_limits.includes(:inbox).each do |inbox_limit| - inbox = inbox_limit.inbox - current = count_current_assignments(agent, inbox, policy) - limit = inbox_limit.conversation_limit + capacity_data = build_capacity_data_for_policy(agent, policy) + build_overall_capacity_response(policy, capacity_data) + end - total_current += current - total_limit += limit + private - inboxes_data << { - inbox_id: inbox.id, - inbox_name: inbox.name, - current_assignments: current, - conversation_limit: limit, - available_capacity: [limit - current, 0].max - } - end + def build_capacity_data_for_policy(agent, policy) + inboxes_data = [] + total_current = 0 + total_limit = 0 - { - policy_id: policy.id, - policy_name: policy.name, - total_current_assignments: total_current, - total_conversation_limit: total_limit, - total_available_capacity: [total_limit - total_current, 0].max, - exclusion_rules: policy.exclusion_rules, - inboxes: inboxes_data - } - end + policy.inbox_capacity_limits.includes(:inbox).each do |inbox_limit| + inbox_data = build_inbox_capacity_data(agent, inbox_limit, policy) - private + total_current += inbox_data[:current_assignments] + total_limit += inbox_data[:conversation_limit] + inboxes_data << inbox_data + end - def compute_agent_capacity(agent, inbox) - account = inbox.account - policy = get_agent_capacity_policy(agent, account) - - return unlimited_capacity if policy.nil? + { + inboxes: inboxes_data, + total_current: total_current, + total_limit: total_limit + } + end - inbox_limit = policy.inbox_capacity_limits.find_by(inbox: inbox) - return unlimited_capacity if inbox_limit.nil? + def build_inbox_capacity_data(agent, inbox_limit, policy) + inbox = inbox_limit.inbox + current = count_current_assignments(agent, inbox, policy) + limit = inbox_limit.conversation_limit - current_assignments = count_current_assignments(agent, inbox, policy) + { + inbox_id: inbox.id, + inbox_name: inbox.name, + current_assignments: current, + conversation_limit: limit, + available_capacity: [limit - current, 0].max + } + end - { - total_capacity: inbox_limit.conversation_limit, - current_assignments: current_assignments, - available_capacity: [inbox_limit.conversation_limit - current_assignments, 0].max, - policy_id: policy.id, - policy_name: policy.name, - exclusion_rules: policy.exclusion_rules - } - end + def build_overall_capacity_response(policy, capacity_data) + { + policy_id: policy.id, + policy_name: policy.name, + total_current_assignments: capacity_data[:total_current], + total_conversation_limit: capacity_data[:total_limit], + total_available_capacity: [capacity_data[:total_limit] - capacity_data[:total_current], 0].max, + exclusion_rules: policy.exclusion_rules, + inboxes: capacity_data[:inboxes] + } + end - def get_agent_capacity_policy(agent, account) - account_user = account.account_users.find_by(user: agent) - return nil unless account_user&.agent_capacity_policy_id + def compute_agent_capacity(agent, inbox) + account = inbox.account + policy = get_agent_capacity_policy(agent, account) - Enterprise::AgentCapacityPolicy.find_by(id: account_user.agent_capacity_policy_id) - end + return unlimited_capacity if policy.nil? - def count_current_assignments(agent, inbox, policy) - scope = agent.assigned_conversations - .where(inbox: inbox, status: 'open') + inbox_limit = policy.inbox_capacity_limits.find_by(inbox: inbox) + return unlimited_capacity if inbox_limit.nil? - # Apply exclusion rules from policy - scope = apply_exclusion_rules(scope, policy) - scope.count - end + current_assignments = count_current_assignments(agent, inbox, policy) - def apply_exclusion_rules(scope, policy) - rules = policy.exclusion_rules || {} + { + total_capacity: inbox_limit.conversation_limit, + current_assignments: current_assignments, + available_capacity: [inbox_limit.conversation_limit - current_assignments, 0].max, + policy_id: policy.id, + policy_name: policy.name, + exclusion_rules: policy.exclusion_rules + } + end - # Exclude conversations with specific labels - if rules['labels'].present? - scope = scope.where.not(id: - ConversationLabel.joins(:label) - .where(labels: { title: rules['labels'] }) - .select(:conversation_id) - ) - end + def get_agent_capacity_policy(agent, account) + account_user = account.account_users.find_by(user: agent) + return nil unless account_user&.agent_capacity_policy_id - # Exclude conversations older than X hours - if rules['hours_threshold'].present? - cutoff = rules['hours_threshold'].hours.ago - scope = scope.where('conversations.created_at > ?', cutoff) - end + Enterprise::AgentCapacityPolicy.find_by(id: account_user.agent_capacity_policy_id) + end - scope - end + def count_current_assignments(agent, inbox, policy) + scope = agent.assigned_conversations + .where(inbox: inbox, status: 'open') - def unlimited_capacity - { - total_capacity: Float::INFINITY, - current_assignments: 0, - available_capacity: Float::INFINITY, - policy_id: nil, - policy_name: 'No capacity policy', - exclusion_rules: {} - } - end + # Apply exclusion rules from policy + scope = apply_exclusion_rules(scope, policy) + scope.count + end - def unlimited_capacity_summary - { - policy_id: nil, - policy_name: 'No capacity policy', - total_current_assignments: 0, - total_conversation_limit: Float::INFINITY, - total_available_capacity: Float::INFINITY, - exclusion_rules: {}, - inboxes: [] - } - end + def apply_exclusion_rules(scope, policy) + rules = policy.exclusion_rules || {} - def parse_cached_capacity(cached) - { - total_capacity: cached['total_capacity'].to_i, - current_assignments: cached['current_assignments'].to_i, - available_capacity: cached['available_capacity'].to_i, - policy_id: cached['policy_id'].presence&.to_i, - policy_name: cached['policy_name'] || 'No capacity policy', - exclusion_rules: JSON.parse(cached['exclusion_rules'] || '{}') - } - end + # Exclude conversations with specific labels + if rules['labels'].present? + scope = scope.where.not(id: + ConversationLabel.joins(:label) + .where(labels: { title: rules['labels'] }) + .select(:conversation_id)) + end - def cache_capacity_data(cache_key, capacity) - Redis::Alfred.multi do |multi| - multi.hset(cache_key, - 'total_capacity', capacity[:total_capacity], - 'current_assignments', capacity[:current_assignments], - 'available_capacity', capacity[:available_capacity], - 'policy_id', capacity[:policy_id], - 'policy_name', capacity[:policy_name], - 'exclusion_rules', capacity[:exclusion_rules].to_json - ) - multi.expire(cache_key, @cache_ttl) - end - end + # Exclude conversations older than X hours + if rules['hours_threshold'].present? + cutoff = rules['hours_threshold'].hours.ago + scope = scope.where('conversations.created_at > ?', cutoff) + end - def capacity_cache_key(agent, inbox) - "assignment_v2:capacity:#{agent.id}:#{inbox.id}" - end + scope + end + + def unlimited_capacity + { + total_capacity: Float::INFINITY, + current_assignments: 0, + available_capacity: Float::INFINITY, + policy_id: nil, + policy_name: 'No capacity policy', + exclusion_rules: {} + } + end + + def unlimited_capacity_summary + { + policy_id: nil, + policy_name: 'No capacity policy', + total_current_assignments: 0, + total_conversation_limit: Float::INFINITY, + total_available_capacity: Float::INFINITY, + exclusion_rules: {}, + inboxes: [] + } + end + + def parse_cached_capacity(cached) + { + total_capacity: cached['total_capacity'].to_i, + current_assignments: cached['current_assignments'].to_i, + available_capacity: cached['available_capacity'].to_i, + policy_id: cached['policy_id'].presence&.to_i, + policy_name: cached['policy_name'] || 'No capacity policy', + exclusion_rules: JSON.parse(cached['exclusion_rules'] || '{}') + } + end + + def cache_capacity_data(cache_key, capacity) + Redis::Alfred.multi do |multi| + multi.hset(cache_key, + 'total_capacity', capacity[:total_capacity], + 'current_assignments', capacity[:current_assignments], + 'available_capacity', capacity[:available_capacity], + 'policy_id', capacity[:policy_id], + 'policy_name', capacity[:policy_name], + 'exclusion_rules', capacity[:exclusion_rules].to_json) + multi.expire(cache_key, @cache_ttl) end end -end \ No newline at end of file + + def capacity_cache_key(agent, inbox) + "assignment_v2:capacity:#{agent.id}:#{inbox.id}" + end +end diff --git a/lib/tasks/assignment_v2.rake b/lib/tasks/assignment_v2.rake index 9799575f2..bba23477e 100644 --- a/lib/tasks/assignment_v2.rake +++ b/lib/tasks/assignment_v2.rake @@ -1,10 +1,11 @@ # frozen_string_literal: true +# rubocop:disable Metrics/BlockLength namespace :assignment_v2 do desc 'Enable Assignment V2 for an account with default policy' task :enable, [:account_id] => :environment do |_task, args| account = Account.find(args[:account_id]) - + # Create default assignment policy if not exists policy = account.assignment_policies.find_or_create_by!(name: 'Default Policy') do |p| p.description = 'Default round-robin assignment policy' @@ -19,24 +20,26 @@ namespace :assignment_v2 do desc 'Disable Assignment V2 for an account' task :disable, [:account_id] => :environment do |_task, args| account = Account.find(args[:account_id]) - + # Disable all assignment policies - account.assignment_policies.update_all(enabled: false) - + account.assignment_policies.find_each do |policy| + policy.update!(enabled: false) + end + puts "Assignment V2 disabled for account #{account.name}" end desc 'Run assignment for all enabled inboxes' task run_all: :environment do count = 0 - + Inbox.joins(:assignment_policy) .where(assignment_policies: { enabled: true }) .find_each do |inbox| AssignmentV2::AssignmentJob.perform_later(inbox_id: inbox.id) count += 1 end - + puts "Queued assignment jobs for #{count} inboxes" end @@ -44,18 +47,19 @@ namespace :assignment_v2 do task status: :environment do puts 'Assignment V2 Status' puts '===================' - + total_policies = AssignmentPolicy.count enabled_policies = AssignmentPolicy.enabled.count - + puts "Total Assignment Policies: #{total_policies}" puts "Enabled Policies: #{enabled_policies}" puts - + inboxes_with_v2 = Inbox.joins(:assignment_policy).count total_inboxes = Inbox.count - + puts "Total Inboxes: #{total_inboxes}" puts "Inboxes with Assignment V2: #{inboxes_with_v2}" end -end \ No newline at end of file +end +# rubocop:enable Metrics/BlockLength diff --git a/lib/tasks/test_assignment_comprehensive.rake b/lib/tasks/test_assignment_comprehensive.rake deleted file mode 100644 index 87e6fda51..000000000 --- a/lib/tasks/test_assignment_comprehensive.rake +++ /dev/null @@ -1,762 +0,0 @@ -namespace :test do - desc "Generate comprehensive assignment test data with multiple scenarios" - task generate_comprehensive_assignment_data: :environment do - puts "๐Ÿš€ Generating comprehensive test data for assignment functionality..." - - account = Account.first - unless account - puts "โŒ No account found. Please create an account first." - exit - end - - # Note about Assignment V2 - puts "\n๐Ÿ“ Note: Assignment V2 requires configuration at the system level." - puts " For this test, we'll demonstrate policy behavior with direct assignment." - - # Clear existing test data first - puts "\n๐Ÿงน Clearing existing test data..." - clear_test_data(account) - - inbox = account.inboxes.first - unless inbox - puts "โŒ No inbox found. Please create an inbox first." - exit - end - - puts "\n๐Ÿ“Š Current Setup:" - puts "Account: #{account.name} (ID: #{account.id})" - puts "Inbox: #{inbox.name} (ID: #{inbox.id})" - puts "Channel Type: #{inbox.channel_type}" - puts "-" * 50 - - # Step 1: Create more test agents - puts "\n๐Ÿ‘ฅ Setting up agents..." - agents = ensure_comprehensive_test_agents(account) - - # Step 2: Add all agents to the inbox - puts "\n๐Ÿ“ฅ Adding agents to inbox..." - add_agents_to_inbox(inbox, agents) - - # Step 3: Create comprehensive assignment policies - puts "\n๐Ÿ“‹ Creating assignment policies..." - policies = create_comprehensive_assignment_policies(account, inbox) - - # Step 4: Create agent capacity policies - puts "\nโš–๏ธ Creating agent capacity policies..." - capacity_policies = create_comprehensive_capacity_policies(account, inbox, agents) - - # Step 5: Create test conversations with various scenarios - puts "\n๐Ÿ’ฌ Creating test conversations..." - conversations = create_scenario_based_conversations(account, inbox) - - # Step 6: Assign some conversations to test capacity - puts "\n๐Ÿ”„ Assigning some conversations to test capacity..." - assign_test_conversations(conversations, agents) - - puts "\nโœ… Test data generation complete!" - puts "\n๐Ÿ“ˆ Summary:" - puts "- Agents: #{agents.count}" - puts "- Agents in Inbox: #{inbox.inbox_members.count}" - puts "- Assignment Policies: #{policies.count}" - puts "- Capacity Policies: #{capacity_policies.count}" - puts "- Total Conversations: #{conversations.count}" - puts "- Unassigned Conversations: #{inbox.conversations.unassigned.count}" - puts "- Assigned Conversations: #{inbox.conversations.assigned.count}" - end - - def ensure_comprehensive_test_agents(account) - agents = [] - - # Create test agents with different roles and experience levels - test_agents_data = [ - # Junior Agents - { name: "Alice Johnson", email: "alice@test.com", role: "agent", level: "junior" }, - { name: "Bob Smith", email: "bob@test.com", role: "agent", level: "junior" }, - { name: "Charlie Davis", email: "charlie@test.com", role: "agent", level: "junior" }, - - # Mid-level Agents - { name: "Diana Wilson", email: "diana@test.com", role: "agent", level: "mid" }, - { name: "Eve Martinez", email: "eve@test.com", role: "agent", level: "mid" }, - { name: "Frank Brown", email: "frank@test.com", role: "agent", level: "mid" }, - - # Senior Agents - { name: "Grace Lee", email: "grace@test.com", role: "agent", level: "senior" }, - { name: "Henry Chen", email: "henry@test.com", role: "agent", level: "senior" }, - - # Specialist Agents - { name: "Iris Kumar", email: "iris@test.com", role: "agent", level: "specialist" }, - { name: "Jack Wilson", email: "jack@test.com", role: "agent", level: "specialist" }, - - # Team Leads - { name: "Karen Miller", email: "karen@test.com", role: "administrator", level: "lead" }, - { name: "Leo Garcia", email: "leo@test.com", role: "administrator", level: "lead" } - ] - - test_agents_data.each do |agent_data| - user = User.find_or_create_by!(email: agent_data[:email]) do |u| - u.name = agent_data[:name] - u.password = "Password123!" - u.password_confirmation = "Password123!" - end - - # Update custom attributes - user.custom_attributes ||= {} - user.custom_attributes['level'] = agent_data[:level] - user.custom_attributes['test_agent'] = true - user.save! - - # Add to account if not already added - account_user = account.account_users.find_or_create_by!(user: user) do |au| - au.role = agent_data[:role] - end - - # Update role if changed - if account_user.role != agent_data[:role] - account_user.update!(role: agent_data[:role]) - end - - agents << user - puts " โœ“ Agent: #{user.name} (#{agent_data[:level]})" - end - - agents - end - - def add_agents_to_inbox(inbox, agents) - agents.each do |agent| - inbox_member = inbox.inbox_members.find_or_create_by!(user: agent) - puts " โœ“ Added #{agent.name} to #{inbox.name}" - end - end - - def create_comprehensive_assignment_policies(account, inbox) - policies = [] - - # Policy 1: Standard Round Robin - policy1 = account.assignment_policies.find_or_create_by!( - name: "Standard Round Robin" - ) do |p| - p.description = "Basic round-robin distribution for all conversations" - p.assignment_order = "round_robin" - p.conversation_priority = "longest_waiting" - p.enabled = true - p.fair_distribution_limit = 10 - p.fair_distribution_window = 3600 - end - policies << policy1 - puts " โœ“ Policy: #{policy1.name}" - - # Policy 2: Priority First Response - policy2 = account.assignment_policies.find_or_create_by!( - name: "Priority First Response" - ) do |p| - p.description = "Prioritizes earliest created conversations with tight limits" - p.assignment_order = "round_robin" - p.conversation_priority = "earliest_created" - p.enabled = true - p.fair_distribution_limit = 3 - p.fair_distribution_window = 900 # 15 minutes - end - policies << policy2 - puts " โœ“ Policy: #{policy2.name}" - - # Policy 3: High Volume Support - policy3 = account.assignment_policies.find_or_create_by!( - name: "High Volume Support" - ) do |p| - p.description = "Handles high volume with generous limits" - p.assignment_order = "round_robin" - p.conversation_priority = "longest_waiting" - p.enabled = true - p.fair_distribution_limit = 30 - p.fair_distribution_window = 7200 # 2 hours - end - policies << policy3 - puts " โœ“ Policy: #{policy3.name}" - - # Policy 4: Burst Traffic Handler - policy4 = account.assignment_policies.find_or_create_by!( - name: "Burst Traffic Handler" - ) do |p| - p.description = "Handles sudden traffic bursts with very high limits" - p.assignment_order = "round_robin" - p.conversation_priority = "earliest_created" - p.enabled = false - p.fair_distribution_limit = 50 - p.fair_distribution_window = 3600 - end - policies << policy4 - puts " โœ“ Policy: #{policy4.name}" - - # Policy 5: Weekend Skeleton Crew - policy5 = account.assignment_policies.find_or_create_by!( - name: "Weekend Skeleton Crew" - ) do |p| - p.description = "Conservative assignment for limited weekend staff" - p.assignment_order = "round_robin" - p.conversation_priority = "longest_waiting" - p.enabled = false - p.fair_distribution_limit = 5 - p.fair_distribution_window = 1800 # 30 minutes - end - policies << policy5 - puts " โœ“ Policy: #{policy5.name}" - - # Policy 6: Night Shift Policy - policy6 = account.assignment_policies.find_or_create_by!( - name: "Night Shift Policy" - ) do |p| - p.description = "Balanced assignment for night shift operations" - p.assignment_order = "round_robin" - p.conversation_priority = "earliest_created" - p.enabled = false - p.fair_distribution_limit = 15 - p.fair_distribution_window = 3600 - end - policies << policy6 - puts " โœ“ Policy: #{policy6.name}" - - # Policy 7: Training Mode - policy7 = account.assignment_policies.find_or_create_by!( - name: "Training Mode" - ) do |p| - p.description = "Limited assignment for agents in training" - p.assignment_order = "round_robin" - p.conversation_priority = "longest_waiting" - p.enabled = false - p.fair_distribution_limit = 2 - p.fair_distribution_window = 1800 - end - policies << policy7 - puts " โœ“ Policy: #{policy7.name}" - - # Policy 8: Peak Hours Policy - policy8 = account.assignment_policies.find_or_create_by!( - name: "Peak Hours Policy" - ) do |p| - p.description = "Optimized for peak business hours" - p.assignment_order = "round_robin" - p.conversation_priority = "earliest_created" - p.enabled = false - p.fair_distribution_limit = 20 - p.fair_distribution_window = 2700 # 45 minutes - end - policies << policy8 - puts " โœ“ Policy: #{policy8.name}" - - # Associate first enabled policy with inbox - enabled_policy = policies.find(&:enabled) - if enabled_policy - if inbox.inbox_assignment_policy - inbox.inbox_assignment_policy.update!(assignment_policy: enabled_policy) - else - InboxAssignmentPolicy.create!( - inbox: inbox, - assignment_policy: enabled_policy - ) - end - puts " โœ“ Associated #{enabled_policy.name} with #{inbox.name}" - end - - policies - end - - def create_comprehensive_capacity_policies(account, inbox, agents) - return [] unless defined?(Enterprise::AgentCapacityPolicy) - - policies = [] - - # Group agents by level - junior_agents = agents.select { |a| a.custom_attributes&.dig('level') == 'junior' } - mid_agents = agents.select { |a| a.custom_attributes&.dig('level') == 'mid' } - senior_agents = agents.select { |a| a.custom_attributes&.dig('level') == 'senior' } - specialist_agents = agents.select { |a| a.custom_attributes&.dig('level') == 'specialist' } - lead_agents = agents.select { |a| a.custom_attributes&.dig('level') == 'lead' } - - # Capacity Policy 1: Junior Agent Training - policy1 = Enterprise::AgentCapacityPolicy.find_or_create_by!( - account: account, - name: "Junior Agent Training Capacity" - ) do |p| - p.description = "Very limited capacity for agents in training (3 conversations max)" - p.exclusion_rules = { - labels: ["training", "complex", "escalated", "vip"], - hours_threshold: 72 - } - end - - # Assign junior agents - junior_agents.each do |agent| - Enterprise::AgentCapacityPolicyUser.find_or_create_by!( - agent_capacity_policy: policy1, - user: agent - ) - end - - # Set inbox limit - Enterprise::InboxCapacityLimit.find_or_create_by!( - agent_capacity_policy: policy1, - inbox: inbox - ) do |limit| - limit.conversation_limit = 3 - end - - policies << policy1 - puts " โœ“ Capacity Policy: #{policy1.name} (#{junior_agents.count} agents, limit: 3)" - - # Capacity Policy 2: Standard Agent Capacity - policy2 = Enterprise::AgentCapacityPolicy.find_or_create_by!( - account: account, - name: "Standard Agent Capacity" - ) do |p| - p.description = "Standard capacity for regular agents (10 conversations max)" - p.exclusion_rules = { - labels: ["escalated", "executive"], - hours_threshold: 48 - } - end - - # Assign mid-level agents - mid_agents.each do |agent| - Enterprise::AgentCapacityPolicyUser.find_or_create_by!( - agent_capacity_policy: policy2, - user: agent - ) - end - - # Set inbox limit - Enterprise::InboxCapacityLimit.find_or_create_by!( - agent_capacity_policy: policy2, - inbox: inbox - ) do |limit| - limit.conversation_limit = 10 - end - - policies << policy2 - puts " โœ“ Capacity Policy: #{policy2.name} (#{mid_agents.count} agents, limit: 10)" - - # Capacity Policy 3: Senior Agent Capacity - policy3 = Enterprise::AgentCapacityPolicy.find_or_create_by!( - account: account, - name: "Senior Agent Capacity" - ) do |p| - p.description = "Higher capacity for experienced agents (20 conversations max)" - p.exclusion_rules = { - labels: ["executive"], - hours_threshold: 24 - } - end - - # Assign senior agents - senior_agents.each do |agent| - Enterprise::AgentCapacityPolicyUser.find_or_create_by!( - agent_capacity_policy: policy3, - user: agent - ) - end - - # Set inbox limit - Enterprise::InboxCapacityLimit.find_or_create_by!( - agent_capacity_policy: policy3, - inbox: inbox - ) do |limit| - limit.conversation_limit = 20 - end - - policies << policy3 - puts " โœ“ Capacity Policy: #{policy3.name} (#{senior_agents.count} agents, limit: 20)" - - # Capacity Policy 4: Specialist Capacity - policy4 = Enterprise::AgentCapacityPolicy.find_or_create_by!( - account: account, - name: "Technical Specialist Capacity" - ) do |p| - p.description = "Moderate capacity for technical specialists (15 conversations max)" - p.exclusion_rules = { - labels: [], - hours_threshold: 12 - } - end - - # Assign specialist agents - specialist_agents.each do |agent| - Enterprise::AgentCapacityPolicyUser.find_or_create_by!( - agent_capacity_policy: policy4, - user: agent - ) - end - - # Set inbox limit - Enterprise::InboxCapacityLimit.find_or_create_by!( - agent_capacity_policy: policy4, - inbox: inbox - ) do |limit| - limit.conversation_limit = 15 - end - - policies << policy4 - puts " โœ“ Capacity Policy: #{policy4.name} (#{specialist_agents.count} agents, limit: 15)" - - # Capacity Policy 5: Team Lead Capacity - policy5 = Enterprise::AgentCapacityPolicy.find_or_create_by!( - account: account, - name: "Team Lead Capacity" - ) do |p| - p.description = "Limited capacity for team leads who also manage (5 conversations max)" - p.exclusion_rules = { - labels: [], - hours_threshold: 6 - } - end - - # Assign lead agents - lead_agents.each do |agent| - Enterprise::AgentCapacityPolicyUser.find_or_create_by!( - agent_capacity_policy: policy5, - user: agent - ) - end - - # Set inbox limit - Enterprise::InboxCapacityLimit.find_or_create_by!( - agent_capacity_policy: policy5, - inbox: inbox - ) do |limit| - limit.conversation_limit = 5 - end - - policies << policy5 - puts " โœ“ Capacity Policy: #{policy5.name} (#{lead_agents.count} agents, limit: 5)" - - # Capacity Policy 6: Weekend Coverage - policy6 = Enterprise::AgentCapacityPolicy.find_or_create_by!( - account: account, - name: "Weekend Coverage Capacity" - ) do |p| - p.description = "Increased capacity for weekend skeleton crew (25 conversations max)" - p.exclusion_rules = { - labels: ["scheduled", "non-urgent"], - hours_threshold: 96 - } - end - - # This policy can be applied to any agents working weekends - # Not assigning anyone by default - - # Set inbox limit - Enterprise::InboxCapacityLimit.find_or_create_by!( - agent_capacity_policy: policy6, - inbox: inbox - ) do |limit| - limit.conversation_limit = 25 - end - - policies << policy6 - puts " โœ“ Capacity Policy: #{policy6.name} (0 agents, limit: 25) - for weekend use" - - policies - rescue => e - puts " โš ๏ธ Could not create capacity policies: #{e.message}" - [] - end - - def create_scenario_based_conversations(account, inbox) - conversations = [] - - # Comprehensive message templates - scenarios = { - urgent_technical: { - messages: [ - "URGENT: Production API is returning 500 errors!", - "Critical: Database connection pool exhausted", - "Emergency: Customer data export failing", - "URGENT: Payment webhook not processing" - ], - labels: ["urgent", "technical", "high-priority"], - count: 5 - }, - vip_sales: { - messages: [ - "Enterprise evaluation - 1000+ agent requirement", - "Fortune 500 inquiry about custom features", - "Government contract compliance questions", - "Multi-national deployment requirements" - ], - labels: ["vip", "sales", "enterprise"], - count: 5 - }, - billing_issues: { - messages: [ - "Duplicate charge on my credit card", - "Invoice showing incorrect amount", - "Need to update payment method urgently", - "Refund request for accidental purchase" - ], - labels: ["billing", "financial"], - count: 10 - }, - technical_support: { - messages: [ - "API rate limiting questions", - "Webhook configuration help needed", - "Integration with Salesforce not working", - "Custom reporting requirements" - ], - labels: ["technical", "integration"], - count: 15 - }, - general_inquiries: { - messages: [ - "How to add team members?", - "What's the difference between plans?", - "Can I schedule messages?", - "How to export conversation history?" - ], - labels: ["general", "question"], - count: 20 - }, - feature_requests: { - messages: [ - "Can you add dark mode?", - "Need bulk operations feature", - "Request for mobile app improvements", - "Custom fields for contacts" - ], - labels: ["feature-request", "enhancement"], - count: 10 - }, - training_suitable: { - messages: [ - "How do I reset my password?", - "Where can I find my API key?", - "How to change notification settings?", - "What is the file size limit?" - ], - labels: ["training", "simple"], - count: 15 - }, - complex_issues: { - messages: [ - "Complex integration scenario with multiple systems", - "Performance issues with large data sets", - "Custom authentication implementation", - "Advanced automation workflow setup" - ], - labels: ["complex", "specialist-required"], - count: 10 - }, - escalated_complaints: { - messages: [ - "Very unhappy with support response time", - "Third time reporting the same issue", - "Threatening to cancel subscription", - "Need to speak with management" - ], - labels: ["escalated", "complaint", "retention-risk"], - count: 5 - }, - scheduled_followups: { - messages: [ - "Following up on our call last week", - "Checking status of feature request", - "Monthly account review", - "Quarterly business review prep" - ], - labels: ["scheduled", "follow-up"], - count: 10 - } - } - - # Create conversations for each scenario - scenarios.each do |scenario_key, scenario_data| - scenario_data[:count].times do |i| - begin - # Create contact - contact = account.contacts.create!( - name: "#{scenario_key.to_s.humanize} Customer #{i+1}", - email: "#{scenario_key}_#{i+1}_#{Time.current.to_i}@test.com", - phone_number: "+1555#{rand(1000000..9999999)}" - ) - - # Create contact inbox - contact_inbox = inbox.contact_inboxes.create!( - contact: contact, - source_id: "test_#{scenario_key}_#{Time.current.to_i}_#{i}" - ) - - # Create conversation - conversation = inbox.conversations.create!( - account: account, - contact: contact, - contact_inbox: contact_inbox, - assignee: nil, # Start unassigned - status: 'open', - additional_attributes: { - source: 'test_generator', - scenario: scenario_key.to_s, - test_batch: Time.current.to_i - } - ) - - # Add labels - conversation.update(label_list: scenario_data[:labels]) - - # Create initial message - conversation.messages.create!( - content: scenario_data[:messages].sample, - account: account, - inbox: inbox, - message_type: :incoming, - sender: contact - ) - - # Add follow-up messages for some scenarios - if [:urgent_technical, :escalated_complaints, :vip_sales].include?(scenario_key) && [true, false].sample - conversation.messages.create!( - content: "This is really urgent, please respond ASAP!", - account: account, - inbox: inbox, - message_type: :incoming, - sender: contact - ) - end - - # Vary creation time based on scenario - time_ago = case scenario_key - when :urgent_technical, :escalated_complaints - rand(1..6).hours.ago - when :vip_sales - rand(2..12).hours.ago - when :scheduled_followups - rand(1..7).days.ago - else - rand(6..72).hours.ago - end - - conversation.update_columns( - created_at: time_ago, - updated_at: time_ago - ) - - conversations << conversation - print "." - - rescue => e - print "โœ—" - puts "\nError creating #{scenario_key} conversation #{i+1}: #{e.message}" - end - end - end - - puts "\n" - conversations - end - - def assign_test_conversations(conversations, agents) - # Simulate Assignment V2 behavior with policy limits - assigned_count = 0 - inbox = conversations.first.inbox - - # Get the active assignment policy for the inbox - assignment_policy = inbox.assignment_policy - - if assignment_policy && assignment_policy.enabled? - puts "\n โœ“ Found active policy: #{assignment_policy.name}" - puts " - Fair Distribution Limit: #{assignment_policy.fair_distribution_limit}" - puts " - Fair Distribution Window: #{assignment_policy.fair_distribution_window} seconds" - puts "\n ๐Ÿ”„ Simulating policy-based assignment..." - - # Track assignments per agent within the time window - agent_assignment_counts = {} - window_start = assignment_policy.fair_distribution_window.seconds.ago - - # Count existing assignments within the time window - agents.each do |agent| - recent_count = Conversation.where( - assignee: agent, - inbox: inbox, - updated_at: window_start..Time.current - ).count - agent_assignment_counts[agent.id] = recent_count - puts " - #{agent.name}: #{recent_count} existing assignments in window" - end - - # Assign conversations respecting the policy limits - unassigned_conversations = conversations.select { |c| c.assignee.nil? } - round_robin_index = 0 - - unassigned_conversations.each do |conversation| - assigned = false - attempts = 0 - - # Try to find an agent who hasn't reached their limit - while !assigned && attempts < agents.count - agent = agents[round_robin_index % agents.count] - current_count = agent_assignment_counts[agent.id] || 0 - - if current_count < assignment_policy.fair_distribution_limit - conversation.update!(assignee: agent) - agent_assignment_counts[agent.id] = current_count + 1 - assigned = true - assigned_count += 1 - print "." - end - - round_robin_index += 1 - attempts += 1 - end - - if !assigned - print "X" # No agent available within limits - end - end - - puts "\n โœ“ Policy-based assignment complete: #{assigned_count} conversations assigned" - else - puts "\n โš ๏ธ No active assignment policy found. Using simple round-robin..." - - # Simple round-robin assignment - unassigned_conversations = conversations.select { |c| c.assignee.nil? } - unassigned_conversations.each_with_index do |conv, i| - agent = agents[i % agents.count] - conv.update!(assignee: agent) - assigned_count += 1 - print "." - end - - puts "\n โœ“ Assigned #{assigned_count} conversations" - end - - # Show assignment distribution - puts "\n ๐Ÿ“Š Assignment Distribution:" - agent_assignments = conversations.reload.group_by(&:assignee) - agents.each do |agent| - count = agent_assignments[agent]&.count || 0 - level = agent.custom_attributes&.dig('level') || 'unknown' - puts " - #{agent.name} (#{level}): #{count} conversations" - end - end - - def clear_test_data(account) - # Clear test conversations - test_conversations = account.conversations.joins(:messages) - .where("conversations.additional_attributes->>'source' = ?", 'test_generator') - count = test_conversations.count - test_conversations.destroy_all - puts " โœ“ Deleted #{count} test conversations" - - # Clear test contacts with pattern matching - test_contacts = account.contacts.where( - "email LIKE '%@test.com' OR email LIKE '%@example.com'" - ) - count = test_contacts.count - test_contacts.destroy_all - puts " โœ“ Deleted #{count} test contacts" - - # Clear test agents (optional - uncomment if needed) - # test_users = account.users.where("custom_attributes->>'test_agent' = ?", 'true') - # count = test_users.count - # test_users.destroy_all - # puts " โœ“ Deleted #{count} test agents" - end -end \ No newline at end of file diff --git a/lib/tasks/test_assignment_direct.rake b/lib/tasks/test_assignment_direct.rake deleted file mode 100644 index ceae3cae4..000000000 --- a/lib/tasks/test_assignment_direct.rake +++ /dev/null @@ -1,199 +0,0 @@ -# frozen_string_literal: true - -namespace :test do - desc "Test direct assignment of unassigned conversations" - task test_direct_assignment: :environment do - puts "๐Ÿš€ Testing direct assignment functionality..." - puts "=" * 60 - - # Get the first account - account = Account.first - unless account - puts "โŒ No account found. Please create an account first." - exit - end - - # Get the first inbox - inbox = account.inboxes.first - unless inbox - puts "โŒ No inbox found. Please create an inbox first." - exit - end - - puts "๐Ÿ“Š Using:" - puts " Account: #{account.name} (ID: #{account.id})" - puts " Inbox: #{inbox.name} (ID: #{inbox.id})" - puts " Channel Type: #{inbox.channel_type}" - puts "-" * 60 - - # Check assignment configuration - puts "\n๐Ÿ”ง Assignment Configuration:" - puts " Assignment V2 enabled: #{inbox.assignment_v2_enabled?}" - puts " Auto assignment enabled: #{inbox.enable_auto_assignment}" - - if inbox.assignment_v2_enabled? - if inbox.assignment_policy - puts " Assignment Policy: #{inbox.assignment_policy.name}" - puts " - Order: #{inbox.assignment_policy.assignment_order}" - puts " - Priority: #{inbox.assignment_policy.conversation_priority}" - puts " - Enabled: #{inbox.assignment_policy.enabled?}" - else - puts " โš ๏ธ No assignment policy configured for this inbox" - end - end - - # Get available agents - puts "\n๐Ÿ‘ฅ Available Agents:" - agents = inbox.members - if agents.empty? - puts " โŒ No agents assigned to this inbox" - exit - end - - agents.each do |agent| - puts " - #{agent.name} (#{agent.email}) - ID: #{agent.id}" - end - - # Get unassigned conversations - puts "\n๐Ÿ’ฌ Unassigned Conversations:" - unassigned_conversations = inbox.conversations.unassigned.open - - if unassigned_conversations.empty? - puts " โŒ No unassigned conversations found" - puts "\n Creating test conversations..." - - # Create some test conversations - contact = inbox.contacts.first || create_test_contact(account, inbox) - - 3.times do |i| - conversation = inbox.conversations.create!( - account: account, - contact: contact, - status: 'open', - additional_attributes: { test: true, created_by: 'assignment_test' } - ) - puts " โœ… Created conversation ##{conversation.display_id}" - end - - unassigned_conversations = inbox.conversations.unassigned.open - end - - puts " Found #{unassigned_conversations.count} unassigned conversations" - unassigned_conversations.limit(5).each do |conv| - puts " - Conversation ##{conv.display_id} (Created: #{conv.created_at})" - end - - # Test Assignment Methods - puts "\n๐Ÿงช Testing Assignment Methods:" - puts "-" * 60 - - # Method 1: Using Legacy Auto Assignment Service - puts "\n1๏ธโƒฃ Testing Legacy Auto Assignment Service:" - test_legacy_assignment(unassigned_conversations.first, inbox) - - # Method 2: Using Assignment V2 Service (if enabled) - if inbox.assignment_v2_enabled? - puts "\n2๏ธโƒฃ Testing Assignment V2 Service:" - test_assignment_v2(inbox) - else - puts "\n2๏ธโƒฃ Assignment V2 is not enabled for this inbox" - puts " To enable, you need to configure assignment_v2 in GlobalConfig" - end - - # Method 3: Direct assignment using conversation model - puts "\n3๏ธโƒฃ Testing Direct Assignment via Conversation Model:" - test_direct_conversation_assignment(unassigned_conversations.second, agents.first) - - # Show final status - puts "\n๐Ÿ“Š Final Status:" - puts " Total conversations: #{inbox.conversations.count}" - puts " Assigned: #{inbox.conversations.assigned.count}" - puts " Unassigned: #{inbox.conversations.unassigned.count}" - - puts "\nโœ… Assignment test completed!" - end - - private - - def create_test_contact(account, inbox) - contact = account.contacts.create!( - name: "Test Contact #{Time.current.to_i}", - email: "test#{Time.current.to_i}@example.com" - ) - - inbox.contact_inboxes.create!( - contact: contact, - source_id: "test_#{Time.current.to_i}" - ) - - contact - end - - def test_legacy_assignment(conversation, inbox) - return unless conversation - - puts " Testing on conversation ##{conversation.display_id}..." - - # Get available agents with capacity - agent_ids = inbox.member_ids_with_assignment_capacity - puts " Agents with capacity: #{agent_ids.inspect}" - - # Use the legacy assignment service - service = ::AutoAssignment::AgentAssignmentService.new( - conversation: conversation, - allowed_agent_ids: agent_ids - ) - - # Find assignee using round robin - assignee = service.find_assignee - if assignee - puts " โœ… Found assignee: #{assignee.name} (ID: #{assignee.id})" - - # Perform the assignment - service.perform - - # Reload and verify - conversation.reload - if conversation.assignee - puts " โœ… Successfully assigned to: #{conversation.assignee.name}" - else - puts " โŒ Assignment failed" - end - else - puts " โŒ No available agent found for assignment" - puts " This could be because all agents are offline or at capacity" - end - end - - def test_assignment_v2(inbox) - service = AssignmentV2::AssignmentService.new(inbox: inbox) - - # Try bulk assignment - assigned_count = service.perform_bulk_assignment(limit: 2) - - puts " โœ… Assigned #{assigned_count} conversations using Assignment V2" - - # Show which conversations were assigned - recent_assignments = inbox.conversations.assigned.order(updated_at: :desc).limit(assigned_count) - recent_assignments.each do |conv| - puts " - Conversation ##{conv.display_id} โ†’ #{conv.assignee.name}" - end - end - - def test_direct_conversation_assignment(conversation, agent) - return unless conversation && agent - - puts " Assigning conversation ##{conversation.display_id} to #{agent.name}..." - - # Direct assignment - conversation.update!(assignee: agent) - - # Verify - conversation.reload - if conversation.assignee == agent - puts " โœ… Successfully assigned directly" - else - puts " โŒ Direct assignment failed" - end - end -end \ No newline at end of file