From 2abb7759f2912411775db34026630cb6b844c77c Mon Sep 17 00:00:00 2001 From: Muhsin <12408980+muhsin-k@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:53:33 +0400 Subject: [PATCH] fix(inboxes): preserve disabled inbox status updates --- .../api/v1/widget/conversations_controller.rb | 2 +- app/jobs/send_reply_job.rb | 28 +++++++++++++-- app/jobs/webhooks/whatsapp_events_job.rb | 10 +++++- .../whatsapp/incoming_call_service.rb | 2 +- .../widget/conversations_controller_spec.rb | 16 +++++++++ .../whatsapp/incoming_call_service_spec.rb | 13 +++++++ spec/jobs/send_reply_job_spec.rb | 35 ++++++++++++++++++- .../jobs/webhooks/whatsapp_events_job_spec.rb | 21 +++++++++++ 8 files changed, 121 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 283b67395..af4e55016 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -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] diff --git a/app/jobs/send_reply_job.rb b/app/jobs/send_reply_job.rb index fb6bcddae..90ed2e999 100644 --- a/app/jobs/send_reply_job.rb +++ b/app/jobs/send_reply_job.rb @@ -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 diff --git a/app/jobs/webhooks/whatsapp_events_job.rb b/app/jobs/webhooks/whatsapp_events_job.rb index f82562b5d..32f64f124 100644 --- a/app/jobs/webhooks/whatsapp_events_job.rb +++ b/app/jobs/webhooks/whatsapp_events_job.rb @@ -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 diff --git a/enterprise/app/services/whatsapp/incoming_call_service.rb b/enterprise/app/services/whatsapp/incoming_call_service.rb index ea08c8415..5d001c814 100644 --- a/enterprise/app/services/whatsapp/incoming_call_service.rb +++ b/enterprise/app/services/whatsapp/incoming_call_service.rb @@ -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 diff --git a/spec/controllers/api/v1/widget/conversations_controller_spec.rb b/spec/controllers/api/v1/widget/conversations_controller_spec.rb index ab01c2ca3..7d804dfda 100644 --- a/spec/controllers/api/v1/widget/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/widget/conversations_controller_spec.rb @@ -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 diff --git a/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb b/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb index cf031f1ca..63ae8da63 100644 --- a/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb +++ b/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb @@ -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) } diff --git a/spec/jobs/send_reply_job_spec.rb b/spec/jobs/send_reply_job_spec.rb index 32a35d6d4..c1da274ae 100644 --- a/spec/jobs/send_reply_job_spec.rb +++ b/spec/jobs/send_reply_job_spec.rb @@ -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 diff --git a/spec/jobs/webhooks/whatsapp_events_job_spec.rb b/spec/jobs/webhooks/whatsapp_events_job_spec.rb index a074dadc8..1008b78f2 100644 --- a/spec/jobs/webhooks/whatsapp_events_job_spec.rb +++ b/spec/jobs/webhooks/whatsapp_events_job_spec.rb @@ -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