fix rubcop validations
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user