fix rubcop validations

This commit is contained in:
Tanmay Deep Sharma
2025-07-31 15:36:06 +05:30
parent 24c7171a4e
commit f1832148fd
16 changed files with 440 additions and 1393 deletions
@@ -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
end
@@ -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
end
@@ -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
end
@@ -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
end
@@ -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
end
+2 -2
View File
@@ -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
end
@@ -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: {
@@ -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
@@ -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
@@ -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
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
@@ -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
end
@@ -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
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
@@ -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
def capacity_cache_key(agent, inbox)
"assignment_v2:capacity:#{agent.id}:#{inbox.id}"
end
end
+15 -11
View File
@@ -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
end
# rubocop:enable Metrics/BlockLength
@@ -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
-199
View File
@@ -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