fix(inboxes): preserve disabled inbox status updates

This commit is contained in:
Muhsin
2026-07-22 17:53:33 +04:00
parent ca1069241f
commit 2abb7759f2
8 changed files with 121 additions and 6 deletions
@@ -2,7 +2,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
include Events::Types
DISABLED_INBOX_ACTIONS = [
:create, :toggle_typing, :toggle_status, :update_last_seen, :set_custom_attributes, :destroy_custom_attributes, :transcript
:create, :toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes, :transcript
].freeze
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
+26 -2
View File
@@ -19,9 +19,8 @@ class SendReplyJob < ApplicationJob
def perform(message_id)
message = Message.find(message_id)
return mark_message_failed(message) unless message.inbox.active?
channel_name = message.conversation.inbox.channel.class.to_s
return handle_disabled_inbox(message, channel_name) unless message.inbox.active?
return send_on_facebook_page(message) if channel_name == 'Channel::FacebookPage'
@@ -33,6 +32,31 @@ class SendReplyJob < ApplicationJob
private
def handle_disabled_inbox(message, channel_name)
return unless send_service_available?(channel_name)
return unless deliverable_reply?(message, channel_name)
mark_message_failed(message)
end
def send_service_available?(channel_name)
channel_name == 'Channel::FacebookPage' || CHANNEL_SERVICES.key?(channel_name)
end
def deliverable_reply?(message, channel_name)
return false if message.private?
return false unless message.outgoing? || message.template?
return false if message.source_id.present?
return false if message.content_type == 'voice_call'
return message.email_notifiable_message? if email_delivery_channel?(channel_name)
true
end
def email_delivery_channel?(channel_name)
%w[Channel::Email Channel::WebWidget Channel::Api].include?(channel_name)
end
def mark_message_failed(message)
Messages::StatusUpdateService.new(message, 'failed', INBOX_DISABLED_ERROR).perform
end
+9 -1
View File
@@ -129,7 +129,7 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob
# Only skip for embedded signup when reauth is required; manual flow uses API keys and should still receive webhooks
return true if channel.reauthorization_required? && embedded_signup_channel?(channel)
return true unless channel.account.active?
return true unless channel.inbox.active? || status_update_event?(params)
return true unless channel.inbox.active? || existing_message_update_event?(params)
false
end
@@ -139,6 +139,14 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob
value[:statuses].present?
end
def existing_message_update_event?(params)
status_update_event?(params) || call_event?(params)
end
def call_event?(params)
params.dig(:entry, 0, :changes, 0, :field) == 'calls'
end
def embedded_signup_channel?(channel)
(channel.provider_config || {}).to_h['source'] == 'embedded_signup'
end
@@ -75,7 +75,7 @@ class Whatsapp::IncomingCallService
end
def create_inbound_call(payload)
unless inbox.channel.inbound_calls_enabled?
unless inbox.active? && inbox.channel.inbound_calls_enabled?
Rails.logger.info "[WHATSAPP CALL] Inbound calls disabled for inbox #{inbox.id}; rejecting call #{payload[:id]}"
inbox.channel.provider_service.reject_call(payload[:id])
return
@@ -269,6 +269,22 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
expect(conversation.reload.contact_last_seen_at).not_to be_nil
end
it 'updates last seen when the inbox is disabled' do
current_time = DateTime.now.utc
allow(DateTime).to receive(:now).and_return(current_time)
web_widget.inbox.update!(active: false)
expect(Conversations::UpdateMessageStatusJob).to receive(:perform_later).with(conversation.id, current_time)
post '/api/v1/widget/conversations/update_last_seen',
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.contact_last_seen_at).not_to be_nil
end
end
end
@@ -44,6 +44,19 @@ describe Whatsapp::IncomingCallService do
end
end
context 'when the inbox is disabled' do
it 'rejects the call with Meta without creating a Call or Conversation' do
inbox.update!(active: false)
provider_service = instance_double(Whatsapp::Providers::WhatsappCloudService, reject_call: true)
allow(inbox.channel).to receive(:provider_service).and_return(provider_service)
params = call_payload(event: 'connect', session: { sdp: "v=0\r\n...sdp...", sdp_type: 'offer' })
expect { described_class.new(inbox: inbox, params: params).perform }
.to not_change(Call, :count).and not_change(Conversation, :count)
expect(provider_service).to have_received(:reject_call).with(provider_call_id)
end
end
describe 'inbound connect' do
let(:sdp_offer) { "v=0\r\n...sdp..." }
let!(:agent) { create(:user, account: account) }
+34 -1
View File
@@ -125,7 +125,7 @@ RSpec.describe SendReplyJob do
it 'marks the message as failed when the inbox is disabled before the queued send runs' do
twilio_channel = create(:channel_twilio_sms)
message = create(:message, conversation: create(:conversation, inbox: twilio_channel.inbox))
message = create(:message, message_type: :outgoing, conversation: create(:conversation, inbox: twilio_channel.inbox))
twilio_channel.inbox.update!(active: false)
expect(Twilio::SendOnTwilioService).not_to receive(:new)
@@ -135,5 +135,38 @@ RSpec.describe SendReplyJob do
expect(message.reload).to be_failed
expect(message.external_error).to eq('This inbox is currently disabled')
end
it 'does not mark incoming messages as failed when the inbox is disabled' do
twilio_channel = create(:channel_twilio_sms)
message = create(:message, message_type: :incoming, conversation: create(:conversation, inbox: twilio_channel.inbox))
twilio_channel.inbox.update!(active: false)
described_class.perform_now(message.id)
expect(message.reload).not_to be_failed
expect(message.external_error).to be_nil
end
it 'does not mark private notes as failed when the inbox is disabled' do
twilio_channel = create(:channel_twilio_sms)
message = create(:message, private: true, conversation: create(:conversation, inbox: twilio_channel.inbox))
twilio_channel.inbox.update!(active: false)
described_class.perform_now(message.id)
expect(message.reload).not_to be_failed
expect(message.external_error).to be_nil
end
it 'does not mark provider echo messages as failed when the inbox is disabled' do
twilio_channel = create(:channel_twilio_sms)
message = create(:message, source_id: 'provider-message-id', conversation: create(:conversation, inbox: twilio_channel.inbox))
twilio_channel.inbox.update!(active: false)
described_class.perform_now(message.id)
expect(message.reload).not_to be_failed
expect(message.external_error).to be_nil
end
end
end
@@ -134,6 +134,27 @@ RSpec.describe Webhooks::WhatsappEventsJob do
job.perform_now(wb_params)
end
it 'processes call callbacks when the inbox is disabled' do
wb_params = params.deep_dup
wb_params[:entry].first[:changes].first[:field] = 'calls'
wb_params[:entry].first[:changes].first[:value][:calls] = [
{ id: 'wacid-test', from: '919745786257', event: 'connect', session: { sdp_type: 'offer' } }
]
channel.inbox.update!(active: false)
allow(Whatsapp::IncomingCallService).to receive(:new).and_return(process_service)
expect(Whatsapp::IncomingCallService).to receive(:new).with(
inbox: channel.inbox,
params: {
calls: [wb_params[:entry].first[:changes].first[:value][:calls].first],
contacts: nil
}
)
job.perform_now(wb_params)
end
it 'uses from_user_id as the mutex sender for BSUID-only inbound messages' do
bsuid = 'IN.2081978709342942'
wb_params = params.deep_dup