From f4643116df850764ecdb0aecd0276a96cf3e2266 Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 29 Aug 2025 15:10:56 -0700 Subject: [PATCH] feat: Run assignment every 15 minutes (#12334) Currently, auto-assignment runs only during conversation creation or update events. If no agents are online when new conversations arrive, those conversations remain unassigned. With this change, unassigned conversations will be automatically assigned once agents become available. The job runs every 15 minutes and uses a fair distribution threshold of 100 to prevent a large number of conversations from being assigned to a single available agent. This will be customizable later. --- app/jobs/inboxes/bulk_auto_assignment_job.rb | 47 ++++++++++ config/schedule.yml | 7 ++ lib/limits.rb | 1 + .../inboxes/bulk_auto_assignment_job_spec.rb | 93 +++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 app/jobs/inboxes/bulk_auto_assignment_job.rb create mode 100644 spec/jobs/inboxes/bulk_auto_assignment_job_spec.rb diff --git a/app/jobs/inboxes/bulk_auto_assignment_job.rb b/app/jobs/inboxes/bulk_auto_assignment_job.rb new file mode 100644 index 000000000..9e808648b --- /dev/null +++ b/app/jobs/inboxes/bulk_auto_assignment_job.rb @@ -0,0 +1,47 @@ +class Inboxes::BulkAutoAssignmentJob < ApplicationJob + queue_as :scheduled_jobs + include BillingHelper + + def perform + Account.feature_assignment_v2.find_each do |account| + if should_skip_auto_assignment?(account) + Rails.logger.info("Skipping auto assignment for account #{account.id}") + next + end + + account.inboxes.where(enable_auto_assignment: true).find_each do |inbox| + process_assignment(inbox) + end + end + end + + private + + def process_assignment(inbox) + allowed_agent_ids = inbox.member_ids_with_assignment_capacity + + if allowed_agent_ids.blank? + Rails.logger.info("No agents available to assign conversation to inbox #{inbox.id}") + return + end + + assign_conversations(inbox, allowed_agent_ids) + end + + def assign_conversations(inbox, allowed_agent_ids) + unassigned_conversations = inbox.conversations.unassigned.open.limit(Limits::AUTO_ASSIGNMENT_BULK_LIMIT) + unassigned_conversations.find_each do |conversation| + ::AutoAssignment::AgentAssignmentService.new( + conversation: conversation, + allowed_agent_ids: allowed_agent_ids + ).perform + Rails.logger.info("Assigned conversation #{conversation.id} to agent #{allowed_agent_ids.first}") + end + end + + def should_skip_auto_assignment?(account) + return false unless ChatwootApp.chatwoot_cloud? + + default_plan?(account) + end +end diff --git a/config/schedule.yml b/config/schedule.yml index c45d395bf..b5b21b4ce 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -46,3 +46,10 @@ delete_accounts_job: cron: '0 1 * * *' class: 'Internal::DeleteAccountsJob' queue: scheduled_jobs + +# executed every 15 minutes +# to assign unassigned conversations for all inboxes +bulk_auto_assignment_job: + cron: '*/15 * * * *' + class: 'Inboxes::BulkAutoAssignmentJob' + queue: scheduled_jobs diff --git a/lib/limits.rb b/lib/limits.rb index 5a50f2f8f..7a2371207 100644 --- a/lib/limits.rb +++ b/lib/limits.rb @@ -5,6 +5,7 @@ module Limits OUT_OF_OFFICE_MESSAGE_MAX_LENGTH = 10_000 GREETING_MESSAGE_MAX_LENGTH = 10_000 CATEGORIES_PER_PAGE = 1000 + AUTO_ASSIGNMENT_BULK_LIMIT = 100 def self.conversation_message_per_minute_limit ENV.fetch('CONVERSATION_MESSAGE_PER_MINUTE_LIMIT', '200').to_i diff --git a/spec/jobs/inboxes/bulk_auto_assignment_job_spec.rb b/spec/jobs/inboxes/bulk_auto_assignment_job_spec.rb new file mode 100644 index 000000000..5e7e3d7cc --- /dev/null +++ b/spec/jobs/inboxes/bulk_auto_assignment_job_spec.rb @@ -0,0 +1,93 @@ +require 'rails_helper' + +RSpec.describe Inboxes::BulkAutoAssignmentJob do + let(:account) { create(:account, custom_attributes: { 'plan_name' => 'Startups' }) } + let(:agent) { create(:user, account: account, role: :agent, auto_offline: false) } + let(:inbox) { create(:inbox, account: account) } + let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: nil, status: :open) } + let(:assignment_service) { double } + + describe '#perform' do + before do + allow(assignment_service).to receive(:perform) + end + + context 'when inbox has inbox members' do + before do + create(:inbox_member, user: agent, inbox: inbox) + account.enable_features!('assignment_v2') + inbox.update!(enable_auto_assignment: true) + end + + it 'assigns unassigned conversations in enabled inboxes' do + allow(AutoAssignment::AgentAssignmentService).to receive(:new).with( + conversation: conversation, + allowed_agent_ids: [agent.id] + ).and_return(assignment_service) + + described_class.perform_now + expect(AutoAssignment::AgentAssignmentService).to have_received(:new).with( + conversation: conversation, + allowed_agent_ids: [agent.id] + ) + end + + it 'skips inboxes with auto assignment disabled' do + inbox.update!(enable_auto_assignment: false) + allow(AutoAssignment::AgentAssignmentService).to receive(:new) + + described_class.perform_now + + expect(AutoAssignment::AgentAssignmentService).not_to have_received(:new).with( + conversation: conversation, + allowed_agent_ids: [agent.id] + ) + end + + context 'when account is on default plan in chatwoot cloud' do + before do + account.update!(custom_attributes: {}) + InstallationConfig.create(name: 'CHATWOOT_CLOUD_PLANS', value: [{ 'name' => 'default' }]) + allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) + end + + it 'skips auto assignment' do + allow(Rails.logger).to receive(:info) + expect(Rails.logger).to receive(:info).with("Skipping auto assignment for account #{account.id}") + + allow(AutoAssignment::AgentAssignmentService).to receive(:new) + expect(AutoAssignment::AgentAssignmentService).not_to receive(:new) + + described_class.perform_now + end + end + end + + context 'when inbox has no members' do + before do + account.enable_features!('assignment_v2') + inbox.update!(enable_auto_assignment: true) + end + + it 'does not assign conversations' do + allow(Rails.logger).to receive(:info) + expect(Rails.logger).to receive(:info).with("No agents available to assign conversation to inbox #{inbox.id}") + + described_class.perform_now + end + end + + context 'when assignment_v2 feature is disabled' do + before do + account.disable_features!('assignment_v2') + end + + it 'skips auto assignment' do + allow(AutoAssignment::AgentAssignmentService).to receive(:new) + expect(AutoAssignment::AgentAssignmentService).not_to receive(:new) + + described_class.perform_now + end + end + end +end