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,83 @@
require 'rails_helper'
describe Sms::DeliveryStatusService do
describe '#perform' do
let!(:account) { create(:account) }
let!(:sms_channel) { create(:channel_sms) }
let!(:contact) { create(:contact, account: account, phone_number: '+12345') }
let(:contact_inbox) { create(:contact_inbox, source_id: '+12345', contact: contact, inbox: sms_channel.inbox) }
let!(:conversation) { create(:conversation, contact: contact, inbox: sms_channel.inbox, contact_inbox: contact_inbox) }
describe '#perform' do
context 'when message delivery status is fired' do
before do
create(:message, account: account, inbox: sms_channel.inbox, conversation: conversation, status: :sent,
source_id: 'SMd560ac79e4a4d36b3ce59f1f50471986')
end
it 'updates the message if the message status is delivered' do
params = {
time: '2022-02-02T23:14:05.309Z',
type: 'message-delivered',
to: sms_channel.phone_number,
description: 'ok',
message: {
'id': conversation.messages.last.source_id
}
}
described_class.new(params: params, inbox: sms_channel.inbox).perform
expect(conversation.reload.messages.last.status).to eq('delivered')
end
it 'updates the message if the message status is failed' do
params = {
time: '2022-02-02T23:14:05.309Z',
type: 'message-failed',
to: sms_channel.phone_number,
description: 'Undeliverable',
errorCode: 995,
message: {
'id': conversation.messages.last.source_id
}
}
described_class.new(params: params, inbox: sms_channel.inbox).perform
expect(conversation.reload.messages.last.status).to eq('failed')
expect(conversation.reload.messages.last.external_error).to eq('995 - Undeliverable')
end
it 'does not update the message if the status is not a support status' do
params = {
time: '2022-02-02T23:14:05.309Z',
type: 'queued',
to: sms_channel.phone_number,
description: 'ok',
message: {
'id': conversation.messages.last.source_id
}
}
described_class.new(params: params, inbox: sms_channel.inbox).perform
expect(conversation.reload.messages.last.status).to eq('sent')
end
it 'does not update the message if the message is not present' do
params = {
time: '2022-02-02T23:14:05.309Z',
type: 'message-delivered',
to: sms_channel.phone_number,
description: 'ok',
message: {
'id': '123'
}
}
described_class.new(params: params, inbox: sms_channel.inbox).perform
expect(conversation.reload.messages.last.status).to eq('sent')
end
end
end
end
end

View File

@@ -0,0 +1,97 @@
require 'rails_helper'
describe Sms::IncomingMessageService do
describe '#perform' do
let!(:sms_channel) { create(:channel_sms) }
let(:params) do
{
'id': '3232420-2323-234324',
'owner': sms_channel.phone_number,
'applicationId': '2342349-324234d-32432432',
'time': '2022-02-02T23:14:05.262Z',
'segmentCount': 1,
'direction': 'in',
'to': [
sms_channel.phone_number
],
'from': '+14234234234',
'text': 'test message'
}.with_indifferent_access
end
context 'when valid text message params' do
it 'creates appropriate conversations, message and contacts' do
described_class.new(inbox: sms_channel.inbox, params: params).perform
expect(sms_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('+1 423-423-4234')
expect(sms_channel.inbox.messages.first.content).to eq(params[:text])
end
it 'appends to last conversation when if conversation already exisits' do
contact_inbox = create(:contact_inbox, inbox: sms_channel.inbox, source_id: params[:from])
2.times.each { create(:conversation, inbox: sms_channel.inbox, contact_inbox: contact_inbox) }
last_conversation = create(:conversation, inbox: sms_channel.inbox, contact_inbox: contact_inbox)
described_class.new(inbox: sms_channel.inbox, params: params).perform
# no new conversation should be created
expect(sms_channel.inbox.conversations.count).to eq(3)
# message appended to the last conversation
expect(last_conversation.messages.last.content).to eq(params[:text])
end
it 'reopen last conversation if last conversation is resolved and lock to single conversation is enabled' do
sms_channel.inbox.update(lock_to_single_conversation: true)
contact_inbox = create(:contact_inbox, inbox: sms_channel.inbox, source_id: params[:from])
last_conversation = create(:conversation, inbox: sms_channel.inbox, contact_inbox: contact_inbox)
last_conversation.update(status: 'resolved')
described_class.new(inbox: sms_channel.inbox, params: params).perform
# no new conversation should be created
expect(sms_channel.inbox.conversations.count).to eq(1)
expect(sms_channel.inbox.conversations.open.last.messages.last.content).to eq(params[:text])
expect(sms_channel.inbox.conversations.open.last.status).to eq('open')
end
it 'creates a new conversation if last conversation is resolved and lock to single conversation is disabled' do
sms_channel.inbox.update(lock_to_single_conversation: false)
contact_inbox = create(:contact_inbox, inbox: sms_channel.inbox, source_id: params[:from])
last_conversation = create(:conversation, inbox: sms_channel.inbox, contact_inbox: contact_inbox)
last_conversation.update(status: 'resolved')
described_class.new(inbox: sms_channel.inbox, params: params).perform
# new conversation should be created
expect(sms_channel.inbox.conversations.count).to eq(2)
# message appended to the last conversation
expect(contact_inbox.conversations.last.messages.last.content).to eq(params[:text])
end
it 'will not create a new conversation if last conversation is not resolved and lock to single conversation is disabled' do
sms_channel.inbox.update(lock_to_single_conversation: false)
contact_inbox = create(:contact_inbox, inbox: sms_channel.inbox, source_id: params[:from])
last_conversation = create(:conversation, inbox: sms_channel.inbox, contact_inbox: contact_inbox)
last_conversation.update(status: Conversation.statuses.except('resolved').keys.sample)
described_class.new(inbox: sms_channel.inbox, params: params).perform
# new conversation should be created
expect(sms_channel.inbox.conversations.count).to eq(1)
# message appended to the last conversation
expect(contact_inbox.conversations.last.messages.last.content).to eq(params[:text])
end
it 'creates attachment messages and ignores .smil files' do
stub_request(:get, 'http://test.com/test.png').to_return(status: 200, body: File.read('spec/assets/sample.png'), headers: {})
stub_request(:get, 'http://test.com/test2.png').to_return(status: 200, body: File.read('spec/assets/sample.png'), headers: {})
media_params = { 'media': [
'http://test.com/test.smil',
'http://test.com/test.png',
'http://test.com/test2.png'
] }.with_indifferent_access
described_class.new(inbox: sms_channel.inbox, params: params.merge(media_params)).perform
expect(sms_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('+1 423-423-4234')
expect(sms_channel.inbox.messages.first.content).to eq('test message')
expect(sms_channel.inbox.messages.first.attachments.present?).to be true
end
end
end
end

View File

@@ -0,0 +1,73 @@
require 'rails_helper'
describe Sms::OneoffSmsCampaignService do
subject(:sms_campaign_service) { described_class.new(campaign: campaign) }
let(:account) { create(:account) }
let!(:sms_channel) { create(:channel_sms, account: account) }
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) }
let(:label1) { create(:label, account: account) }
let(:label2) { create(:label, account: account) }
let!(:campaign) do
create(:campaign, inbox: sms_inbox, account: account,
audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }])
end
describe 'perform' do
before do
stub_request(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages').to_return(
status: 200,
body: { 'id' => '1' }.to_json,
headers: {}
)
allow_any_instance_of(described_class).to receive(:channel).and_return(sms_channel) # rubocop:disable RSpec/AnyInstance
end
it 'raises error if the campaign is completed' do
campaign.completed!
expect { sms_campaign_service.perform }.to raise_error 'Completed Campaign'
end
it 'raises error invalid campaign when its not a oneoff sms campaign' do
campaign = create(:campaign)
expect { described_class.new(campaign: campaign).perform }.to raise_error "Invalid campaign #{campaign.id}"
end
it 'send messages to contacts in the audience and marks the campaign completed' do
contact_with_label1, contact_with_label2, contact_with_both_labels = FactoryBot.create_list(:contact, 3, :with_phone_number, account: account)
contact_with_label1.update_labels([label1.title])
contact_with_label2.update_labels([label2.title])
contact_with_both_labels.update_labels([label1.title, label2.title])
sms_campaign_service.perform
assert_requested(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages', times: 3)
expect(campaign.reload.completed?).to be true
end
it 'uses liquid template service to process campaign message' do
contact = create(:contact, :with_phone_number, account: account)
contact.update_labels([label1.title])
expect(Liquid::CampaignTemplateService).to receive(:new).with(campaign: campaign, contact: contact).and_call_original
sms_campaign_service.perform
end
it 'continues processing contacts when sending message raises an error' do
contact_error, contact_success = FactoryBot.create_list(:contact, 2, :with_phone_number, account: account)
contact_error.update_labels([label1.title])
contact_success.update_labels([label1.title])
error_message = 'SMS provider error'
expect(sms_channel).to receive(:send_text_message).with(contact_error.phone_number, anything).and_raise(StandardError, error_message)
expect(sms_channel).to receive(:send_text_message).with(contact_success.phone_number, anything).and_return(nil)
expect(Rails.logger).to receive(:error).with("[SMS Campaign #{campaign.id}] Failed to send to #{contact_error.phone_number}: #{error_message}")
sms_campaign_service.perform
expect(campaign.reload.completed?).to be true
end
end
end

View File

@@ -0,0 +1,53 @@
require 'rails_helper'
describe Sms::SendOnSmsService do
describe '#perform' do
context 'when a valid message' do
let(:sms_request) { double }
let!(:sms_channel) { create(:channel_sms) }
let!(:contact_inbox) { create(:contact_inbox, inbox: sms_channel.inbox, source_id: '+123456789') }
let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: sms_channel.inbox) }
it 'calls channel.send_message' do
message = create(:message, message_type: :outgoing, content: 'test',
conversation: conversation)
allow(HTTParty).to receive(:post).and_return(sms_request)
allow(sms_request).to receive(:success?).and_return(true)
allow(sms_request).to receive(:parsed_response).and_return({ 'id' => '123456789' })
expect(HTTParty).to receive(:post).with(
'https://messaging.bandwidth.com/api/v2/users/1/messages',
basic_auth: { username: '1', password: '1' },
headers: { 'Content-Type' => 'application/json' },
body: { 'to' => '+123456789', 'from' => sms_channel.phone_number, 'text' => 'test', 'applicationId' => '1' }.to_json
)
described_class.new(message: message).perform
expect(message.reload.source_id).to eq('123456789')
end
it 'calls channel.send_message with attachments' do
message = build(:message, message_type: :outgoing, content: 'test',
conversation: conversation)
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
attachment2 = message.attachments.new(account_id: message.account_id, file_type: :image)
attachment2.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
message.save!
allow(HTTParty).to receive(:post).and_return(sms_request)
allow(sms_request).to receive(:success?).and_return(true)
allow(sms_request).to receive(:parsed_response).and_return({ 'id' => '123456789' })
allow(attachment).to receive(:download_url).and_return('url1')
allow(attachment2).to receive(:download_url).and_return('url2')
expect(HTTParty).to receive(:post).with(
'https://messaging.bandwidth.com/api/v2/users/1/messages',
basic_auth: { username: '1', password: '1' },
headers: { 'Content-Type' => 'application/json' },
body: { 'to' => '+123456789', 'from' => sms_channel.phone_number, 'text' => 'test', 'applicationId' => '1',
'media' => %w[url1 url2] }.to_json
)
described_class.new(message: message).perform
expect(message.reload.source_id).to eq('123456789')
end
end
end
end