diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index 2e58bce10..e0a63d81a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -204,7 +204,8 @@ export default { if ( this.isAWhatsAppChannel || this.isATwilioChannel || - this.isAFacebookInbox + this.isAFacebookInbox || + this.isASmsInbox ) { return this.sourceId && this.isSent; } @@ -215,7 +216,11 @@ export default { return false; } - if (this.isAWhatsAppChannel || this.isATwilioChannel) { + if ( + this.isAWhatsAppChannel || + this.isATwilioChannel || + this.isASmsInbox + ) { return this.sourceId && this.isDelivered; } // We will consider messages as delivered for web widget inbox and API inbox if they are sent diff --git a/app/jobs/webhooks/sms_events_job.rb b/app/jobs/webhooks/sms_events_job.rb index c982e0da1..21318f766 100644 --- a/app/jobs/webhooks/sms_events_job.rb +++ b/app/jobs/webhooks/sms_events_job.rb @@ -1,13 +1,28 @@ class Webhooks::SmsEventsJob < ApplicationJob queue_as :default + SUPPORTED_EVENTS = %w[message-received message-delivered message-failed].freeze + def perform(params = {}) - return unless params[:type] == 'message-received' + return unless SUPPORTED_EVENTS.include?(params[:type]) channel = Channel::Sms.find_by(phone_number: params[:to]) return unless channel - # TODO: pass to appropriate provider service from here - Sms::IncomingMessageService.new(inbox: channel.inbox, params: params[:message].with_indifferent_access).perform + process_event_params(channel, params) + end + + private + + def process_event_params(channel, params) + if delivery_event?(params) + Sms::DeliveryStatusService.new(channel: channel, params: params[:message].with_indifferent_access).perform + else + Sms::IncomingMessageService.new(inbox: channel.inbox, params: params[:message].with_indifferent_access).perform + end + end + + def delivery_event?(params) + params[:type] == 'message-delivered' || params[:type] == 'message-failed' end end diff --git a/app/services/sms/delivery_status_service.rb b/app/services/sms/delivery_status_service.rb new file mode 100644 index 000000000..dd071e242 --- /dev/null +++ b/app/services/sms/delivery_status_service.rb @@ -0,0 +1,52 @@ +class Sms::DeliveryStatusService + pattr_initialize [:inbox!, :params!] + + def perform + return unless supported_status? + + process_status if message.present? + end + + private + + def process_status + @message.status = status + @message.external_error = external_error if error_occurred? + @message.save! + end + + def supported_status? + %w[message-delivered message-failed].include?(params[:type]) + end + + # Relevant documentation: + # https://dev.bandwidth.com/docs/mfa/webhooks/international/message-delivered + # https://dev.bandwidth.com/docs/mfa/webhooks/international/message-failed + def status + type_mapping = { + 'message-delivered' => 'delivered', + 'message-failed' => 'failed' + } + + type_mapping[params[:type]] + end + + def external_error + return nil unless error_occurred? + + error_message = params[:description] + error_code = params[:errorCode] + + "#{error_code} - #{error_message}" + end + + def error_occurred? + params[:errorCode] && params[:type] == 'message-failed' + end + + def message + return unless params[:message][:id] + + @message ||= inbox.messages.find_by(source_id: params[:message][:id]) + end +end diff --git a/spec/jobs/webhooks/sms_events_job_spec.rb b/spec/jobs/webhooks/sms_events_job_spec.rb index 3874ba24d..305601176 100644 --- a/spec/jobs/webhooks/sms_events_job_spec.rb +++ b/spec/jobs/webhooks/sms_events_job_spec.rb @@ -43,7 +43,7 @@ RSpec.describe Webhooks::SmsEventsJob do end context 'when valid params' do - it 'calls Sms::IncomingMessageService' do + it 'calls Sms::IncomingMessageService if the message type is message-received' do process_service = double allow(Sms::IncomingMessageService).to receive(:new).and_return(process_service) allow(process_service).to receive(:perform) @@ -52,5 +52,34 @@ RSpec.describe Webhooks::SmsEventsJob do expect(process_service).to receive(:perform) described_class.perform_now(params) end + + it 'calls Sms::DeliveryStatusService if the message type is message-delivered' do + params[:type] = 'message-delivered' + process_service = double + allow(Sms::DeliveryStatusService).to receive(:new).and_return(process_service) + allow(process_service).to receive(:perform) + expect(Sms::DeliveryStatusService).to receive(:new).with(channel: sms_channel, + params: params[:message].with_indifferent_access) + expect(process_service).to receive(:perform) + described_class.perform_now(params) + end + + it 'calls Sms::DeliveryStatusService if the message type is message-failed' do + params[:type] = 'message-failed' + process_service = double + allow(Sms::DeliveryStatusService).to receive(:new).and_return(process_service) + allow(process_service).to receive(:perform) + expect(Sms::DeliveryStatusService).to receive(:new).with(channel: sms_channel, + params: params[:message].with_indifferent_access) + expect(process_service).to receive(:perform) + described_class.perform_now(params) + end + + it 'does not call any service if the message type is not supported' do + params[:type] = 'message-sent' + expect(Sms::IncomingMessageService).not_to receive(:new) + expect(Sms::DeliveryStatusService).not_to receive(:new) + described_class.perform_now(params) + end end end diff --git a/spec/services/sms/delivery_status_service_spec.rb b/spec/services/sms/delivery_status_service_spec.rb new file mode 100644 index 000000000..c60f0146a --- /dev/null +++ b/spec/services/sms/delivery_status_service_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +describe Sms::DeliveryStatusService do + describe '#perform' do + let!(:account) { create(:account) } + let!(:sms_channel) { create(:channel_sms) } + let!(:contact) { create(:contact, account: account, phone_number: '+12345') } + let(:contact_inbox) { create(:contact_inbox, source_id: '+12345', contact: contact, inbox: sms_channel.inbox) } + let!(:conversation) { create(:conversation, contact: contact, inbox: sms_channel.inbox, contact_inbox: contact_inbox) } + + describe '#perform' do + context 'when message delivery status is fired' do + before do + create(:message, account: account, inbox: sms_channel.inbox, conversation: conversation, status: :sent, + source_id: 'SMd560ac79e4a4d36b3ce59f1f50471986') + end + + it 'updates the message if the message status is delivered' do + params = { + time: '2022-02-02T23:14:05.309Z', + type: 'message-delivered', + to: sms_channel.phone_number, + description: 'ok', + message: { + 'id': conversation.messages.last.source_id + } + } + + described_class.new(params: params, inbox: sms_channel.inbox).perform + expect(conversation.reload.messages.last.status).to eq('delivered') + end + + it 'updates the message if the message status is failed' do + params = { + time: '2022-02-02T23:14:05.309Z', + type: 'message-failed', + to: sms_channel.phone_number, + description: 'Undeliverable', + errorCode: 995, + message: { + 'id': conversation.messages.last.source_id + } + } + + described_class.new(params: params, inbox: sms_channel.inbox).perform + expect(conversation.reload.messages.last.status).to eq('failed') + + expect(conversation.reload.messages.last.external_error).to eq('995 - Undeliverable') + end + + it 'does not update the message if the status is not a support status' do + params = { + time: '2022-02-02T23:14:05.309Z', + type: 'queued', + to: sms_channel.phone_number, + description: 'ok', + message: { + 'id': conversation.messages.last.source_id + } + } + + described_class.new(params: params, inbox: sms_channel.inbox).perform + expect(conversation.reload.messages.last.status).to eq('sent') + end + + it 'does not update the message if the message is not present' do + params = { + time: '2022-02-02T23:14:05.309Z', + type: 'message-delivered', + to: sms_channel.phone_number, + description: 'ok', + message: { + 'id': '123' + } + } + + described_class.new(params: params, inbox: sms_channel.inbox).perform + expect(conversation.reload.messages.last.status).to eq('sent') + end + end + end + end +end