Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -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

View File

@@ -0,0 +1,68 @@
require 'rails_helper'
RSpec.describe Inboxes::FetchImapEmailInboxesJob do
let(:account) { create(:account) }
let(:suspended_account) { create(:account, status: 'suspended') }
let(:premium_account) { create(:account, custom_attributes: { plan_name: 'Startups' }) }
let(:imap_email_channel) do
create(:channel_email, imap_enabled: true, account: account)
end
let(:imap_email_channel_suspended) do
create(:channel_email, imap_enabled: true, account: suspended_account)
end
let(:disabled_imap_channel) do
create(:channel_email, imap_enabled: false, account: account)
end
let(:reauth_required_channel) do
create(:channel_email, imap_enabled: true, account: account)
end
let(:premium_imap_channel) do
create(:channel_email, imap_enabled: true, account: premium_account)
end
before do
reauth_required_channel.prompt_reauthorization!
premium_account.custom_attributes['plan_name'] = 'Startups'
end
it 'enqueues the job' do
expect { described_class.perform_later }.to have_enqueued_job(described_class)
.on_queue('scheduled_jobs')
end
context 'when called' do
it 'fetches emails only for active accounts with imap enabled' do
# Should call perform_later only once for the active, imap-enabled inbox
expect(Inboxes::FetchImapEmailsJob).to receive(:perform_later).with(imap_email_channel).once
# Should not call for suspended account or disabled IMAP channels
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(imap_email_channel_suspended)
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(disabled_imap_channel)
described_class.perform_now
end
it 'skips suspended accounts' do
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(imap_email_channel_suspended)
described_class.perform_now
end
it 'skips disabled imap channels' do
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(disabled_imap_channel)
described_class.perform_now
end
it 'skips channels requiring reauthorization' do
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(reauth_required_channel)
described_class.perform_now
end
end
end

View File

@@ -0,0 +1,123 @@
require 'rails_helper'
RSpec.describe Inboxes::FetchImapEmailsJob do
include ActiveJob::TestHelper
include ActionMailbox::TestHelper
let(:account) { create(:account) }
let(:imap_email_channel) { create(:channel_email, :imap_email, account: account) }
let(:channel_with_imap_disabled) { create(:channel_email, :imap_email, imap_enabled: false, account: account) }
let(:microsoft_imap_email_channel) { create(:channel_email, :microsoft_email) }
describe '#perform' do
it 'enqueues the job' do
expect do
described_class.perform_later(imap_email_channel, 1)
end.to have_enqueued_job(described_class).on_queue('scheduled_jobs')
end
context 'when IMAP is disabled' do
it 'does not fetch emails' do
expect(Imap::FetchEmailService).not_to receive(:new)
expect(Imap::MicrosoftFetchEmailService).not_to receive(:new)
described_class.perform_now(channel_with_imap_disabled)
end
end
context 'when IMAP reauthorization is required' do
it 'does not fetch emails' do
10.times do
imap_email_channel.authorization_error!
end
expect(Imap::FetchEmailService).not_to receive(:new)
# Confirm the imap_enabled flag is true to avoid false positives.
expect(imap_email_channel.imap_enabled?).to be true
described_class.perform_now(imap_email_channel)
end
end
context 'when the channel is regular imap' do
it 'calls the imap fetch service' do
fetch_service = double
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
allow(fetch_service).to receive(:perform).and_return([])
described_class.perform_now(imap_email_channel)
expect(fetch_service).to have_received(:perform)
end
it 'calls the imap fetch service with the correct interval' do
fetch_service = double
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 4).and_return(fetch_service)
allow(fetch_service).to receive(:perform).and_return([])
described_class.perform_now(imap_email_channel, 4)
expect(fetch_service).to have_received(:perform)
end
end
context 'when the channel is Microsoft' do
it 'calls the Microsoft fetch service' do
fetch_service = double
allow(Imap::MicrosoftFetchEmailService).to receive(:new).with(channel: microsoft_imap_email_channel, interval: 1).and_return(fetch_service)
allow(fetch_service).to receive(:perform).and_return([])
described_class.perform_now(microsoft_imap_email_channel)
expect(fetch_service).to have_received(:perform)
end
end
context 'when IMAP OAuth errors out' do
it 'marks the connection as requiring authorization' do
error_response = double
oauth_error = OAuth2::Error.new(error_response)
allow(Imap::MicrosoftFetchEmailService).to receive(:new)
.with(channel: microsoft_imap_email_channel, interval: 1)
.and_raise(oauth_error)
allow(Redis::Alfred).to receive(:incr)
expect(Redis::Alfred).to receive(:incr)
.with("AUTHORIZATION_ERROR_COUNT:channel_email:#{microsoft_imap_email_channel.id}")
described_class.perform_now(microsoft_imap_email_channel)
end
end
context 'when the fetch service returns the email objects' do
let(:inbound_mail) { create_inbound_email_from_fixture('welcome.eml').mail }
let(:mailbox) { double }
let(:exception_tracker) { double }
let(:fetch_service) { double }
before do
allow(Imap::ImapMailbox).to receive(:new).and_return(mailbox)
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
allow(fetch_service).to receive(:perform).and_return([inbound_mail])
end
it 'calls the mailbox to create emails' do
allow(mailbox).to receive(:process)
expect(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
expect(fetch_service).to receive(:perform).and_return([inbound_mail])
expect(mailbox).to receive(:process).with(inbound_mail, imap_email_channel)
described_class.perform_now(imap_email_channel)
end
it 'logs errors if mailbox returns errors' do
allow(mailbox).to receive(:process).and_raise(StandardError)
expect(exception_tracker).to receive(:capture_exception)
described_class.perform_now(imap_email_channel)
end
end
end
end

View File

@@ -0,0 +1,26 @@
require 'rails_helper'
RSpec.describe Inboxes::SyncWidgetPreChatCustomFieldsJob do
pre_chat_fields = [{
'label' => 'Developer Id',
'name' => 'developer_id'
}, {
'label' => 'Full Name',
'name' => 'full_name'
}]
pre_chat_message = 'Share your queries here.'
let!(:account) { create(:account) }
let!(:web_widget) do
create(:channel_widget, account: account, pre_chat_form_options: { pre_chat_message: pre_chat_message, pre_chat_fields: pre_chat_fields })
end
context 'when called' do
it 'sync pre chat fields if custom attribute deleted' do
described_class.perform_now(account, 'developer_id')
expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [{
'label' => 'Full Name',
'name' => 'full_name'
}]
end
end
end

View File

@@ -0,0 +1,33 @@
require 'rails_helper'
RSpec.describe Inboxes::UpdateWidgetPreChatCustomFieldsJob do
pre_chat_fields = [{
'label' => 'Developer Id',
'name' => 'developer_id'
}, {
'label' => 'Full Name',
'name' => 'full_name'
}]
pre_chat_message = 'Share your queries here.'
custom_attribute = {
'attribute_key' => 'developer_id',
'attribute_display_name' => 'Developer Number',
'regex_pattern' => '^[0-9]*',
'regex_cue' => 'It should be only digits'
}
let!(:account) { create(:account) }
let!(:web_widget) do
create(:channel_widget, account: account, pre_chat_form_options: { pre_chat_message: pre_chat_message, pre_chat_fields: pre_chat_fields })
end
context 'when called' do
it 'sync pre chat fields if custom attribute updated' do
described_class.perform_now(account, custom_attribute)
expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [
{ 'label' => 'Developer Number', 'name' => 'developer_id', 'placeholder' => 'Developer Number',
'values' => nil, 'regex_pattern' => '^[0-9]*', 'regex_cue' => 'It should be only digits' },
{ 'label' => 'Full Name', 'name' => 'full_name' }
]
end
end
end