fix: Do not allow sending messages if merged contact has a duplicate session (#11152)
In this PR https://github.com/chatwoot/chatwoot/pull/11139, if there is an attempt to create a duplication session for the contact in the same inbox, we will anonymize the old session. This PR would prevent sending messages to the older sessions. The support agents will have to create a new conversation to continue messages with customer.
This commit is contained in:
@@ -59,7 +59,7 @@ describe ContactInboxBuilder do
|
||||
contact: contact,
|
||||
inbox: twilio_inbox
|
||||
).perform
|
||||
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
|
||||
end.to raise_error(ArgumentError, 'contact phone number required')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,7 +117,7 @@ describe ContactInboxBuilder do
|
||||
contact: contact,
|
||||
inbox: twilio_inbox
|
||||
).perform
|
||||
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
|
||||
end.to raise_error(ArgumentError, 'contact phone number required')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -174,7 +174,7 @@ describe ContactInboxBuilder do
|
||||
contact: contact,
|
||||
inbox: whatsapp_inbox
|
||||
).perform
|
||||
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
|
||||
end.to raise_error(ArgumentError, 'contact phone number required')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -232,7 +232,7 @@ describe ContactInboxBuilder do
|
||||
contact: contact,
|
||||
inbox: sms_inbox
|
||||
).perform
|
||||
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
|
||||
end.to raise_error(ArgumentError, 'contact phone number required')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -290,7 +290,7 @@ describe ContactInboxBuilder do
|
||||
contact: contact,
|
||||
inbox: email_inbox
|
||||
).perform
|
||||
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact email')
|
||||
end.to raise_error(ArgumentError, 'contact email required')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
117
spec/services/contact_inbox/source_id_service_spec.rb
Normal file
117
spec/services/contact_inbox/source_id_service_spec.rb
Normal file
@@ -0,0 +1,117 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ContactInbox::SourceIdService do
|
||||
let(:contact) { create(:contact, email: 'test@example.com', phone_number: '+1234567890') }
|
||||
|
||||
describe '#generate' do
|
||||
context 'when channel is TwilioSms' do
|
||||
let(:channel_type) { 'Channel::TwilioSms' }
|
||||
|
||||
context 'with SMS medium' do
|
||||
subject { described_class.new(contact: contact, channel_type: channel_type, medium: 'sms') }
|
||||
|
||||
it 'returns phone number as source id' do
|
||||
expect(subject.generate).to eq(contact.phone_number)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with WhatsApp medium' do
|
||||
subject { described_class.new(contact: contact, channel_type: channel_type, medium: 'whatsapp') }
|
||||
|
||||
it 'returns whatsapp prefixed phone number as source id' do
|
||||
expect(subject.generate).to eq("whatsapp:#{contact.phone_number}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid medium' do
|
||||
subject { described_class.new(contact: contact, channel_type: channel_type, medium: 'invalid') }
|
||||
|
||||
it 'raises ArgumentError' do
|
||||
expect { subject.generate }.to raise_error(ArgumentError, 'Unsupported Twilio medium: invalid')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without medium' do
|
||||
subject { described_class.new(contact: contact, channel_type: channel_type) }
|
||||
|
||||
it 'raises ArgumentError' do
|
||||
expect { subject.generate }.to raise_error(ArgumentError, 'medium required for Twilio channel')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is Whatsapp' do
|
||||
subject { described_class.new(contact: contact, channel_type: 'Channel::Whatsapp') }
|
||||
|
||||
it 'returns phone number without + as source id' do
|
||||
expect(subject.generate).to eq(contact.phone_number.delete('+'))
|
||||
end
|
||||
|
||||
context 'when contact has no phone number' do
|
||||
let(:contact) { create(:contact, phone_number: nil) }
|
||||
|
||||
it 'raises ArgumentError' do
|
||||
expect { subject.generate }.to raise_error(ArgumentError, 'contact phone number required')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is Email' do
|
||||
subject { described_class.new(contact: contact, channel_type: 'Channel::Email') }
|
||||
|
||||
it 'returns email as source id' do
|
||||
expect(subject.generate).to eq(contact.email)
|
||||
end
|
||||
|
||||
context 'when contact has no email' do
|
||||
let(:contact) { create(:contact, email: nil) }
|
||||
|
||||
it 'raises ArgumentError' do
|
||||
expect { subject.generate }.to raise_error(ArgumentError, 'contact email required')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is SMS' do
|
||||
subject { described_class.new(contact: contact, channel_type: 'Channel::Sms') }
|
||||
|
||||
it 'returns phone number as source id' do
|
||||
expect(subject.generate).to eq(contact.phone_number)
|
||||
end
|
||||
|
||||
context 'when contact has no phone number' do
|
||||
let(:contact) { create(:contact, phone_number: nil) }
|
||||
|
||||
it 'raises ArgumentError' do
|
||||
expect { subject.generate }.to raise_error(ArgumentError, 'contact phone number required')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is Api' do
|
||||
subject { described_class.new(contact: contact, channel_type: 'Channel::Api') }
|
||||
|
||||
it 'returns a UUID as source id' do
|
||||
allow(SecureRandom).to receive(:uuid).and_return('uuid-123')
|
||||
expect(subject.generate).to eq('uuid-123')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is WebWidget' do
|
||||
subject { described_class.new(contact: contact, channel_type: 'Channel::WebWidget') }
|
||||
|
||||
it 'returns a UUID as source id' do
|
||||
allow(SecureRandom).to receive(:uuid).and_return('uuid-123')
|
||||
expect(subject.generate).to eq('uuid-123')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is unsupported' do
|
||||
subject { described_class.new(contact: contact, channel_type: 'Channel::Unknown') }
|
||||
|
||||
it 'raises ArgumentError' do
|
||||
expect { subject.generate }.to raise_error(ArgumentError, 'Unsupported operation for this channel: Channel::Unknown')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,8 +5,9 @@ describe Sms::SendOnSmsService 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) }
|
||||
let!(:contact) { create(:contact, phone_number: '+123456789') }
|
||||
let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: sms_channel.inbox, source_id: '+123456789') }
|
||||
let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: sms_channel.inbox, contact: contact) }
|
||||
|
||||
it 'calls channel.send_message' do
|
||||
message = create(:message, message_type: :outgoing, content: 'test',
|
||||
|
||||
@@ -13,9 +13,11 @@ describe Twilio::SendOnTwilioService do
|
||||
let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
|
||||
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
|
||||
let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
|
||||
let!(:contact) { create(:contact, account: account) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: twilio_inbox) }
|
||||
let!(:contact) { create(:contact, account: account, phone_number: '+123456789') }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: '+123456789') }
|
||||
let(:whatsapp_contact_inbox) { create(:contact_inbox, contact: contact, inbox: twilio_whatsapp_inbox, source_id: 'whatsapp:+123456789') }
|
||||
let(:conversation) { create(:conversation, contact: contact, inbox: twilio_inbox, contact_inbox: contact_inbox) }
|
||||
let(:whatsapp_conversation) { create(:conversation, contact: contact, inbox: twilio_whatsapp_inbox, contact_inbox: whatsapp_contact_inbox) }
|
||||
|
||||
before do
|
||||
allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
|
||||
@@ -71,7 +73,7 @@ describe Twilio::SendOnTwilioService do
|
||||
allow(message_record_double).to receive(:sid).and_return('1234')
|
||||
|
||||
message = build(
|
||||
:message, message_type: 'outgoing', inbox: twilio_whatsapp_inbox, account: account, conversation: conversation
|
||||
:message, message_type: 'outgoing', inbox: twilio_whatsapp_inbox, account: account, conversation: whatsapp_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')
|
||||
|
||||
@@ -18,8 +18,9 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
context 'when a valid message' do
|
||||
let(:whatsapp_request) { instance_double(HTTParty::Response) }
|
||||
let!(:whatsapp_channel) { create(:channel_whatsapp, sync_templates: false) }
|
||||
let!(:contact_inbox) { create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: '123456789') }
|
||||
let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: whatsapp_channel.inbox) }
|
||||
let!(:contact) { create(:contact, phone_number: '+123456789') }
|
||||
let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: whatsapp_channel.inbox, source_id: '123456789') }
|
||||
let!(:conversation) { create(:conversation, contact: contact, contact_inbox: contact_inbox, inbox: whatsapp_channel.inbox) }
|
||||
let(:api_key) { 'test_key' }
|
||||
let(:headers) { { 'D360-API-KEY' => api_key, 'Content-Type' => 'application/json' } }
|
||||
let(:template_body) do
|
||||
@@ -35,14 +36,12 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
}
|
||||
end
|
||||
|
||||
let(:success_response) { { 'messages' => [{ 'id' => '123456789' }] }.to_json }
|
||||
let(:success_response) { { 'messages' => [{ 'id' => 'message-123456789' }] }.to_json }
|
||||
|
||||
it 'calls channel.send_message when with in 24 hour limit' do
|
||||
# to handle the case of 24 hour window limit.
|
||||
create(:message, message_type: :incoming, content: 'test',
|
||||
conversation: conversation)
|
||||
message = create(:message, message_type: :outgoing, content: 'test',
|
||||
conversation: conversation)
|
||||
create(:message, message_type: :incoming, content: 'test', conversation: conversation)
|
||||
message = create(:message, message_type: :outgoing, content: 'test', conversation: conversation)
|
||||
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||
.with(
|
||||
@@ -52,7 +51,7 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
.to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
expect(message.reload.source_id).to eq('message-123456789')
|
||||
end
|
||||
|
||||
it 'calls channel.send_template when after 24 hour limit' do
|
||||
@@ -66,7 +65,7 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
expect(message.reload.source_id).to eq('message-123456789')
|
||||
end
|
||||
|
||||
it 'calls channel.send_template if template_params are present' do
|
||||
@@ -79,7 +78,7 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
expect(message.reload.source_id).to eq('message-123456789')
|
||||
end
|
||||
|
||||
it 'calls channel.send_template when template has regexp characters' do
|
||||
@@ -106,7 +105,18 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
expect(message.reload.source_id).to eq('message-123456789')
|
||||
end
|
||||
|
||||
context 'when source_id validation is required' do
|
||||
let(:message) { create(:message, conversation: conversation, message_type: :outgoing) }
|
||||
|
||||
it 'marks message as failed when source_ids do not match' do
|
||||
contact_inbox.update!(source_id: '1234567890')
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.status).to eq('failed')
|
||||
expect(message.external_error).to include('This conversation may have originally belonged to a different contact')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user