feat(inboxes): add inbox disable toggle

This commit is contained in:
Muhsin
2026-06-09 21:07:05 +04:00
parent 8a3b129292
commit cbd24dbdb2
49 changed files with 239 additions and 19 deletions
@@ -3,6 +3,8 @@ class Campaigns::CampaignConversationBuilder
def perform
@contact_inbox = ContactInbox.find(@contact_inbox_id)
return unless @contact_inbox.inbox.active?
@campaign = @contact_inbox.inbox.campaigns.find_by!(display_id: campaign_display_id)
ActiveRecord::Base.transaction do
+2
View File
@@ -2,6 +2,8 @@ class ConversationBuilder
pattr_initialize [:params!, :contact_inbox!]
def perform
raise CustomExceptions::Inbox::Disabled unless @contact_inbox.inbox.active?
look_up_exising_conversation || create_new_conversation
end
@@ -20,6 +20,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
def perform
# This channel might require reauthorization, may be owner might have changed the fb password
return if @inbox.channel.reauthorization_required?
return unless @inbox.active?
ActiveRecord::Base.transaction do
build_contact_inbox
@@ -10,6 +10,7 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
def perform
return if @inbox.channel.reauthorization_required?
return unless @inbox.active?
ActiveRecord::Base.transaction do
build_message
+3 -2
View File
@@ -22,14 +22,15 @@ class Messages::MessageBuilder
end
def perform
raise CustomExceptions::Inbox::Disabled unless @conversation.inbox.active?
@message = @conversation.messages.build(message_params)
process_attachments
process_emails
# When the message has no quoted content, it will just be rendered as a regular message
# The frontend is equipped to handle this case
process_email_content
@message.save!
@message
@message.tap(&:save!)
end
private
@@ -1,5 +1,6 @@
class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
before_action :ensure_api_inbox, only: :update
before_action :ensure_inbox_active, only: [:create, :retry]
def index
@messages = message_finder.perform
@@ -9,6 +10,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
user = Current.user || @resource
mb = Messages::MessageBuilder.new(user, @conversation, params)
@message = mb.perform
rescue CustomExceptions::Inbox::Disabled
render_inbox_disabled_error
rescue StandardError => e
render_could_not_create_error(e.message)
end
@@ -32,6 +35,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
service.perform
message.update!(content_attributes: {})
::SendReplyJob.perform_later(message.id)
rescue CustomExceptions::Inbox::Disabled
render_inbox_disabled_error
rescue StandardError => e
render_could_not_create_error(e.message)
end
@@ -77,4 +82,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
# Only API inboxes can update messages
render json: { error: 'Message status update is only allowed for API inboxes' }, status: :forbidden unless @conversation.inbox.api?
end
def ensure_inbox_active
render_inbox_disabled_error unless @conversation.inbox.active?
end
end
@@ -197,6 +197,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@inbox = Current.account.inboxes.find(params[:inbox_id])
authorize @inbox, :show?
render_inbox_disabled_error unless @inbox.active?
end
def contact
@@ -213,6 +214,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
# and deprecate the support of passing only source_id as the param
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
authorize @contact_inbox.inbox, :show?
render_inbox_disabled_error unless @contact_inbox.inbox.active?
rescue ActiveRecord::RecordNotUnique
render json: { error: 'source_id should be unique' }, status: :unprocessable_entity
end
@@ -220,12 +222,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def build_contact_inbox
return if @inbox.blank? || @contact.blank?
ContactInboxBuilder.new(
contact: @contact,
inbox: @inbox,
source_id: params[:source_id],
hmac_verified: hmac_verified?
).perform
ContactInboxBuilder.new(contact: @contact, inbox: @inbox, source_id: params[:source_id], hmac_verified: hmac_verified?).perform
end
def conversation_finder
@@ -157,7 +157,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
[:name, :avatar, :active, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name,
{ csat_config: [:display_type, :message, :button_text, :language,
@@ -3,6 +3,7 @@ class Api::V1::Widget::BaseController < ApplicationController
include WebsiteTokenHelper
before_action :set_web_widget
before_action :ensure_inbox_active
before_action :set_contact
private
@@ -86,4 +87,8 @@ class Api::V1::Widget::BaseController < ApplicationController
message_type: :incoming
}
end
def ensure_inbox_active
render_inbox_disabled_error unless @web_widget.inbox.active?
end
end
@@ -1,6 +1,10 @@
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].freeze
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
before_action :ensure_inbox_active, only: DISABLED_INBOX_ACTIONS
def index
@conversation = conversation
@@ -3,6 +3,7 @@ module RequestExceptionHandler
included do
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
rescue_from CustomExceptions::Inbox::Disabled, with: :render_inbox_disabled_error
end
private
@@ -35,6 +36,13 @@ module RequestExceptionHandler
render json: { error: message }, status: :unprocessable_entity
end
def render_inbox_disabled_error(_exception = nil)
render json: {
error: 'inbox_disabled',
message: 'This inbox is currently disabled'
}, status: :forbidden
end
def render_payment_required(message)
render json: { error: message }, status: :payment_required
end
@@ -0,0 +1,34 @@
class Platform::Api::V1::InboxesController < PlatformController
before_action :set_account
before_action :validate_account_permissible
before_action :set_inbox
def disable
update_active_state(false)
end
def enable
update_active_state(true)
end
private
def set_account
@account = Account.find(params[:account_id])
end
def validate_account_permissible
return if @platform_app.platform_app_permissibles.find_by(permissible: @account)
render json: { error: 'Non permissible resource' }, status: :unauthorized
end
def set_inbox
@inbox = @account.inboxes.find(params[:id])
end
def update_active_state(active)
@inbox.update!(active: active)
render json: { success: true, inbox_id: @inbox.id, active: @inbox.active }
end
end
@@ -1,5 +1,6 @@
class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesController
before_action :contact_inbox, except: [:create]
before_action :ensure_inbox_active, only: [:create, :update]
before_action :process_hmac
def show; end
@@ -45,4 +46,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
def permitted_params
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
end
def ensure_inbox_active
render_inbox_disabled_error unless @inbox_channel.inbox.active?
end
end
@@ -1,6 +1,7 @@
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
include Events::Types
before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status]
before_action :ensure_inbox_active, only: [:create, :toggle_typing, :toggle_status, :update_last_seen]
def index
@conversations = @contact_inbox.hmac_verified? ? @contact_inbox.contact.conversations : @contact_inbox.conversations
@@ -64,4 +65,8 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
def conversation_params
params.permit(custom_attributes: {})
end
def ensure_inbox_active
render_inbox_disabled_error unless @contact_inbox.inbox.active?
end
end
@@ -1,5 +1,6 @@
class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesController
before_action :set_message, only: [:update]
before_action :ensure_inbox_active, only: [:create, :update]
def index
@messages = @conversation.nil? ? [] : message_finder.perform
@@ -70,4 +71,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
def check_csat_locked
(Time.zone.now.to_date - @message.created_at.to_date).to_i > 14 and @message.content_type == 'input_csat'
end
def ensure_inbox_active
render_inbox_disabled_error unless @conversation.inbox.active?
end
end
+5
View File
@@ -5,6 +5,7 @@ class WidgetsController < ActionController::Base
before_action :set_global_config
before_action :set_web_widget
before_action :ensure_account_is_active
before_action :ensure_inbox_active
before_action :ensure_location_is_supported
before_action :set_token
before_action :set_contact
@@ -62,6 +63,10 @@ class WidgetsController < ActionController::Base
render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active?
end
def ensure_inbox_active
head :not_found unless @web_widget.inbox.active?
end
def ensure_location_is_supported; end
def additional_attributes
@@ -10,10 +10,16 @@ defineProps({
</script>
<template>
<div :title="inbox.name" class="flex items-center gap-0.5 min-w-0">
<div :title="inbox.name" class="flex items-center gap-1 min-w-0">
<ChannelIcon :inbox="inbox" class="size-4 flex-shrink-0 text-n-slate-11" />
<span class="truncate text-body-main text-n-slate-11">
{{ inbox.name }}
</span>
<span
v-if="inbox.active === false"
class="shrink-0 rounded-md bg-n-ruby-3 px-1 py-0.5 text-xs font-medium text-n-ruby-11"
>
{{ $t('INBOX_MGMT.DISABLED') }}
</span>
</div>
</template>
@@ -27,6 +27,10 @@ const props = defineProps({
const reauthorizationRequired = computed(() => {
return props.inbox.reauthorization_required;
});
const inboxDisabled = computed(() => {
return props.inbox.active === false;
});
</script>
<template>
@@ -34,6 +38,13 @@ const reauthorizationRequired = computed(() => {
<ChannelIcon :inbox="inbox" class="size-4" />
</span>
<div class="flex-1 truncate min-w-0">{{ label }}</div>
<div
v-if="inboxDisabled"
v-tooltip.top-end="$t('INBOX_MGMT.DISABLED')"
class="grid place-content-center size-5 bg-n-ruby-3 rounded-full"
>
<Icon icon="i-woot-alert" class="size-3 text-n-ruby-9" />
</div>
<SidebarUnreadBadge :count="badgeCount" />
<div
v-if="reauthorizationRequired"
@@ -10,10 +10,16 @@ defineProps({
</script>
<template>
<div :title="inbox.name" class="flex items-center gap-0.5 min-w-0">
<div :title="inbox.name" class="flex items-center gap-1 min-w-0">
<ChannelIcon :inbox="inbox" class="size-4 flex-shrink-0 text-n-slate-11" />
<span class="truncate text-label-small text-n-slate-11">
{{ inbox.name }}
</span>
<span
v-if="inbox.active === false"
class="shrink-0 rounded-md bg-n-ruby-3 px-1 py-0.5 text-xs font-medium text-n-ruby-11"
>
{{ $t('INBOX_MGMT.DISABLED') }}
</span>
</div>
</template>
@@ -170,8 +170,14 @@ export default {
instagramInbox
);
},
isInboxDisabled() {
return this.inbox.active === false;
},
replyWindowBannerMessage() {
if (this.isInboxDisabled) {
return this.$t('CONVERSATION.INBOX_DISABLED');
}
if (this.isAWhatsAppChannel) {
return this.$t('CONVERSATION.TWILIO_WHATSAPP_CAN_REPLY');
}
@@ -194,6 +200,9 @@ export default {
return this.$t('CONVERSATION.CANNOT_REPLY');
},
replyWindowLink() {
if (this.isInboxDisabled) {
return '';
}
if (this.isAFacebookInbox || this.isAnInstagramChannel) {
return REPLY_POLICY.FACEBOOK;
}
@@ -209,6 +218,9 @@ export default {
return '';
},
replyWindowLinkText() {
if (this.isInboxDisabled) {
return '';
}
if (
this.isAWhatsAppChannel ||
this.isAFacebookInbox ||
@@ -455,7 +467,7 @@ export default {
>
<div ref="topBannerRef">
<Banner
v-if="!currentChat.can_reply"
v-if="isInboxDisabled || !currentChat.can_reply"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="replyWindowBannerMessage"
@@ -208,6 +208,9 @@ export default {
},
messagePlaceHolder() {
if (this.isEditorDisabled) {
if (this.inbox.active === false) {
return this.$t('CONVERSATION.FOOTER.MESSAGING_RESTRICTED_INBOX');
}
if (this.isAWhatsAppChannel) {
return this.$t('CONVERSATION.FOOTER.MESSAGING_RESTRICTED_WHATSAPP');
}
@@ -438,6 +441,10 @@ export default {
return !this.showAudioRecorderEditor && !this.copilot.isActive.value;
},
isEditorDisabled() {
if (this.inbox.active === false) {
return true;
}
return (
(this.isAWhatsAppChannel || this.isAPIInbox) &&
!this.isOnPrivateNote &&
@@ -31,6 +31,7 @@
"LOADING_INBOXES": "Loading inboxes",
"LOADING_CONVERSATIONS": "Loading Conversations",
"CANNOT_REPLY": "You cannot reply due to",
"INBOX_DISABLED": "This inbox is disabled. You cannot send messages.",
"24_HOURS_WINDOW": "24 hour message window restriction",
"48_HOURS_WINDOW": "48 hour message window restriction",
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
@@ -211,6 +212,7 @@
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents",
"MESSAGING_RESTRICTED": "You cannot reply to this conversation",
"MESSAGING_RESTRICTED_INBOX": "This inbox is disabled. You cannot send messages.",
"MESSAGING_RESTRICTED_WHATSAPP": "You can only reply using a template message due to 24-hour message window restriction",
"MESSAGING_RESTRICTED_API": "You can only reply using a template message due to message window restriction",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
@@ -743,6 +743,7 @@
}
},
"SETTINGS": "Settings",
"DISABLED": "Disabled",
"FEATURES": {
"LABEL": "Features",
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
@@ -772,6 +773,10 @@
},
"INBOX_AGENTS": "Agents",
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
"INBOX_ACTIVE": {
"TITLE": "Enable inbox",
"DESCRIPTION": "When disabled, this inbox stays visible but stops new conversations, incoming messages, replies, private notes, automations, and bot processing."
},
"AGENT_ASSIGNMENT": "Conversation Assignment",
"AGENT_ASSIGNMENT_SUB_TEXT": "Update conversation assignment settings",
"UPDATE": "Update",
@@ -139,9 +139,19 @@ const openDelete = inbox => {
<ChannelIcon class="size-6 text-n-slate-10" :inbox="inbox" />
</div>
<div class="flex flex-col items-start gap-1">
<span class="block text-heading-3 text-n-slate-12 capitalize">
{{ inbox.name }}
</span>
<div class="flex items-center gap-2 min-w-0">
<span
class="block text-heading-3 text-n-slate-12 capitalize truncate"
>
{{ inbox.name }}
</span>
<span
v-if="inbox.active === false"
class="shrink-0 rounded-md bg-n-ruby-3 px-1.5 py-0.5 text-xs font-medium text-n-ruby-11"
>
{{ $t('INBOX_MGMT.DISABLED') }}
</span>
</div>
<ChannelName
:channel-type="inbox.channel_type"
:medium="inbox.medium"
@@ -91,6 +91,7 @@ export default {
senderNameType: 'friendly',
businessName: '',
locktoSingleConversation: false,
inboxActive: true,
allowMessagesAfterResolved: true,
continuityViaEmail: true,
selectedInboxName: '',
@@ -470,6 +471,7 @@ export default {
this.businessName = this.inbox.business_name;
this.allowMessagesAfterResolved =
this.inbox.allow_messages_after_resolved;
this.inboxActive = this.inbox.active !== false;
this.continuityViaEmail = this.inbox.continuity_via_email;
this.channelWebsiteUrl = this.inbox.website_url;
this.channelWelcomeTitle = this.inbox.welcome_title;
@@ -581,6 +583,7 @@ export default {
const payload = {
id: this.currentInboxId,
name: this.selectedInboxName?.trim(),
active: this.inboxActive,
enable_email_collect: this.emailCollectEnabled,
allow_messages_after_resolved: this.allowMessagesAfterResolved,
greeting_enabled: this.greetingEnabled,
@@ -819,6 +822,15 @@ export default {
/>
</SettingsFieldSection>
<SettingsToggleSection
v-model="inboxActive"
:header="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_ACTIVE.TITLE')"
:description="
$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_ACTIVE.DESCRIPTION')
"
class="mb-4"
/>
<SettingsFieldSection
v-if="isAWebWidgetInbox"
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL')"
@@ -13,6 +13,7 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
def should_fetch_emails?(inbox)
return false if inbox.account.suspended?
return false unless inbox.active?
return false unless inbox.channel.imap_enabled
return false if inbox.channel.reauthorization_required?
+1 -1
View File
@@ -25,7 +25,7 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
private
def should_fetch_email?(channel)
channel.imap_enabled? && !channel.reauthorization_required?
channel.inbox.active? && channel.imap_enabled? && !channel.reauthorization_required?
end
def process_email_for_channel(channel, interval)
+2
View File
@@ -17,6 +17,8 @@ class SendReplyJob < ApplicationJob
def perform(message_id)
message = Message.find(message_id)
return unless message.inbox.active?
channel_name = message.conversation.inbox.channel.class.to_s
return send_on_facebook_page(message) if channel_name == 'Channel::FacebookPage'
@@ -56,6 +56,7 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
channel = find_channel(instagram_id)
next if channel.blank?
next unless channel.inbox.active?
if (event_name = event_name(messaging))
send(event_name, messaging, channel)
+1
View File
@@ -4,6 +4,7 @@ class Webhooks::LineEventsJob < ApplicationJob
def perform(params: {}, signature: '', post_body: '')
@params = params
return unless valid_event_payload?
return unless @channel.inbox.active?
return unless valid_post_body?(post_body, signature)
Line::IncomingMessageService.new(inbox: @channel.inbox, params: @params['line'].with_indifferent_access).perform
+1
View File
@@ -8,6 +8,7 @@ class Webhooks::SmsEventsJob < ApplicationJob
channel = Channel::Sms.find_by(phone_number: params[:to])
return unless channel
return unless channel.inbox.active?
process_event_params(channel, params)
end
+1
View File
@@ -19,6 +19,7 @@ class Webhooks::TelegramEventsJob < ApplicationJob
def channel_is_inactive?(channel)
return true if channel.blank?
return true unless channel.account.active?
return true unless channel.inbox.active?
false
end
+1
View File
@@ -21,6 +21,7 @@ class Webhooks::TiktokEventsJob < MutexApplicationJob
def channel_is_inactive?
return true if channel.blank?
return true unless channel.account.active?
return true unless channel.inbox.active?
false
end
+1
View File
@@ -129,6 +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?
false
end
+2
View File
@@ -63,6 +63,8 @@ class AgentBotListener < BaseListener
private
def agent_bots_for(inbox, conversation = nil)
return [] unless inbox.active?
bots = []
bots << conversation.assignee_agent_bot if conversation&.assignee_agent_bot.present?
inbox_bot = active_inbox_agent_bot(inbox)
+8 -2
View File
@@ -18,6 +18,7 @@ class AutomationRuleListener < BaseListener
def message_created(event)
message = event.data[:message]
return unless message.inbox.active?
return if ignore_message_created_event?(event)
account = message.try(:account)
@@ -39,10 +40,11 @@ class AutomationRuleListener < BaseListener
def process_conversation_event(event, event_name)
return if performed_by_automation?(event)
auto_reply_skip_events = %w[conversation_created conversation_opened]
return if auto_reply_skip_events.include?(event_name) && ignore_auto_reply_event?(event)
return if auto_reply_event?(event, event_name)
conversation = event.data[:conversation]
return unless conversation.inbox.active?
account = conversation.account
changed_attributes = event.data[:changed_attributes]
@@ -74,6 +76,10 @@ class AutomationRuleListener < BaseListener
event.data[:performed_by].present? && event.data[:performed_by].instance_of?(AutomationRule)
end
def auto_reply_event?(event, event_name)
%w[conversation_created conversation_opened].include?(event_name) && ignore_auto_reply_event?(event)
end
def ignore_auto_reply_event?(event)
conversation = event.data[:conversation]
conversation.additional_attributes['auto_reply'].present?
@@ -7,6 +7,8 @@ class Line::IncomingMessageService
LINE_STICKER_IMAGE_URL = 'https://stickershop.line-scdn.net/stickershop/v1/sticker/%s/android/sticker.png'.freeze
def perform
return unless @inbox.active?
# probably test events
return if params[:events].blank?
@@ -4,6 +4,8 @@ class Sms::IncomingMessageService
pattr_initialize [:inbox!, :params!]
def perform
return unless @inbox.active?
set_contact
set_conversation
@message = @conversation.messages.create!(
@@ -7,6 +7,8 @@ class Telegram::IncomingMessageService
pattr_initialize [:inbox!, :params!]
def perform
return unless @inbox.active?
# chatwoot doesn't support group conversations at the moment
transform_business_message!
return unless private_message?
+2
View File
@@ -4,6 +4,8 @@ class Tiktok::MessageService
pattr_initialize [:channel!, :content!, :outgoing_echo]
def perform
return unless channel.inbox.active?
if outgoing_message?
message = find_message(tt_conversation_id, tt_message_id)
return if message.present?
@@ -6,6 +6,7 @@ class Twilio::IncomingMessageService
def perform
return if twilio_channel.blank?
return unless inbox.active?
set_contact
set_conversation
@@ -5,6 +5,8 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
return if source_app_id == parent_app_id
set_inbox
return unless @inbox.active?
ensure_contacts
set_conversation
@message = @conversation.messages.create!(
@@ -4,6 +4,7 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
def perform
set_inbox
return unless @inbox.active?
return if !tweets_enabled? || message_already_exist? || user_has_blocked?
create_message
@@ -8,6 +8,8 @@ class Whatsapp::IncomingMessageBaseService
pattr_initialize [:inbox!, :params!, :outgoing_echo]
def perform
return unless @inbox.active?
processed_params
if processed_params.try(:[], :statuses).present?
@@ -2,6 +2,7 @@ json.id resource.id
json.avatar_url resource.try(:avatar_url)
json.channel_id resource.channel_id
json.name resource.name
json.active resource.active
json.channel_type resource.channel_type
json.greeting_enabled resource.greeting_enabled
json.greeting_message resource.greeting_message
+6
View File
@@ -556,6 +556,12 @@ Rails.application.routes.draw do
end
end
resources :email_channel_migrations, only: [:create]
resources :inboxes, only: [] do
member do
post :disable
post :enable
end
end
end
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddActiveToInboxes < ActiveRecord::Migration[7.0]
def change
add_column :inboxes, :active, :boolean, default: true, null: false
end
end
+2 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2026_05_25_093000) do
ActiveRecord::Schema[7.1].define(version: 2026_06_09_090000) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -898,6 +898,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_05_25_093000) do
t.boolean "enable_email_collect", default: true
t.boolean "csat_survey_enabled", default: false
t.boolean "allow_messages_after_resolved", default: true
t.boolean "active", default: true, null: false
t.jsonb "auto_assignment_config", default: {}
t.boolean "lock_to_single_conversation", default: false, null: false
t.bigint "portal_id"
+19
View File
@@ -0,0 +1,19 @@
# frozen_string_literal: true
class CustomExceptions::Inbox::Disabled < CustomExceptions::Base
def initialize(data = {})
super
end
def message
'This inbox is currently disabled'
end
def error_code
'inbox_disabled'
end
def http_status
:forbidden
end
end