fix(zapi): support for LID-only conversations (#123)
* fix: ignore action events * feat(zapi): allow updating instance ID * feat: handle `chatLid` field * test: break down handler specs into separate files * feat: update send_message method to use recipient_id logic for zapi provider * fix: use identifier instead of source_id for zapi * test: fix specs * feat: prioritize senderName over chatName for contact naming in received callback
This commit is contained in:
parent
3a14e2c0b8
commit
8ff5834ee8
@ -790,6 +790,8 @@
|
|||||||
"WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts",
|
"WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts",
|
||||||
"WHATSAPP_INSTANCE_ID_TITLE": "Instance ID",
|
"WHATSAPP_INSTANCE_ID_TITLE": "Instance ID",
|
||||||
"WHATSAPP_INSTANCE_ID_SUBHEADER": "Your Z-API Instance ID.",
|
"WHATSAPP_INSTANCE_ID_SUBHEADER": "Your Z-API Instance ID.",
|
||||||
|
"WHATSAPP_INSTANCE_ID_UPDATE_TITLE": "Update Instance ID",
|
||||||
|
"WHATSAPP_INSTANCE_ID_UPDATE_SUBHEADER": "Enter the new Instance ID here",
|
||||||
"WHATSAPP_TOKEN_TITLE": "Token",
|
"WHATSAPP_TOKEN_TITLE": "Token",
|
||||||
"WHATSAPP_TOKEN_SUBHEADER": "Your Z-API instance Token.",
|
"WHATSAPP_TOKEN_SUBHEADER": "Your Z-API instance Token.",
|
||||||
"WHATSAPP_TOKEN_UPDATE_TITLE": "Update Token",
|
"WHATSAPP_TOKEN_UPDATE_TITLE": "Update Token",
|
||||||
|
|||||||
@ -790,6 +790,8 @@
|
|||||||
"WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura",
|
"WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura",
|
||||||
"WHATSAPP_INSTANCE_ID_TITLE": "ID da Instância",
|
"WHATSAPP_INSTANCE_ID_TITLE": "ID da Instância",
|
||||||
"WHATSAPP_INSTANCE_ID_SUBHEADER": "Seu ID da Instância Z-API.",
|
"WHATSAPP_INSTANCE_ID_SUBHEADER": "Seu ID da Instância Z-API.",
|
||||||
|
"WHATSAPP_INSTANCE_ID_UPDATE_TITLE": "Atualizar ID da Instância",
|
||||||
|
"WHATSAPP_INSTANCE_ID_UPDATE_SUBHEADER": "Digite o novo ID da Instância aqui",
|
||||||
"WHATSAPP_TOKEN_TITLE": "Token",
|
"WHATSAPP_TOKEN_TITLE": "Token",
|
||||||
"WHATSAPP_TOKEN_SUBHEADER": "Seu Token da Instância Z-API.",
|
"WHATSAPP_TOKEN_SUBHEADER": "Seu Token da Instância Z-API.",
|
||||||
"WHATSAPP_TOKEN_UPDATE_TITLE": "Atualizar Token",
|
"WHATSAPP_TOKEN_UPDATE_TITLE": "Atualizar Token",
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export default {
|
|||||||
zapiInstanceId: '',
|
zapiInstanceId: '',
|
||||||
zapiToken: '',
|
zapiToken: '',
|
||||||
zapiClientToken: '',
|
zapiClientToken: '',
|
||||||
|
zapiInstanceIdUpdate: '',
|
||||||
zapiTokenUpdate: '',
|
zapiTokenUpdate: '',
|
||||||
zapiClientTokenUpdate: '',
|
zapiClientTokenUpdate: '',
|
||||||
};
|
};
|
||||||
@ -63,6 +64,7 @@ export default {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
baileysProviderUrl: { isValidURL: value => !value || isValidURL(value) },
|
baileysProviderUrl: { isValidURL: value => !value || isValidURL(value) },
|
||||||
|
zapiInstanceIdUpdate: {},
|
||||||
zapiTokenUpdate: {},
|
zapiTokenUpdate: {},
|
||||||
zapiClientTokenUpdate: {},
|
zapiClientTokenUpdate: {},
|
||||||
};
|
};
|
||||||
@ -213,6 +215,24 @@ export default {
|
|||||||
onCloseLinkDeviceModal() {
|
onCloseLinkDeviceModal() {
|
||||||
this.showLinkDeviceModal = false;
|
this.showLinkDeviceModal = false;
|
||||||
},
|
},
|
||||||
|
async updateZapiInstanceId() {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
id: this.inbox.id,
|
||||||
|
formData: false,
|
||||||
|
channel: {
|
||||||
|
provider_config: {
|
||||||
|
...this.inbox.provider_config,
|
||||||
|
instance_id: this.zapiInstanceIdUpdate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await this.$store.dispatch('inboxes/updateInbox', payload);
|
||||||
|
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
},
|
||||||
async updateZapiToken() {
|
async updateZapiToken() {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -676,14 +696,46 @@ export default {
|
|||||||
</NextButton>
|
</NextButton>
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<template v-if="inbox.provider_config.instance_id">
|
||||||
|
<SettingsSection
|
||||||
|
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_INSTANCE_ID_TITLE')"
|
||||||
|
:sub-title="
|
||||||
|
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_INSTANCE_ID_SUBHEADER')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<woot-code :script="inbox.provider_config.instance_id" />
|
||||||
|
</SettingsSection>
|
||||||
|
</template>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_INSTANCE_ID_TITLE')"
|
:title="
|
||||||
|
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_INSTANCE_ID_UPDATE_TITLE')
|
||||||
|
"
|
||||||
:sub-title="
|
:sub-title="
|
||||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_INSTANCE_ID_SUBHEADER')
|
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_INSTANCE_ID_UPDATE_SUBHEADER')
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<woot-code :script="inbox.provider_config.instance_id" />
|
<div
|
||||||
|
class="flex items-center justify-between flex-1 mt-2 whatsapp-settings--content"
|
||||||
|
>
|
||||||
|
<woot-input
|
||||||
|
v-model="zapiInstanceIdUpdate"
|
||||||
|
type="text"
|
||||||
|
class="flex-1 mr-2"
|
||||||
|
/>
|
||||||
|
<NextButton
|
||||||
|
:disabled="
|
||||||
|
v$.zapiInstanceIdUpdate.$invalid ||
|
||||||
|
(!inbox.provider_config.instance_id && !zapiInstanceIdUpdate) ||
|
||||||
|
zapiInstanceIdUpdate === inbox.provider_config.instance_id
|
||||||
|
"
|
||||||
|
@click="updateZapiInstanceId"
|
||||||
|
>
|
||||||
|
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }}
|
||||||
|
</NextButton>
|
||||||
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<template v-if="inbox.provider_config.token">
|
<template v-if="inbox.provider_config.token">
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_TOKEN_TITLE')"
|
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_TOKEN_TITLE')"
|
||||||
|
|||||||
@ -7,8 +7,8 @@ class Whatsapp::Providers::WhatsappZapiService < Whatsapp::Providers::BaseServic
|
|||||||
|
|
||||||
def sync_templates; end
|
def sync_templates; end
|
||||||
|
|
||||||
def send_message(phone_number, message)
|
def send_message(phone, message)
|
||||||
phone = phone_number.delete('+')
|
phone = phone.delete('+')
|
||||||
params = message.content_attributes[:zapi_args].presence || {}
|
params = message.content_attributes[:zapi_args].presence || {}
|
||||||
|
|
||||||
params[:messageId] = message.in_reply_to_external_id if message.in_reply_to_external_id.present?
|
params[:messageId] = message.in_reply_to_external_id if message.in_reply_to_external_id.present?
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
message_id = channel.send_template(message.conversation.contact_inbox.source_id, {
|
message_id = channel.send_template(recipient_id, {
|
||||||
name: name,
|
name: name,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
lang_code: lang_code,
|
lang_code: lang_code,
|
||||||
@ -46,10 +46,21 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_session_message
|
def send_session_message
|
||||||
message_id = channel.send_message(message.conversation.contact_inbox.source_id, message)
|
message_id = channel.send_message(recipient_id, message)
|
||||||
message.update!(source_id: message_id) if message_id.present?
|
message.update!(source_id: message_id) if message_id.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recipient_id
|
||||||
|
return message.conversation.contact_inbox.source_id unless channel.provider == 'zapi'
|
||||||
|
|
||||||
|
if message.conversation.contact.phone_number.present?
|
||||||
|
message.conversation.contact.phone_number.gsub(/[^\d]/, '')
|
||||||
|
else
|
||||||
|
# NOTE: `identifier` is the WhatsApp LID format. See `Whatsapp::ZapiHandlers::ReceivedCallback#set_contact`
|
||||||
|
message.conversation.contact.identifier
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
message.additional_attributes && message.additional_attributes['template_params']
|
message.additional_attributes && message.additional_attributes['template_params']
|
||||||
end
|
end
|
||||||
|
|||||||
@ -64,26 +64,36 @@ module Whatsapp::ZapiHandlers::ReceivedCallback # rubocop:disable Metrics/Module
|
|||||||
end
|
end
|
||||||
|
|
||||||
def contact_name
|
def contact_name
|
||||||
@raw_message[:chatName] || @raw_message[:senderName] || @raw_message[:phone]
|
@raw_message[:senderName] || @raw_message[:chatName] || @raw_message[:phone]
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_contact
|
def set_contact
|
||||||
push_name = contact_name
|
push_name = contact_name
|
||||||
source_id = @raw_message[:phone]
|
|
||||||
|
contact_attributes = { name: push_name, identifier: @raw_message[:chatLid] }
|
||||||
|
contact_attributes[:phone_number] = "+#{@raw_message[:phone]}" unless @raw_message[:phone].ends_with?('@lid')
|
||||||
|
|
||||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: source_id,
|
source_id: @raw_message[:chatLid].to_s.gsub(/[^\d]/, ''),
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
contact_attributes: { name: push_name, phone_number: "+#{source_id}" }
|
contact_attributes: contact_attributes
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
@contact_inbox = contact_inbox
|
@contact_inbox = contact_inbox
|
||||||
@contact = contact_inbox.contact
|
@contact = contact_inbox.contact
|
||||||
|
|
||||||
@contact.update!(name: push_name) if @contact.name == source_id
|
@contact.update!(name: push_name) if @contact.name == @raw_message[:phone]
|
||||||
|
update_contact_phone_number
|
||||||
try_update_contact_avatar
|
try_update_contact_avatar
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_contact_phone_number
|
||||||
|
return if @contact.phone_number.present?
|
||||||
|
return if @raw_message[:phone].ends_with?('@lid')
|
||||||
|
|
||||||
|
@contact.update!(phone_number: "+#{@raw_message[:phone]}")
|
||||||
|
end
|
||||||
|
|
||||||
def try_update_contact_avatar
|
def try_update_contact_avatar
|
||||||
avatar_url = @raw_message[:senderPhoto] || @raw_message[:photo]
|
avatar_url = @raw_message[:senderPhoto] || @raw_message[:photo]
|
||||||
return unless avatar_url.present? && avatar_url.start_with?('http')
|
return unless avatar_url.present? && avatar_url.start_with?('http')
|
||||||
|
|||||||
@ -35,760 +35,5 @@ describe Whatsapp::IncomingMessageZapiService do
|
|||||||
expect(Rails.logger).to have_received(:warn).with(/Z-API unsupported event/)
|
expect(Rails.logger).to have_received(:warn).with(/Z-API unsupported event/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when processing connected_callback event' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ConnectedCallback',
|
|
||||||
phone: whatsapp_channel.phone_number.delete('+')
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates provider connection to open when phone numbers match' do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('open')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates provider connection to close when phone numbers do not match' do
|
|
||||||
params[:phone] = '5511123456789'
|
|
||||||
allow(whatsapp_channel).to receive(:disconnect_channel_provider)
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
|
|
||||||
expect(whatsapp_channel.provider_connection['error']).to eq(I18n.t('errors.inboxes.channel.provider_connection.wrong_phone_number'))
|
|
||||||
expect(whatsapp_channel).to have_received(:disconnect_channel_provider)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'handles Brazil mobile number normalization' do
|
|
||||||
whatsapp_channel.update!(phone_number: '+5511987654321')
|
|
||||||
params[:phone] = '551187654321' # Without leading digit '9'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('open')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing disconnected_callback event' do
|
|
||||||
let(:params) { { type: 'DisconnectedCallback' } }
|
|
||||||
|
|
||||||
it 'updates provider connection to close' do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing received_callback event' do
|
|
||||||
let(:contact_phone) { '+5511987654321' }
|
|
||||||
let(:message_id) { 'msg_123' }
|
|
||||||
let(:contact) { create(:contact, phone_number: contact_phone, account: inbox.account) }
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: message_id,
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
messageType: 'chat',
|
|
||||||
text: { message: 'Hello World' }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a new message when message does not exist' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.content).to eq('Hello World')
|
|
||||||
expect(message.source_id).to eq(message_id)
|
|
||||||
expect(message.message_type).to eq('incoming')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create duplicate messages' do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to change(Message, :count)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'handles edited messages' do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
original_message = Message.last
|
|
||||||
edited_params = params.merge(
|
|
||||||
isEdit: true,
|
|
||||||
text: { message: 'Hello World - Edited' }
|
|
||||||
)
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: edited_params).perform
|
|
||||||
|
|
||||||
expect(original_message.reload.content).to eq('Hello World - Edited')
|
|
||||||
expect(original_message.content_attributes['is_edited']).to be(true)
|
|
||||||
expect(original_message.content_attributes['previous_content']).to eq('Hello World')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'calls channel received_messages method for incoming messages' do
|
|
||||||
allow(inbox.channel).to receive(:received_messages)
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
conversation = message.conversation
|
|
||||||
expect(inbox.channel).to have_received(:received_messages).with([message], conversation)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing image message' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'img_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
messageType: 'image',
|
|
||||||
image: {
|
|
||||||
caption: 'Check this image',
|
|
||||||
imageUrl: 'https://example.com/image.jpg',
|
|
||||||
mimeType: 'image/jpeg'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:get, 'https://example.com/image.jpg')
|
|
||||||
.to_return(status: 200, body: 'fake image data', headers: { 'Content-Type' => 'image/jpeg' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates message with image attachment' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.content).to eq('Check this image')
|
|
||||||
expect(message.attachments.count).to eq(1)
|
|
||||||
expect(message.attachments.first.file_type).to eq('image')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing audio message' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'audio_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
messageType: 'audio',
|
|
||||||
audio: {
|
|
||||||
audioUrl: 'https://example.com/audio.mp3',
|
|
||||||
mimeType: 'audio/mpeg'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:get, 'https://example.com/audio.mp3')
|
|
||||||
.to_return(status: 200, body: 'fake audio data', headers: { 'Content-Type' => 'audio/mpeg' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates message with audio attachment' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.attachments.count).to eq(1)
|
|
||||||
expect(message.attachments.first.file_type).to eq('audio')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing video message' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'video_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
messageType: 'video',
|
|
||||||
video: {
|
|
||||||
caption: 'Check this video',
|
|
||||||
videoUrl: 'https://example.com/video.mp4',
|
|
||||||
mimeType: 'video/mp4'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:get, 'https://example.com/video.mp4')
|
|
||||||
.to_return(status: 200, body: 'fake video data', headers: { 'Content-Type' => 'video/mp4' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates message with video attachment' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.content).to eq('Check this video')
|
|
||||||
expect(message.attachments.count).to eq(1)
|
|
||||||
expect(message.attachments.first.file_type).to eq('video')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing document message' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'doc_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
messageType: 'document',
|
|
||||||
document: {
|
|
||||||
caption: 'Important document',
|
|
||||||
documentUrl: 'https://example.com/document.pdf',
|
|
||||||
fileName: 'document.pdf',
|
|
||||||
mimeType: 'application/pdf'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:get, 'https://example.com/document.pdf')
|
|
||||||
.to_return(status: 200, body: 'fake pdf data', headers: { 'Content-Type' => 'application/pdf' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates message with document attachment' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.content).to eq('document.pdf')
|
|
||||||
expect(message.attachments.count).to eq(1)
|
|
||||||
expect(message.attachments.first.file_type).to eq('file')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing unsupported message type' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'unsupported_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
messageType: 'unsupported',
|
|
||||||
data: 'some unsupported data'
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates message marked as unsupported' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.content).to be_blank
|
|
||||||
expect(message.is_unsupported).to be(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing reaction message' do
|
|
||||||
let(:contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511987654321') }
|
|
||||||
let(:conversation) { create(:conversation, inbox: inbox, contact_inbox: contact_inbox) }
|
|
||||||
let!(:original_message) { create(:message, inbox: inbox, conversation: conversation, source_id: 'original_123') } # rubocop:disable RSpec/LetSetup
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'reaction_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
reaction: {
|
|
||||||
value: '👍',
|
|
||||||
referencedMessage: { messageId: 'original_123' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates reaction message' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.content).to eq('👍')
|
|
||||||
expect(message.content_attributes[:is_reaction]).to be(true)
|
|
||||||
expect(message.content_attributes[:in_reply_to_external_id]).to eq('original_123')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates empty reaction message' do
|
|
||||||
params[:reaction][:value] = ''
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.content).to eq('')
|
|
||||||
expect(message.content_attributes[:is_reaction]).to be(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing sticker message' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'sticker_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
sticker: {
|
|
||||||
stickerUrl: 'https://example.com/sticker.webp',
|
|
||||||
mimeType: 'image/webp'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:get, 'https://example.com/sticker.webp')
|
|
||||||
.to_return(status: 200, body: 'fake sticker data', headers: { 'Content-Type' => 'image/webp' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates message with sticker attachment' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.attachments.count).to eq(1)
|
|
||||||
expect(message.attachments.first.file_type).to eq('image')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing outgoing message' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'outgoing_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: true,
|
|
||||||
text: { message: 'Outgoing message' }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
create(:account_user, account: inbox.account)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates outgoing message' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.content).to eq('Outgoing message')
|
|
||||||
expect(message.message_type).to eq('outgoing')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not call channel received_messages method for outgoing messages' do
|
|
||||||
allow(inbox.channel).to receive(:received_messages)
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(inbox.channel).not_to have_received(:received_messages)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when handling duplicated events' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'duplicate_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
text: { message: 'Duplicated event' }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create message if it is already being processed' do
|
|
||||||
allow(Redis::Alfred).to receive(:get)
|
|
||||||
.with(format_message_source_key('duplicate_123'))
|
|
||||||
.and_return(true)
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to change(Message, :count)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'caches and clears message source id in Redis' do
|
|
||||||
allow(Redis::Alfred).to receive(:setex)
|
|
||||||
allow(Redis::Alfred).to receive(:delete)
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(Redis::Alfred).to have_received(:setex)
|
|
||||||
.with(format_message_source_key('duplicate_123'), true)
|
|
||||||
expect(Redis::Alfred).to have_received(:delete)
|
|
||||||
.with(format_message_source_key('duplicate_123'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when attachment download fails' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'img_fail_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
image: {
|
|
||||||
caption: 'Failed image',
|
|
||||||
imageUrl: 'https://example.com/broken.jpg',
|
|
||||||
mimeType: 'image/jpeg'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(Down).to receive(:download).and_raise(Down::ResponseError.new('Download failed'))
|
|
||||||
allow(Rails.logger).to receive(:error)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates message marked as unsupported when download fails' do
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.is_unsupported).to be(true)
|
|
||||||
expect(Rails.logger).to have_received(:error).with(/Failed to download attachment/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'handles malformed attachment URLs gracefully' do
|
|
||||||
allow(Down).to receive(:download).and_raise(Down::InvalidUrl.new('Invalid URL'))
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.is_unsupported).to be(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'handles network timeout errors' do
|
|
||||||
allow(Down).to receive(:download).and_raise(Down::TimeoutError.new('Download timeout'))
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.to change(Message, :count).by(1)
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.is_unsupported).to be(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when contact name handling' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'name_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654322',
|
|
||||||
fromMe: false,
|
|
||||||
senderName: 'John Doe from Z-API',
|
|
||||||
text: { message: 'Hello with name' }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates contact with sender name when provided' do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
contact = Contact.last
|
|
||||||
expect(contact.name).to eq('John Doe from Z-API')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'uses phone number as name when sender name is not provided' do
|
|
||||||
params.delete(:senderName)
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
expect(message.sender.name).to eq('5511987654322')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when message should not be processed' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'filtered_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
text: { message: 'Filtered message' }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not process group messages' do
|
|
||||||
params[:isGroup] = true
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to change(Message, :count)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not process newsletter messages' do
|
|
||||||
params[:isNewsletter] = true
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to change(Message, :count)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not process broadcast messages' do
|
|
||||||
params[:broadcast] = true
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to change(Message, :count)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not process status reply messages' do
|
|
||||||
params[:isStatusReply] = true
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to change(Message, :count)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing attachment with file extensions' do
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'ReceivedCallback',
|
|
||||||
messageId: 'ext_123',
|
|
||||||
momment: Time.current.to_i * 1000,
|
|
||||||
phone: '5511987654321',
|
|
||||||
fromMe: false,
|
|
||||||
document: {
|
|
||||||
fileName: 'report.xlsx',
|
|
||||||
documentUrl: 'https://example.com/report.xlsx',
|
|
||||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:get, 'https://example.com/report.xlsx')
|
|
||||||
.to_return(status: 200, body: 'fake excel data',
|
|
||||||
headers: { 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'preserves original filename and extension' do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
message = Message.last
|
|
||||||
attachment = message.attachments.first
|
|
||||||
expect(attachment.file.filename.to_s).to eq('report.xlsx')
|
|
||||||
expect(attachment.file.content_type).to eq('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing delivery_callback event' do
|
|
||||||
let(:message) { create(:message, inbox: inbox, source_id: 'msg_456') }
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'DeliveryCallback',
|
|
||||||
messageId: message.source_id,
|
|
||||||
momment: Time.current.to_i * 1000
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates message status to delivered' do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message.reload.status).to eq('delivered')
|
|
||||||
expect(message.external_created_at).to eq(params[:momment] / 1000)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates message status to failed when error is present' do
|
|
||||||
params[:error] = 'Message delivery failed'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message.reload.status).to eq('failed')
|
|
||||||
expect(message.external_error).to eq('Message delivery failed')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does nothing when message is not found' do
|
|
||||||
params[:messageId] = 'non_existent_message'
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to change(message, :status)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when processing message_status_callback event' do
|
|
||||||
let(:message1) { create(:message, inbox: inbox, source_id: 'msg_123') }
|
|
||||||
let(:message2) { create(:message, inbox: inbox, source_id: 'msg_456') }
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
type: 'MessageStatusCallback',
|
|
||||||
ids: [message1.source_id, message2.source_id],
|
|
||||||
status: 'SENT'
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates message status to sent when Z-API status is SENT' do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('sent')
|
|
||||||
expect(message2.reload.status).to eq('sent')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates message status to delivered for DELIVERED status' do
|
|
||||||
params[:status] = 'DELIVERED'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('delivered')
|
|
||||||
expect(message2.reload.status).to eq('delivered')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates message status to delivered for RECEIVED status' do
|
|
||||||
params[:status] = 'RECEIVED'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('delivered')
|
|
||||||
expect(message2.reload.status).to eq('delivered')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates message status to read for READ status' do
|
|
||||||
params[:status] = 'READ'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('read')
|
|
||||||
expect(message2.reload.status).to eq('read')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates message status to read for READ_BY_ME status' do
|
|
||||||
params[:status] = 'READ_BY_ME'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('read')
|
|
||||||
expect(message2.reload.status).to eq('read')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates message status to read for PLAYED status' do
|
|
||||||
params[:status] = 'PLAYED'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('read')
|
|
||||||
expect(message2.reload.status).to eq('read')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not update status on unknown status and logs warning' do
|
|
||||||
params[:status] = 'UNKNOWN_STATUS'
|
|
||||||
allow(Rails.logger).to receive(:warn)
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to(change { [message1.reload.status, message2.reload.status] })
|
|
||||||
|
|
||||||
expect(Rails.logger).to have_received(:warn).with('Unknown ZAPI status: UNKNOWN_STATUS')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when status transition is not allowed' do
|
|
||||||
it 'does not downgrade read message to delivered' do
|
|
||||||
message1.update!(status: 'read')
|
|
||||||
params[:status] = 'DELIVERED'
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to(change { message1.reload.status })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not downgrade read message to sent' do
|
|
||||||
message1.update!(status: 'read')
|
|
||||||
params[:status] = 'SENT'
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to(change { message1.reload.status })
|
|
||||||
|
|
||||||
expect(message1.status).to eq('read')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not downgrade delivered message to sent' do
|
|
||||||
message1.update!(status: 'delivered')
|
|
||||||
params[:status] = 'SENT'
|
|
||||||
|
|
||||||
expect do
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
end.not_to(change { message1.reload.status })
|
|
||||||
|
|
||||||
expect(message1.status).to eq('delivered')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows upgrading delivered message to read' do
|
|
||||||
message1.update!(status: 'delivered')
|
|
||||||
params[:status] = 'READ'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('read')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows upgrading sent message to delivered' do
|
|
||||||
message1.update!(status: 'sent')
|
|
||||||
params[:status] = 'DELIVERED'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('delivered')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows upgrading sent message to read' do
|
|
||||||
message1.update!(status: 'sent')
|
|
||||||
params[:status] = 'READ'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('read')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'handles mixed status transitions correctly' do
|
|
||||||
message1.update!(status: 'sent')
|
|
||||||
message2.update!(status: 'read')
|
|
||||||
params[:status] = 'DELIVERED'
|
|
||||||
|
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
|
||||||
|
|
||||||
expect(message1.reload.status).to eq('delivered')
|
|
||||||
expect(message2.reload.status).to eq('read')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def format_message_source_key(message_id)
|
|
||||||
format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -405,5 +405,39 @@ describe Whatsapp::SendOnWhatsappService do
|
|||||||
expect(message.reload.source_id).to eq('123456789')
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when provider is zapi' do
|
||||||
|
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'zapi', validate_provider_config: 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(:success_response) { { 'messageId' => 'msg_123' }.to_json }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:post, /.*/)
|
||||||
|
.to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with recipient_id logic' do
|
||||||
|
it 'uses phone number when contact has phone_number for session messages' do
|
||||||
|
conversation.contact.update!(phone_number: '+5511987654321')
|
||||||
|
create(:message, message_type: :incoming, content: 'test', conversation: conversation)
|
||||||
|
message = create(:message, message_type: :outgoing, content: 'test', conversation: conversation)
|
||||||
|
|
||||||
|
expect(whatsapp_channel).to receive(:send_message).with('5511987654321', message).and_return('msg_123')
|
||||||
|
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses identifier with @lid suffix when contact has no phone_number for session messages' do
|
||||||
|
conversation.contact.update!(phone_number: nil, identifier: '123456789@lid')
|
||||||
|
create(:message, message_type: :incoming, content: 'test', conversation: conversation)
|
||||||
|
message = create(:message, message_type: :outgoing, content: 'test', conversation: conversation)
|
||||||
|
|
||||||
|
expect(whatsapp_channel).to receive(:send_message).with('123456789@lid', message).and_return('msg_123')
|
||||||
|
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Whatsapp::ZapiHandlers::ConnectedCallback do
|
||||||
|
let!(:whatsapp_channel) do
|
||||||
|
create(:channel_whatsapp, provider: 'zapi', validate_provider_config: false)
|
||||||
|
end
|
||||||
|
let(:inbox) { whatsapp_channel.inbox }
|
||||||
|
let(:service) { Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: params) }
|
||||||
|
|
||||||
|
describe '#process_connected_callback' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ConnectedCallback',
|
||||||
|
phone: whatsapp_channel.phone_number.delete('+')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates provider connection to open when phone numbers match' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('open')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates provider connection to close when phone numbers do not match' do
|
||||||
|
params[:phone] = '5511123456789'
|
||||||
|
allow(whatsapp_channel).to receive(:disconnect_channel_provider)
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
|
||||||
|
expect(whatsapp_channel.provider_connection['error']).to eq(I18n.t('errors.inboxes.channel.provider_connection.wrong_phone_number'))
|
||||||
|
expect(whatsapp_channel).to have_received(:disconnect_channel_provider)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles Brazil mobile number normalization' do
|
||||||
|
whatsapp_channel.update!(phone_number: '+5511987654321')
|
||||||
|
params[:phone] = '551187654321' # Without leading digit '9'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('open')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Whatsapp::ZapiHandlers::DeliveryCallback do
|
||||||
|
let!(:whatsapp_channel) do
|
||||||
|
create(:channel_whatsapp, provider: 'zapi', validate_provider_config: false)
|
||||||
|
end
|
||||||
|
let(:inbox) { whatsapp_channel.inbox }
|
||||||
|
let(:service) { Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: params) }
|
||||||
|
|
||||||
|
describe '#process_delivery_callback' do
|
||||||
|
let(:message) { create(:message, inbox: inbox, source_id: 'msg_456') }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'DeliveryCallback',
|
||||||
|
messageId: message.source_id,
|
||||||
|
momment: Time.current.to_i * 1000
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates message status to delivered' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message.reload.status).to eq('delivered')
|
||||||
|
expect(message.external_created_at).to eq(params[:momment] / 1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates message status to failed when error is present' do
|
||||||
|
params[:error] = 'Message delivery failed'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message.reload.status).to eq('failed')
|
||||||
|
expect(message.external_error).to eq('Message delivery failed')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does nothing when message is not found' do
|
||||||
|
params[:messageId] = 'non_existent_message'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(message, :status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Whatsapp::ZapiHandlers::DisconnectedCallback do
|
||||||
|
let!(:whatsapp_channel) do
|
||||||
|
create(:channel_whatsapp, provider: 'zapi', validate_provider_config: false)
|
||||||
|
end
|
||||||
|
let(:inbox) { whatsapp_channel.inbox }
|
||||||
|
let(:service) { Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: params) }
|
||||||
|
|
||||||
|
describe '#process_disconnected_callback' do
|
||||||
|
let(:params) { { type: 'DisconnectedCallback' } }
|
||||||
|
|
||||||
|
it 'updates provider connection to close' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,155 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Whatsapp::ZapiHandlers::MessageStatusCallback do
|
||||||
|
let!(:whatsapp_channel) do
|
||||||
|
create(:channel_whatsapp, provider: 'zapi', validate_provider_config: false)
|
||||||
|
end
|
||||||
|
let(:inbox) { whatsapp_channel.inbox }
|
||||||
|
let(:service) { Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: params) }
|
||||||
|
|
||||||
|
describe '#process_message_status_callback' do
|
||||||
|
let(:message1) { create(:message, inbox: inbox, source_id: 'msg_123') }
|
||||||
|
let(:message2) { create(:message, inbox: inbox, source_id: 'msg_456') }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'MessageStatusCallback',
|
||||||
|
ids: [message1.source_id, message2.source_id],
|
||||||
|
status: 'SENT'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates message status to sent when Z-API status is SENT' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('sent')
|
||||||
|
expect(message2.reload.status).to eq('sent')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates message status to delivered for DELIVERED status' do
|
||||||
|
params[:status] = 'DELIVERED'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('delivered')
|
||||||
|
expect(message2.reload.status).to eq('delivered')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates message status to delivered for RECEIVED status' do
|
||||||
|
params[:status] = 'RECEIVED'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('delivered')
|
||||||
|
expect(message2.reload.status).to eq('delivered')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates message status to read for READ status' do
|
||||||
|
params[:status] = 'READ'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('read')
|
||||||
|
expect(message2.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates message status to read for READ_BY_ME status' do
|
||||||
|
params[:status] = 'READ_BY_ME'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('read')
|
||||||
|
expect(message2.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates message status to read for PLAYED status' do
|
||||||
|
params[:status] = 'PLAYED'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('read')
|
||||||
|
expect(message2.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update status on unknown status and logs warning' do
|
||||||
|
params[:status] = 'UNKNOWN_STATUS'
|
||||||
|
allow(Rails.logger).to receive(:warn)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to(change { [message1.reload.status, message2.reload.status] })
|
||||||
|
|
||||||
|
expect(Rails.logger).to have_received(:warn).with('Unknown ZAPI status: UNKNOWN_STATUS')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when status transition is not allowed' do
|
||||||
|
it 'does not downgrade read message to delivered' do
|
||||||
|
message1.update!(status: 'read')
|
||||||
|
params[:status] = 'DELIVERED'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to(change { message1.reload.status })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not downgrade read message to sent' do
|
||||||
|
message1.update!(status: 'read')
|
||||||
|
params[:status] = 'SENT'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to(change { message1.reload.status })
|
||||||
|
|
||||||
|
expect(message1.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not downgrade delivered message to sent' do
|
||||||
|
message1.update!(status: 'delivered')
|
||||||
|
params[:status] = 'SENT'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to(change { message1.reload.status })
|
||||||
|
|
||||||
|
expect(message1.status).to eq('delivered')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows upgrading delivered message to read' do
|
||||||
|
message1.update!(status: 'delivered')
|
||||||
|
params[:status] = 'READ'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows upgrading sent message to delivered' do
|
||||||
|
message1.update!(status: 'sent')
|
||||||
|
params[:status] = 'DELIVERED'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('delivered')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows upgrading sent message to read' do
|
||||||
|
message1.update!(status: 'sent')
|
||||||
|
params[:status] = 'READ'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles mixed status transitions correctly' do
|
||||||
|
message1.update!(status: 'sent')
|
||||||
|
message2.update!(status: 'read')
|
||||||
|
params[:status] = 'DELIVERED'
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(message1.reload.status).to eq('delivered')
|
||||||
|
expect(message2.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
961
spec/services/whatsapp/zapi_handlers/received_callback_spec.rb
Normal file
961
spec/services/whatsapp/zapi_handlers/received_callback_spec.rb
Normal file
@ -0,0 +1,961 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Whatsapp::ZapiHandlers::ReceivedCallback do
|
||||||
|
let!(:whatsapp_channel) do
|
||||||
|
create(:channel_whatsapp, provider: 'zapi', validate_provider_config: false)
|
||||||
|
end
|
||||||
|
let(:inbox) { whatsapp_channel.inbox }
|
||||||
|
let(:service) { Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: params) }
|
||||||
|
|
||||||
|
describe '#set_contact with LID handling' do
|
||||||
|
let(:base_params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'msg_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
fromMe: false,
|
||||||
|
chatName: 'John Doe',
|
||||||
|
text: { message: 'Hello' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when creating a new contact' do
|
||||||
|
context 'with LID only (no phone number available)' do
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '123456789@lid',
|
||||||
|
chatLid: '123456789@lid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates contact with identifier but no phone_number' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Contact, :count).by(1)
|
||||||
|
|
||||||
|
contact = Contact.last
|
||||||
|
expect(contact.identifier).to eq('123456789@lid')
|
||||||
|
expect(contact.phone_number).to be_nil
|
||||||
|
expect(contact.name).to eq('John Doe')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates contact_inbox with correct source_id' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
contact_inbox = ContactInbox.last
|
||||||
|
expect(contact_inbox.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with both phone number and LID' do
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '123456789@lid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates contact with both identifier and phone_number' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Contact, :count).by(1)
|
||||||
|
|
||||||
|
contact = Contact.last
|
||||||
|
expect(contact.identifier).to eq('123456789@lid')
|
||||||
|
expect(contact.phone_number).to eq('+5511987654321')
|
||||||
|
expect(contact.name).to eq('John Doe')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates contact_inbox with LID as source_id' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
contact_inbox = ContactInbox.last
|
||||||
|
expect(contact_inbox.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when phone matches chatLid (both are LID)' do
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '123456789@lid',
|
||||||
|
chatLid: '123456789@lid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates contact with identifier but no phone_number' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
contact = Contact.last
|
||||||
|
expect(contact.identifier).to eq('123456789@lid')
|
||||||
|
expect(contact.phone_number).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updating an existing contact' do
|
||||||
|
context 'when contact is created with LID only, now receives actual phone number' do
|
||||||
|
let!(:existing_contact) do
|
||||||
|
create(:contact,
|
||||||
|
account: inbox.account,
|
||||||
|
identifier: '123456789@lid',
|
||||||
|
phone_number: nil,
|
||||||
|
name: 'John Doe')
|
||||||
|
end
|
||||||
|
let!(:existing_contact_inbox) do
|
||||||
|
create(:contact_inbox,
|
||||||
|
inbox: inbox,
|
||||||
|
contact: existing_contact,
|
||||||
|
source_id: '123456789')
|
||||||
|
end
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '123456789@lid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reuses existing contact and does not create a new one' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Contact, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reuses existing contact_inbox' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(ContactInbox, :count)
|
||||||
|
|
||||||
|
expect(ContactInbox.last.id).to eq(existing_contact_inbox.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the contact phone_number when it becomes available' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(existing_contact.reload.phone_number).to eq('+5511987654321')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates message for the conversation' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.sender).to eq(existing_contact)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not overwrite phone_number if contact already has one' do
|
||||||
|
existing_contact.update!(phone_number: '+5511999999999')
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(existing_contact.reload.phone_number).to eq('+5511999999999')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when contact is created with phone number, now receives LID' do
|
||||||
|
let!(:existing_contact) do
|
||||||
|
create(:contact,
|
||||||
|
account: inbox.account,
|
||||||
|
identifier: nil,
|
||||||
|
phone_number: '+5511987654321',
|
||||||
|
name: 'John Doe')
|
||||||
|
end
|
||||||
|
let!(:existing_contact_inbox) do
|
||||||
|
create(:contact_inbox,
|
||||||
|
inbox: inbox,
|
||||||
|
contact: existing_contact,
|
||||||
|
source_id: '5511987654321')
|
||||||
|
end
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '123456789@lid',
|
||||||
|
chatLid: '123456789@lid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new contact with LID identifier' do
|
||||||
|
# ContactInboxWithContactBuilder will not find by source_id (different)
|
||||||
|
# Will not find by identifier (nil)
|
||||||
|
# Will not find by phone_number (param has no phone_number)
|
||||||
|
# So it creates a new contact
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Contact, :count).by(1)
|
||||||
|
|
||||||
|
new_contact = Contact.last
|
||||||
|
expect(new_contact.identifier).to eq('123456789@lid')
|
||||||
|
expect(new_contact.phone_number).to be_nil
|
||||||
|
expect(new_contact).not_to eq(existing_contact)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new contact_inbox with new source_id' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(ContactInbox, :count).by(1)
|
||||||
|
|
||||||
|
new_contact_inbox = ContactInbox.last
|
||||||
|
expect(new_contact_inbox.source_id).to eq('123456789')
|
||||||
|
expect(new_contact_inbox).not_to eq(existing_contact_inbox)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when contact exists with identifier and no phone, receives same LID' do
|
||||||
|
let!(:existing_contact) do
|
||||||
|
create(:contact,
|
||||||
|
account: inbox.account,
|
||||||
|
identifier: '123456789@lid',
|
||||||
|
phone_number: nil,
|
||||||
|
name: 'John Doe')
|
||||||
|
end
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '123456789@lid',
|
||||||
|
chatLid: '123456789@lid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:contact_inbox,
|
||||||
|
inbox: inbox,
|
||||||
|
contact: existing_contact,
|
||||||
|
source_id: '123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reuses existing contact' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Contact, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reuses existing contact_inbox' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(ContactInbox, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updating contact name when name is raw phone number' do
|
||||||
|
let!(:existing_contact) do
|
||||||
|
create(:contact,
|
||||||
|
account: inbox.account,
|
||||||
|
identifier: '123456789@lid',
|
||||||
|
phone_number: nil,
|
||||||
|
name: '5511987654321')
|
||||||
|
end
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '123456789@lid',
|
||||||
|
chatName: 'John Updated'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:contact_inbox,
|
||||||
|
inbox: inbox,
|
||||||
|
contact: existing_contact,
|
||||||
|
source_id: '123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates contact name when it matches raw phone' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(existing_contact.reload.name).to eq('John Updated')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updating contact name when name is LID' do
|
||||||
|
let!(:existing_contact) do
|
||||||
|
create(:contact,
|
||||||
|
account: inbox.account,
|
||||||
|
identifier: '123456789@lid',
|
||||||
|
phone_number: nil,
|
||||||
|
name: '123456789@lid')
|
||||||
|
end
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '123456789@lid',
|
||||||
|
chatLid: '123456789@lid',
|
||||||
|
chatName: 'John Updated'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:contact_inbox,
|
||||||
|
inbox: inbox,
|
||||||
|
contact: existing_contact,
|
||||||
|
source_id: '123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates contact name when it matches the phone value' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(existing_contact.reload.name).to eq('John Updated')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when handling contact name' do
|
||||||
|
let(:params_with_lid) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '123456789@lid',
|
||||||
|
chatLid: '123456789@lid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when senderName is present' do
|
||||||
|
let(:params) { params_with_lid.merge(senderName: 'John Sender', chatName: 'John from Chat') }
|
||||||
|
|
||||||
|
it 'uses senderName as contact name (higher priority)' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
contact = Contact.last
|
||||||
|
expect(contact.name).to eq('John Sender')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when senderName is absent but chatName is present' do
|
||||||
|
let(:params) { params_with_lid.merge(senderName: nil, chatName: 'John from Chat') }
|
||||||
|
|
||||||
|
it 'uses chatName as contact name' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
contact = Contact.last
|
||||||
|
expect(contact.name).to eq('John from Chat')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when both senderName and chatName are absent' do
|
||||||
|
let(:params) { params_with_lid.merge(senderName: nil, chatName: nil) }
|
||||||
|
|
||||||
|
it 'uses phone as contact name' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
contact = Contact.last
|
||||||
|
expect(contact.name).to eq('123456789@lid')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when contact exists by phone but new LID provided' do
|
||||||
|
let!(:existing_contact) do
|
||||||
|
create(:contact,
|
||||||
|
account: inbox.account,
|
||||||
|
identifier: '999999999@lid',
|
||||||
|
phone_number: '+5511987654321',
|
||||||
|
name: 'Existing Contact')
|
||||||
|
end
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '123456789@lid'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:contact_inbox,
|
||||||
|
inbox: inbox,
|
||||||
|
contact: existing_contact,
|
||||||
|
source_id: '999999999')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds existing contact by phone_number' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Contact, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates new contact_inbox with new source_id for same contact' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(ContactInbox, :count).by(1)
|
||||||
|
|
||||||
|
new_contact_inbox = ContactInbox.last
|
||||||
|
expect(new_contact_inbox.source_id).to eq('123456789')
|
||||||
|
expect(new_contact_inbox.contact).to eq(existing_contact)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when handling avatar' do
|
||||||
|
let(:params) do
|
||||||
|
base_params.merge(
|
||||||
|
phone: '123456789@lid',
|
||||||
|
chatLid: '123456789@lid',
|
||||||
|
senderPhoto: 'https://example.com/avatar.jpg'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'enqueues avatar update job when senderPhoto is present' do
|
||||||
|
expect(Avatar::AvatarFromUrlJob).to receive(:perform_later)
|
||||||
|
.with(kind_of(Contact), 'https://example.com/avatar.jpg')
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not enqueue avatar update job when senderPhoto is missing' do
|
||||||
|
params[:senderPhoto] = nil
|
||||||
|
|
||||||
|
expect(Avatar::AvatarFromUrlJob).not_to receive(:perform_later)
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses photo field as fallback when senderPhoto is not present' do
|
||||||
|
params[:senderPhoto] = nil
|
||||||
|
params[:photo] = 'https://example.com/photo.jpg'
|
||||||
|
|
||||||
|
expect(Avatar::AvatarFromUrlJob).to receive(:perform_later)
|
||||||
|
.with(kind_of(Contact), 'https://example.com/photo.jpg')
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not enqueue job when URL is invalid' do
|
||||||
|
params[:senderPhoto] = 'not-a-url'
|
||||||
|
|
||||||
|
expect(Avatar::AvatarFromUrlJob).not_to receive(:perform_later)
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#process_received_callback' do
|
||||||
|
let(:contact_phone) { '+5511987654321' }
|
||||||
|
let(:message_id) { 'msg_123' }
|
||||||
|
let(:contact) { create(:contact, phone_number: contact_phone, account: inbox.account) }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: message_id,
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
text: { message: 'Hello World' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new message when message does not exist' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.content).to eq('Hello World')
|
||||||
|
expect(message.source_id).to eq(message_id)
|
||||||
|
expect(message.message_type).to eq('incoming')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create duplicate messages' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles edited messages' do
|
||||||
|
service.perform
|
||||||
|
original_message = Message.last
|
||||||
|
edited_params = params.merge(
|
||||||
|
isEdit: true,
|
||||||
|
text: { message: 'Hello World - Edited' }
|
||||||
|
)
|
||||||
|
|
||||||
|
Whatsapp::IncomingMessageZapiService.new(inbox: inbox, params: edited_params).perform
|
||||||
|
|
||||||
|
expect(original_message.reload.content).to eq('Hello World - Edited')
|
||||||
|
expect(original_message.content_attributes['is_edited']).to be(true)
|
||||||
|
expect(original_message.content_attributes['previous_content']).to eq('Hello World')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls channel received_messages method for incoming messages' do
|
||||||
|
allow(inbox.channel).to receive(:received_messages)
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
conversation = message.conversation
|
||||||
|
expect(inbox.channel).to have_received(:received_messages).with([message], conversation)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing image message' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'img_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
image: {
|
||||||
|
caption: 'Check this image',
|
||||||
|
imageUrl: 'https://example.com/image.jpg',
|
||||||
|
mimeType: 'image/jpeg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/image.jpg')
|
||||||
|
.to_return(status: 200, body: 'fake image data', headers: { 'Content-Type' => 'image/jpeg' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates message with image attachment' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.content).to eq('Check this image')
|
||||||
|
expect(message.attachments.count).to eq(1)
|
||||||
|
expect(message.attachments.first.file_type).to eq('image')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing audio message' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'audio_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
audio: {
|
||||||
|
audioUrl: 'https://example.com/audio.mp3',
|
||||||
|
mimeType: 'audio/mpeg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/audio.mp3')
|
||||||
|
.to_return(status: 200, body: 'fake audio data', headers: { 'Content-Type' => 'audio/mpeg' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates message with audio attachment' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.attachments.count).to eq(1)
|
||||||
|
expect(message.attachments.first.file_type).to eq('audio')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing video message' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'video_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
video: {
|
||||||
|
caption: 'Check this video',
|
||||||
|
videoUrl: 'https://example.com/video.mp4',
|
||||||
|
mimeType: 'video/mp4'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/video.mp4')
|
||||||
|
.to_return(status: 200, body: 'fake video data', headers: { 'Content-Type' => 'video/mp4' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates message with video attachment' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.content).to eq('Check this video')
|
||||||
|
expect(message.attachments.count).to eq(1)
|
||||||
|
expect(message.attachments.first.file_type).to eq('video')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing document message' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'doc_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
document: {
|
||||||
|
documentUrl: 'https://example.com/document.pdf',
|
||||||
|
fileName: 'document.pdf',
|
||||||
|
mimeType: 'application/pdf'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/document.pdf')
|
||||||
|
.to_return(status: 200, body: 'fake pdf data', headers: { 'Content-Type' => 'application/pdf' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates message with document attachment' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.content).to eq('document.pdf')
|
||||||
|
expect(message.attachments.count).to eq(1)
|
||||||
|
expect(message.attachments.first.file_type).to eq('file')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing unsupported message type' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'unsupported_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
data: 'some unsupported data'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates message marked as unsupported' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.content).to be_blank
|
||||||
|
expect(message.is_unsupported).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing reaction message' do
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511987654321') }
|
||||||
|
let(:conversation) { create(:conversation, inbox: inbox, contact_inbox: contact_inbox) }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'reaction_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
reaction: {
|
||||||
|
value: '👍',
|
||||||
|
referencedMessage: { messageId: 'original_123' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:message, inbox: inbox, conversation: conversation, source_id: 'original_123')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates reaction message' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.content).to eq('👍')
|
||||||
|
expect(message.content_attributes[:is_reaction]).to be(true)
|
||||||
|
expect(message.content_attributes[:in_reply_to_external_id]).to eq('original_123')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates empty reaction message' do
|
||||||
|
params[:reaction][:value] = ''
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.content).to eq('')
|
||||||
|
expect(message.content_attributes[:is_reaction]).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing sticker message' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'sticker_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
sticker: {
|
||||||
|
stickerUrl: 'https://example.com/sticker.webp',
|
||||||
|
mimeType: 'image/webp'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/sticker.webp')
|
||||||
|
.to_return(status: 200, body: 'fake sticker data', headers: { 'Content-Type' => 'image/webp' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates message with sticker attachment' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.attachments.count).to eq(1)
|
||||||
|
expect(message.attachments.first.file_type).to eq('image')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing outgoing message' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'outgoing_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: true,
|
||||||
|
text: { message: 'Outgoing message' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:account_user, account: inbox.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates outgoing message' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.content).to eq('Outgoing message')
|
||||||
|
expect(message.message_type).to eq('outgoing')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not call channel received_messages method for outgoing messages' do
|
||||||
|
allow(inbox.channel).to receive(:received_messages)
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(inbox.channel).not_to have_received(:received_messages)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when handling duplicated events' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'duplicate_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
text: { message: 'Duplicated event' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create message if it is already being processed' do
|
||||||
|
allow(Redis::Alfred).to receive(:get)
|
||||||
|
.with(format_message_source_key('duplicate_123'))
|
||||||
|
.and_return(true)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'caches and clears message source id in Redis' do
|
||||||
|
allow(Redis::Alfred).to receive(:setex)
|
||||||
|
allow(Redis::Alfred).to receive(:delete)
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
expect(Redis::Alfred).to have_received(:setex)
|
||||||
|
.with(format_message_source_key('duplicate_123'), true)
|
||||||
|
expect(Redis::Alfred).to have_received(:delete)
|
||||||
|
.with(format_message_source_key('duplicate_123'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when attachment download fails' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'img_fail_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
image: {
|
||||||
|
caption: 'Failed image',
|
||||||
|
imageUrl: 'https://example.com/broken.jpg',
|
||||||
|
mimeType: 'image/jpeg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Down).to receive(:download).and_raise(Down::ResponseError.new('Download failed'))
|
||||||
|
allow(Rails.logger).to receive(:error)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates message marked as unsupported when download fails' do
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.is_unsupported).to be(true)
|
||||||
|
expect(Rails.logger).to have_received(:error).with(/Failed to download attachment/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles malformed attachment URLs gracefully' do
|
||||||
|
allow(Down).to receive(:download).and_raise(Down::InvalidUrl.new('Invalid URL'))
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.is_unsupported).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles network timeout errors' do
|
||||||
|
allow(Down).to receive(:download).and_raise(Down::TimeoutError.new('Download timeout'))
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.to change(Message, :count).by(1)
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.is_unsupported).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when contact name handling' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'name_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654322',
|
||||||
|
chatLid: '5511987654322',
|
||||||
|
fromMe: false,
|
||||||
|
senderName: 'John Doe from Z-API',
|
||||||
|
text: { message: 'Hello with name' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates contact with sender name when provided' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
contact = Contact.last
|
||||||
|
expect(contact.name).to eq('John Doe from Z-API')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses phone number as name when sender name is not provided' do
|
||||||
|
params.delete(:senderName)
|
||||||
|
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
expect(message.sender.name).to eq('5511987654322')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when message should not be processed' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'filtered_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
text: { message: 'Filtered message' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not process group messages' do
|
||||||
|
params[:isGroup] = true
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not process newsletter messages' do
|
||||||
|
params[:isNewsletter] = true
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not process broadcast messages' do
|
||||||
|
params[:broadcast] = true
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not process status reply messages' do
|
||||||
|
params[:isStatusReply] = true
|
||||||
|
|
||||||
|
expect do
|
||||||
|
service.perform
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when processing attachment with file extensions' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
type: 'ReceivedCallback',
|
||||||
|
messageId: 'ext_123',
|
||||||
|
momment: Time.current.to_i * 1000,
|
||||||
|
phone: '5511987654321',
|
||||||
|
chatLid: '5511987654321',
|
||||||
|
fromMe: false,
|
||||||
|
document: {
|
||||||
|
fileName: 'report.xlsx',
|
||||||
|
documentUrl: 'https://example.com/report.xlsx',
|
||||||
|
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/report.xlsx')
|
||||||
|
.to_return(status: 200, body: 'fake excel data',
|
||||||
|
headers: { 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'preserves original filename and extension' do
|
||||||
|
service.perform
|
||||||
|
|
||||||
|
message = Message.last
|
||||||
|
attachment = message.attachments.first
|
||||||
|
expect(attachment.file.filename.to_s).to eq('report.xlsx')
|
||||||
|
expect(attachment.file.content_type).to eq('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def format_message_source_key(message_id)
|
||||||
|
format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user