Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
949a728977 | ||
|
|
2437da5339 | ||
|
|
2abb7759f2 | ||
|
|
ca1069241f | ||
|
|
a744b1e247 | ||
|
|
559a052e42 | ||
|
|
f855429d9a | ||
|
|
97b30127a6 | ||
|
|
3a7c418139 | ||
|
|
f2e3e5a83f | ||
|
|
23fac0e7b1 | ||
|
|
02ff710709 | ||
|
|
3f9864c74f | ||
|
|
0b9b137723 | ||
|
|
42e392eca4 | ||
|
|
e668f9da20 | ||
|
|
65a78c7c20 | ||
|
|
86d0955794 | ||
|
|
d720e013d4 | ||
|
|
2f52af6e2d | ||
|
|
d59415ff5d | ||
|
|
14e4ec81c6 | ||
|
|
cbd24dbdb2 |
@@ -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,6 +2,8 @@ class ConversationBuilder
|
||||
pattr_initialize [:params!, :contact_inbox!]
|
||||
|
||||
def perform
|
||||
raise CustomExceptions::InboxDisabled 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
|
||||
|
||||
@@ -22,14 +22,15 @@ class Messages::MessageBuilder
|
||||
end
|
||||
|
||||
def perform
|
||||
raise CustomExceptions::InboxDisabled 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::InboxDisabled
|
||||
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::InboxDisabled
|
||||
render_inbox_disabled_error
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
@@ -80,4 +85,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
|
||||
|
||||
@@ -198,6 +198,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
|
||||
@@ -214,6 +215,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
|
||||
@@ -221,12 +223,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
|
||||
|
||||
@@ -193,7 +193,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
def normalized_branded_email_layout = params[:branded_email_layout] == 'null' ? nil : params[:branded_email_layout]
|
||||
|
||||
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,
|
||||
|
||||
@@ -90,4 +90,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,4 +1,5 @@
|
||||
class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
||||
before_action :ensure_inbox_active
|
||||
before_action :set_global_config
|
||||
|
||||
def create
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
include WidgetHelper
|
||||
|
||||
before_action :ensure_inbox_active, only: [:update, :set_user, :destroy_custom_attributes]
|
||||
before_action :validate_hmac, only: [:set_user]
|
||||
before_action :validate_hmac_for_identified_update, only: [:update]
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
|
||||
DISABLED_INBOX_ACTIONS = [
|
||||
: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]
|
||||
before_action :ensure_inbox_active, only: DISABLED_INBOX_ACTIONS
|
||||
|
||||
def index
|
||||
@conversation = conversation
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController
|
||||
include WebsiteTokenHelper
|
||||
before_action :set_web_widget
|
||||
before_action :ensure_inbox_active
|
||||
before_action :set_contact
|
||||
|
||||
def create
|
||||
@@ -8,4 +9,15 @@ class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsCon
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_inbox_active
|
||||
return if @web_widget.inbox.active?
|
||||
|
||||
render json: {
|
||||
error: 'inbox_disabled',
|
||||
message: 'This inbox is currently disabled'
|
||||
}, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
|
||||
before_action :ensure_inbox_active
|
||||
|
||||
def create
|
||||
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox,
|
||||
event_info: permitted_params[:event_info].to_h.merge(event_info))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class Api::V1::Widget::Integrations::DyteController < Api::V1::Widget::BaseController
|
||||
before_action :ensure_inbox_active
|
||||
before_action :set_message
|
||||
|
||||
def add_participant_to_meeting
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController
|
||||
before_action :ensure_inbox_active
|
||||
|
||||
def create
|
||||
if conversation.present? && label_defined_in_account?
|
||||
conversation.label_list.add(permitted_params[:label])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
before_action :ensure_inbox_active, only: [:create, :update]
|
||||
before_action :set_conversation, only: [:create]
|
||||
before_action :set_message, only: [:update]
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ module RequestExceptionHandler
|
||||
|
||||
included do
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
rescue_from CustomExceptions::InboxDisabled, with: :render_inbox_disabled_error
|
||||
rescue_from CustomExceptions::Inbox::LimitExceeded, with: :render_error_response
|
||||
end
|
||||
|
||||
@@ -45,6 +46,13 @@ module RequestExceptionHandler
|
||||
render json: { error: sanitized_error_message(error) }, 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
|
||||
@@ -50,4 +51,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]
|
||||
|
||||
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,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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||
|
||||
defineProps({
|
||||
@@ -7,13 +8,21 @@ defineProps({
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
</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="inline-flex h-4 shrink-0 items-center rounded-md bg-n-alpha-2 px-1 text-label-mini font-medium text-n-slate-11"
|
||||
>
|
||||
{{ t('INBOX_MGMT.DISABLED') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||
|
||||
defineProps({
|
||||
@@ -7,13 +8,21 @@ defineProps({
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
</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="inline-flex h-4 shrink-0 items-center rounded-md bg-n-alpha-2 px-1 text-label-mini font-medium text-n-slate-11"
|
||||
>
|
||||
{{ t('INBOX_MGMT.DISABLED') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
|
||||
"LEARN_MORE": "Learn more about inboxes",
|
||||
"COUNT": "{n} inbox | {n} inboxes",
|
||||
"DISABLED": "Disabled",
|
||||
"SEARCH_PLACEHOLDER": "Search inboxes...",
|
||||
"NO_RESULTS": "No inboxes found matching your search",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
|
||||
@@ -142,10 +142,20 @@ 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 flex-col items-start gap-1 min-w-0">
|
||||
<div class="flex items-center gap-1.5 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="inline-flex h-5 shrink-0 items-center rounded-md bg-n-alpha-2 px-1.5 text-label-mini font-medium text-n-slate-11"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.DISABLED') }}
|
||||
</span>
|
||||
</div>
|
||||
<ChannelName
|
||||
:channel-type="inbox.channel_type"
|
||||
:medium="inbox.medium"
|
||||
|
||||
@@ -4,6 +4,7 @@ class ConversationReplyEmailJob < ApplicationJob
|
||||
def perform(conversation_id, last_queued_id)
|
||||
conversation = Conversation.find(conversation_id)
|
||||
return unless conversation.account.active?
|
||||
return unless conversation.inbox.active?
|
||||
|
||||
if conversation.messages.incoming&.last&.content_type == 'incoming_email'
|
||||
ConversationReplyMailer.with(account: conversation.account).reply_without_summary(conversation, last_queued_id).deliver_later
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class SendReplyJob < ApplicationJob
|
||||
queue_as :high
|
||||
|
||||
INBOX_DISABLED_ERROR = 'This inbox is currently disabled'.freeze
|
||||
|
||||
CHANNEL_SERVICES = {
|
||||
'Channel::TwitterProfile' => ::Twitter::SendOnTwitterService,
|
||||
'Channel::TwilioSms' => ::Twilio::SendOnTwilioService,
|
||||
@@ -18,6 +20,7 @@ class SendReplyJob < ApplicationJob
|
||||
def perform(message_id)
|
||||
message = Message.find(message_id)
|
||||
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'
|
||||
|
||||
@@ -29,6 +32,61 @@ 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 unless deliverable_message?(message)
|
||||
return message.email_notifiable_message? if email_channel?(channel_name)
|
||||
return email_notification_deliverable?(message, channel_name) if email_notification_channel?(channel_name)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def deliverable_message?(message)
|
||||
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'
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def email_channel?(channel_name)
|
||||
channel_name == 'Channel::Email'
|
||||
end
|
||||
|
||||
def email_notification_channel?(channel_name)
|
||||
%w[Channel::WebWidget Channel::Api].include?(channel_name)
|
||||
end
|
||||
|
||||
def email_notification_deliverable?(message, channel_name)
|
||||
return false unless message.email_notifiable_message?
|
||||
return false if message.conversation.contact.email.blank?
|
||||
return false unless message.account.within_email_rate_limit?
|
||||
|
||||
email_notification_enabled?(message.inbox, channel_name)
|
||||
end
|
||||
|
||||
def email_notification_enabled?(inbox, channel_name)
|
||||
return inbox.channel.continuity_via_email if channel_name == 'Channel::WebWidget'
|
||||
return inbox.account.feature_enabled?('email_continuity_on_api_channel') if channel_name == 'Channel::Api'
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def mark_message_failed(message)
|
||||
Messages::StatusUpdateService.new(message, 'failed', INBOX_DISABLED_ERROR).perform
|
||||
end
|
||||
|
||||
def send_on_facebook_page(message)
|
||||
if message.conversation.additional_attributes['type'] == 'instagram_direct_message'
|
||||
::Instagram::Messenger::SendOnInstagramService.new(message: message).perform
|
||||
|
||||
@@ -57,9 +57,11 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
||||
|
||||
next if channel.blank?
|
||||
|
||||
if (event_name = event_name(messaging))
|
||||
send(event_name, messaging, channel)
|
||||
end
|
||||
event_name = event_name(messaging)
|
||||
next if event_name.blank?
|
||||
next unless channel.inbox.active? || event_name == :read
|
||||
|
||||
send(event_name, messaging, channel)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -126,7 +128,7 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
||||
end
|
||||
|
||||
def event_name(messaging)
|
||||
@event_name ||= SUPPORTED_EVENTS.find { |key| messaging.key?(key) }
|
||||
SUPPORTED_EVENTS.find { |key| messaging.key?(key) }
|
||||
end
|
||||
|
||||
def message(messaging, channel)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@ class Webhooks::SmsEventsJob < ApplicationJob
|
||||
|
||||
channel = Channel::Sms.find_by(phone_number: params[:to])
|
||||
return unless channel
|
||||
return if incoming_event?(params) && !channel.inbox.active?
|
||||
|
||||
process_event_params(channel, params)
|
||||
end
|
||||
@@ -25,4 +26,8 @@ class Webhooks::SmsEventsJob < ApplicationJob
|
||||
def delivery_event?(params)
|
||||
params[:type] == 'message-delivered' || params[:type] == 'message-failed'
|
||||
end
|
||||
|
||||
def incoming_event?(params)
|
||||
params[:type] == 'message-received'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class Webhooks::TelegramEventsJob < ApplicationJob
|
||||
|
||||
channel = Channel::Telegram.find_by(bot_token: params[:bot_token])
|
||||
|
||||
if channel_is_inactive?(channel)
|
||||
if channel_is_inactive?(channel, params)
|
||||
log_inactive_channel(channel, params)
|
||||
return
|
||||
end
|
||||
@@ -16,13 +16,18 @@ class Webhooks::TelegramEventsJob < ApplicationJob
|
||||
|
||||
private
|
||||
|
||||
def channel_is_inactive?(channel)
|
||||
def channel_is_inactive?(channel, params)
|
||||
return true if channel.blank?
|
||||
return true unless channel.account.active?
|
||||
return true unless channel.inbox.active? || update_message_event?(params)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def update_message_event?(params)
|
||||
params.dig(:telegram, :edited_message).present? || params.dig(:telegram, :edited_business_message).present?
|
||||
end
|
||||
|
||||
def log_inactive_channel(channel, params)
|
||||
message = if channel&.id
|
||||
"Account #{channel.account.id} is not active for channel #{channel.id}"
|
||||
|
||||
@@ -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? || event_name == 'im_mark_read_msg'
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob
|
||||
def perform(params = {})
|
||||
channel = find_channel_from_whatsapp_business_payload(params)
|
||||
|
||||
if channel_is_inactive?(channel)
|
||||
if channel_is_inactive?(channel, params)
|
||||
Rails.logger.warn("Inactive WhatsApp channel: #{channel&.phone_number || "unknown - #{params[:phone_number]}"}")
|
||||
return
|
||||
end
|
||||
@@ -124,15 +124,29 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob
|
||||
].compact_blank.first
|
||||
end
|
||||
|
||||
def channel_is_inactive?(channel)
|
||||
def channel_is_inactive?(channel, params)
|
||||
return true if channel.blank?
|
||||
# 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? || existing_message_update_event?(params)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def status_update_event?(params)
|
||||
value = params.dig(:entry, 0, :changes, 0, :value) || params
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -3,6 +3,7 @@ class CsatSurveyListener < BaseListener
|
||||
conversation = extract_conversation_and_account(event)[0]
|
||||
|
||||
return unless conversation.resolved?
|
||||
return unless conversation.inbox.active?
|
||||
|
||||
CsatSurveyService.new(conversation: conversation).perform
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ class ReplyMailbox < ApplicationMailbox
|
||||
def process
|
||||
# Return early if no conversation was found (e.g., notification emails, suspended accounts)
|
||||
return unless @conversation
|
||||
return unless @conversation.inbox.active?
|
||||
|
||||
# Wrap everything in a transaction to ensure atomicity
|
||||
# This prevents orphan conversations if message/attachment creation fails
|
||||
|
||||
@@ -58,6 +58,7 @@ class Campaign < ApplicationRecord
|
||||
def trigger!
|
||||
return unless one_off?
|
||||
return unless feature_enabled?
|
||||
return unless inbox.active?
|
||||
return unless mark_processing!
|
||||
|
||||
execute_campaign
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class Mailbox::ConversationFinderStrategies::NewConversationStrategy < Mailbox::
|
||||
# The actual persistence happens in ReplyMailbox within a transaction that includes message creation.
|
||||
def find
|
||||
return nil unless @channel # No valid channel found
|
||||
return nil unless @inbox.active?
|
||||
return nil unless incoming_email_from_valid_email? # Skip edge cases
|
||||
|
||||
# Check if conversation already exists by in_reply_to
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -7,6 +7,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
|
||||
|
||||
@@ -13,6 +13,8 @@ class Whatsapp::IncomingMessageBaseService
|
||||
if processed_params.try(:[], :statuses).present?
|
||||
process_statuses
|
||||
elsif messages_data.present?
|
||||
return unless @inbox.active?
|
||||
|
||||
process_messages
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -581,6 +581,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
|
||||
@@ -1048,6 +1048,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_07_13_184351) 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"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseController
|
||||
before_action :set_call, only: %i[show accept reject terminate upload_recording]
|
||||
before_action :set_call_context, only: :initiate
|
||||
before_action :ensure_inbox_active, only: :initiate
|
||||
before_action :ensure_calling_enabled, only: :initiate
|
||||
before_action :ensure_sdp_offer, only: :initiate
|
||||
before_action :ensure_contact_phone, only: :initiate
|
||||
@@ -96,6 +97,10 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
|
||||
render_could_not_create_error(I18n.t('errors.whatsapp.calls.not_enabled'))
|
||||
end
|
||||
|
||||
def ensure_inbox_active
|
||||
render_inbox_disabled_error unless @inbox.active?
|
||||
end
|
||||
|
||||
def ensure_sdp_offer
|
||||
return if params[:sdp_offer].present?
|
||||
|
||||
|
||||
@@ -90,10 +90,10 @@ class Twilio::VoiceController < ApplicationController
|
||||
from_number.start_with?('client:')
|
||||
end
|
||||
|
||||
# A fresh contact-initiated leg on an inbox with inbound calls turned off.
|
||||
# A fresh contact-initiated leg on an inbox that cannot receive calls.
|
||||
# Reject it so no conference, conversation, or Call row is created.
|
||||
def reject_inbound?
|
||||
twilio_direction == 'inbound' && !agent_leg?(twilio_from) && !inbox.channel.inbound_calls_enabled?
|
||||
twilio_direction == 'inbound' && !agent_leg?(twilio_from) && (!inbox.active? || !inbox.channel.inbound_calls_enabled?)
|
||||
end
|
||||
|
||||
def reject_twiml
|
||||
|
||||
@@ -16,6 +16,7 @@ class Voice::OutboundCallBuilder
|
||||
def perform!
|
||||
raise ArgumentError, 'Contact phone number required' if contact.phone_number.blank?
|
||||
raise ArgumentError, 'Agent required' if user.blank?
|
||||
raise CustomExceptions::InboxDisabled unless inbox.active?
|
||||
|
||||
# Claim for the caller if a reused conversation is unassigned at trigger time; wins over auto-assignment.
|
||||
# New conversations set the assignee at creation instead (see create_conversation!).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CustomExceptions::InboxDisabled < 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
|
||||
@@ -31,6 +31,18 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.keys).to include(*response_keys)
|
||||
end
|
||||
|
||||
it 'does not initialize config or create a contact when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
expect do
|
||||
post '/api/v1/widget/config',
|
||||
params: params,
|
||||
as: :json
|
||||
end.not_to change(Contact, :count)
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token and valid X-Auth-Token' do
|
||||
@@ -48,6 +60,17 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
||||
expect(response_data['contact']['pubsub_token']).to eq(contact_inbox.pubsub_token)
|
||||
end
|
||||
|
||||
it 'does not initialize config when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
post '/api/v1/widget/config',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'returns 401 if account is suspended' do
|
||||
account.update!(status: :suspended)
|
||||
|
||||
|
||||
@@ -37,6 +37,18 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
expect(ContactIdentifyAction).to have_received(:new).with(expected_params)
|
||||
expect(identify_action).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'does not update the contact when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
patch '/api/v1/widget/contact',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(ContactIdentifyAction).not_to have_received(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with update contact' do
|
||||
@@ -255,6 +267,18 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'does not set user when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
patch '/api/v1/widget/contact/set_user',
|
||||
params: params.merge(identifier_hash: correct_identifier_hash),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(ContactIdentifyAction).not_to have_received(:new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -263,19 +287,32 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/widget/destroy_custom_attributes', params: { website_token: '' }
|
||||
post '/api/v1/widget/contact/destroy_custom_attributes', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token' do
|
||||
it 'calls destroy custom attributes' do
|
||||
post '/api/v1/widget/destroy_custom_attributes',
|
||||
post '/api/v1/widget/contact/destroy_custom_attributes',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
expect(contact.reload.custom_attributes).to eq({})
|
||||
end
|
||||
|
||||
it 'does not destroy custom attributes when the inbox is disabled' do
|
||||
contact.update!(custom_attributes: { 'test' => 'value' })
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
post '/api/v1/widget/contact/destroy_custom_attributes',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(contact.reload.custom_attributes).to eq('test' => 'value')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,6 +43,18 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||
expect(json_response['id']).to eq(conversation.display_id)
|
||||
expect(json_response['status']).to eq(conversation.status)
|
||||
end
|
||||
|
||||
it 'returns the conversation when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
get '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['id']).to eq(conversation.display_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a conversation but invalid source id' do
|
||||
@@ -62,6 +74,19 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/conversations' do
|
||||
it 'does not create a conversation when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
expect do
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: conversation_params,
|
||||
as: :json
|
||||
end.not_to change(Conversation, :count)
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'creates a conversation with correct details' do
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
@@ -244,11 +269,40 @@ 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
|
||||
|
||||
describe 'POST /api/v1/widget/conversations/transcript' do
|
||||
context 'with a conversation' do
|
||||
it 'does not send transcript email when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
contact.update!(email: 'test@test.com')
|
||||
expect(ConversationReplyMailer).not_to receive(:with)
|
||||
|
||||
post '/api/v1/widget/conversations/transcript',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'sends transcript email' do
|
||||
contact.update(email: 'test@test.com')
|
||||
mailer = double
|
||||
|
||||
@@ -34,6 +34,24 @@ RSpec.describe '/api/v1/widget/direct_uploads', type: :request do
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content_type']).to eq('image/png')
|
||||
end
|
||||
|
||||
it 'does not create direct upload when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
post api_v1_widget_direct_uploads_url,
|
||||
params: {
|
||||
website_token: web_widget.website_token,
|
||||
blob: {
|
||||
filename: 'avatar.png',
|
||||
byte_size: '1234',
|
||||
checksum: 'dsjbsdhbfif3874823mnsdbf',
|
||||
content_type: 'image/png'
|
||||
}
|
||||
},
|
||||
headers: { 'X-Auth-Token' => token }
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,6 +34,20 @@ RSpec.describe '/api/v1/widget/events', type: :request do
|
||||
.with(params[:name], anything, contact_inbox: contact_inbox,
|
||||
event_info: { test_id: 'test', browser_language: nil, widget_language: nil, browser: anything })
|
||||
end
|
||||
|
||||
it 'does not dispatch events when the inbox is disabled' do
|
||||
token
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
expect(Rails.configuration.dispatcher).not_to receive(:dispatch)
|
||||
|
||||
post '/api/v1/widget/events',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,6 +70,17 @@ RSpec.describe '/api/v1/widget/integrations/dyte', type: :request do
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not add a participant when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token, message_id: integration_message.id },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,6 +39,18 @@ RSpec.describe '/api/v1/widget/labels', type: :request do
|
||||
expect(conversation.reload.label_list.count).to eq 1
|
||||
expect(conversation.reload.label_list.first).to eq 'customer-support'
|
||||
end
|
||||
|
||||
it 'does not add labels when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
post '/api/v1/widget/labels',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(conversation.reload.label_list.count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid website token' do
|
||||
@@ -67,6 +79,18 @@ RSpec.describe '/api/v1/widget/labels', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.label_list.count).to eq 0
|
||||
end
|
||||
|
||||
it 'does not remove labels when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
delete "/api/v1/widget/labels/#{params[:label]}",
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(conversation.reload.label_list.count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid website token' do
|
||||
|
||||
@@ -28,6 +28,18 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||
expect(json_response['meta']).not_to be_empty
|
||||
end
|
||||
|
||||
it 'returns messages when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
get api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload'].length).to eq(4)
|
||||
end
|
||||
|
||||
it 'returns empty messages', :skip_before do
|
||||
get api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token },
|
||||
@@ -43,6 +55,20 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||
|
||||
describe 'POST /api/v1/widget/messages' do
|
||||
context 'when post request is made' do
|
||||
it 'does not create message in conversation when the inbox is disabled' do
|
||||
web_widget.inbox.update!(active: false)
|
||||
message_params = { content: 'hello world', timestamp: Time.current }
|
||||
|
||||
expect do
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: message_params },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
end.not_to change(Message, :count)
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'creates message in conversation' do
|
||||
conversation.destroy! # Test all params
|
||||
message_params = { content: 'hello world', timestamp: Time.current }
|
||||
@@ -213,6 +239,21 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/widget/messages' do
|
||||
context 'when the inbox is disabled' do
|
||||
it 'does not update the message' do
|
||||
message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
|
||||
web_widget.inbox.update!(active: false)
|
||||
|
||||
put api_v1_widget_message_url(message.id),
|
||||
params: { website_token: web_widget.website_token, contact: { email: Faker::Internet.email } },
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(message.reload.submitted_email).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when put request targets a message from another visitor in the same inbox' do
|
||||
it 'does not update the foreign message' do
|
||||
other_contact = create(:contact, account: account, email: nil)
|
||||
|
||||
@@ -139,5 +139,18 @@ RSpec.describe 'Public Inbox Contact Conversations API', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.contact_last_seen_at).not_to eq contact_last_seen_at
|
||||
end
|
||||
|
||||
it 'updates the last seen when the inbox is disabled' do
|
||||
current_time = DateTime.now.utc
|
||||
allow(DateTime).to receive(:now).and_return(current_time)
|
||||
api_channel.inbox.update!(active: false)
|
||||
|
||||
expect(Conversations::UpdateMessageStatusJob).to receive(:perform_later).with(conversation.id, current_time)
|
||||
|
||||
post update_last_seen_path
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.contact_last_seen_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -208,6 +208,18 @@ RSpec.describe 'WhatsApp Calls API', type: :request do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq(I18n.t('errors.whatsapp.calls.not_enabled'))
|
||||
end
|
||||
|
||||
it 'returns 403 before initiating the provider call when the inbox is disabled' do
|
||||
inbox.update!(active: false)
|
||||
allow(provider_service).to receive(:initiate_call)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/whatsapp_calls/initiate",
|
||||
params: { conversation_id: initiate_conversation.display_id, sdp_offer: 'sdp_offer' },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(provider_service).not_to have_received(:initiate_call)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/whatsapp_calls/:id/upload_recording' do
|
||||
|
||||
@@ -129,6 +129,23 @@ RSpec.describe 'Twilio::VoiceController', type: :request do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('<Reject')
|
||||
end
|
||||
|
||||
it 'rejects the inbound contact leg without building a call when the inbox is disabled' do
|
||||
inbox.update!(active: false)
|
||||
expect(Voice::InboundCallBuilder).not_to receive(:perform!)
|
||||
|
||||
expect do
|
||||
post "/twilio/voice/call/#{digits}", params: {
|
||||
'CallSid' => call_sid,
|
||||
'From' => from_number,
|
||||
'To' => to_number,
|
||||
'Direction' => 'inbound'
|
||||
}
|
||||
end.not_to change(Call, :count)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('<Reject')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /twilio/voice/status/:phone' do
|
||||
|
||||
@@ -127,5 +127,20 @@ RSpec.describe Voice::OutboundCallBuilder do
|
||||
)
|
||||
end.to raise_error(ArgumentError, 'Agent required')
|
||||
end
|
||||
|
||||
it 'raises before initiating the provider call when the inbox is disabled' do
|
||||
inbox.update!(active: false)
|
||||
|
||||
expect do
|
||||
described_class.perform!(
|
||||
account: account,
|
||||
inbox: inbox,
|
||||
user: user,
|
||||
contact: contact
|
||||
)
|
||||
end.to raise_error(CustomExceptions::InboxDisabled)
|
||||
|
||||
expect(channel).not_to have_received(:initiate_call)
|
||||
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) }
|
||||
|
||||
@@ -30,4 +30,12 @@ RSpec.describe ConversationReplyEmailJob, type: :job do
|
||||
described_class.perform_now(conversation.id, 123)
|
||||
expect(mailer).to have_received(:reply_without_summary)
|
||||
end
|
||||
|
||||
it 'does not send an email when the inbox is disabled before the job runs' do
|
||||
conversation.inbox.update!(active: false)
|
||||
|
||||
described_class.perform_now(conversation.id, 123)
|
||||
|
||||
expect(ConversationReplyMailer).not_to have_received(:with)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -122,5 +122,62 @@ RSpec.describe SendReplyJob do
|
||||
message = create(:message, conversation: create(:conversation, inbox: tiktok_channel.inbox))
|
||||
expect_mapped_service_to_perform(message, 'Tiktok::SendOnTiktokService')
|
||||
end
|
||||
|
||||
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, message_type: :outgoing, conversation: create(:conversation, inbox: twilio_channel.inbox))
|
||||
twilio_channel.inbox.update!(active: false)
|
||||
|
||||
expect(Twilio::SendOnTwilioService).not_to receive(:new)
|
||||
|
||||
described_class.perform_now(message.id)
|
||||
|
||||
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
|
||||
|
||||
it 'does not mark web widget replies as failed when email continuity is disabled' do
|
||||
webwidget_channel = create(:channel_widget, continuity_via_email: false)
|
||||
message = create(:message, message_type: :outgoing, conversation: create(:conversation, inbox: webwidget_channel.inbox))
|
||||
webwidget_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
|
||||
|
||||
@@ -202,6 +202,24 @@ describe Webhooks::InstagramEventsJob do
|
||||
instagram_webhook.perform_now(messaging_seen_event[:entry])
|
||||
end
|
||||
|
||||
it 'handles messaging_seen callback when the inbox is disabled' do
|
||||
messaging_seen_event = build(:messaging_seen_event).with_indifferent_access
|
||||
instagram_messenger_inbox.update!(active: false)
|
||||
|
||||
expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0],
|
||||
channel: instagram_messenger_inbox.channel).and_call_original
|
||||
instagram_webhook.perform_now(messaging_seen_event[:entry])
|
||||
end
|
||||
|
||||
it 'does not create message callbacks when the inbox is disabled' do
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
instagram_messenger_inbox.update!(active: false)
|
||||
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 0
|
||||
end
|
||||
|
||||
it 'handles unsupported message' do
|
||||
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
|
||||
sender_id = unsupported_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
@@ -53,6 +53,14 @@ RSpec.describe Webhooks::SmsEventsJob do
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
it 'does not call Sms::IncomingMessageService for received messages when the inbox is disabled' do
|
||||
sms_channel.inbox.update!(active: false)
|
||||
|
||||
expect(Sms::IncomingMessageService).not_to receive(:new)
|
||||
|
||||
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
|
||||
@@ -64,6 +72,20 @@ RSpec.describe Webhooks::SmsEventsJob do
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
it 'calls Sms::DeliveryStatusService for delivered messages when the inbox is disabled' do
|
||||
sms_channel.inbox.update!(active: false)
|
||||
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
|
||||
|
||||
@@ -46,6 +46,14 @@ RSpec.describe Webhooks::TelegramEventsJob do
|
||||
expect(Telegram::IncomingMessageService).not_to receive(:new)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
|
||||
it 'does not process incoming message events when the inbox is disabled' do
|
||||
telegram_channel.inbox.update!(active: false)
|
||||
|
||||
expect(Telegram::IncomingMessageService).not_to receive(:new)
|
||||
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update message params' do
|
||||
@@ -60,5 +68,17 @@ RSpec.describe Webhooks::TelegramEventsJob do
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
|
||||
it 'calls Telegram::UpdateMessageService when the inbox is disabled' do
|
||||
telegram_channel.inbox.update!(active: false)
|
||||
|
||||
process_service = double
|
||||
allow(Telegram::UpdateMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Telegram::UpdateMessageService).to receive(:new).with(inbox: telegram_channel.inbox,
|
||||
params: params['telegram'].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,6 +42,38 @@ RSpec.describe Webhooks::TiktokEventsJob do
|
||||
expect(read_status_service).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'processes im_mark_read_msg events when the inbox is disabled' do
|
||||
channel.inbox.update!(active: false)
|
||||
read_status_service = instance_double(Tiktok::ReadStatusService, perform: true)
|
||||
allow(Tiktok::ReadStatusService).to receive(:new).and_return(read_status_service)
|
||||
|
||||
event = {
|
||||
event: 'im_mark_read_msg',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1', read: { last_read_timestamp: 1_700_000_000_000 }, from_user: { id: 'user-1' } }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::ReadStatusService).to have_received(:new).with(channel: channel, content: hash_including(conversation_id: 'tt-conv-1'))
|
||||
expect(read_status_service).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'does not process im_receive_msg events when the inbox is disabled' do
|
||||
channel.inbox.update!(active: false)
|
||||
allow(Tiktok::MessageService).to receive(:new)
|
||||
|
||||
event = {
|
||||
event: 'im_receive_msg',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'ignores unsupported event types' do
|
||||
allow(Tiktok::MessageService).to receive(:new)
|
||||
|
||||
|
||||
@@ -106,6 +106,55 @@ RSpec.describe Webhooks::WhatsappEventsJob do
|
||||
job.perform_now(phone_number: unknown_phone)
|
||||
end
|
||||
|
||||
it 'processes status callbacks when the inbox is disabled' do
|
||||
wb_params = params.deep_dup
|
||||
wb_params[:entry].first[:changes].first[:value][:statuses] = [
|
||||
{ id: 'wamid-test', recipient_id: '919745786257', status: 'delivered' }
|
||||
]
|
||||
channel.inbox.update!(active: false)
|
||||
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).with(inbox: channel.inbox, params: wb_params)
|
||||
|
||||
job.perform_now(wb_params)
|
||||
end
|
||||
|
||||
it 'does not process message callbacks when the inbox is disabled' do
|
||||
wb_params = params.deep_dup
|
||||
wb_params[:entry].first[:changes].first[:value][:messages] = [
|
||||
{ from: '919745786257', id: 'wamid-test', text: { body: 'Hello' }, type: 'text' }
|
||||
]
|
||||
channel.inbox.update!(active: false)
|
||||
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new)
|
||||
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
|
||||
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
|
||||
|
||||
@@ -52,6 +52,14 @@ describe CsatSurveyListener do
|
||||
|
||||
listener.conversation_status_changed(event)
|
||||
end
|
||||
|
||||
it 'does not trigger CSAT survey service when the inbox is disabled' do
|
||||
resolved_conversation.inbox.update!(active: false)
|
||||
event = Events::Base.new(event_name, Time.zone.now, conversation: resolved_conversation)
|
||||
|
||||
expect(CsatSurveyService).not_to receive(:new)
|
||||
listener.conversation_status_changed(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation is not resolved' do
|
||||
|
||||
@@ -42,6 +42,17 @@ RSpec.describe ReplyMailbox do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with reply uuid present for a disabled inbox' do
|
||||
before do
|
||||
conversation.update!(uuid: '6bdc3f4d-0bec-4515-a284-5d916fdde489')
|
||||
conversation.inbox.update!(active: false)
|
||||
end
|
||||
|
||||
it 'does not add the mail content as a new message' do
|
||||
expect { described_subject }.not_to change(Message, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with in reply to email' do
|
||||
let(:reply_mail_without_uuid) { create_inbound_email_from_fixture('reply_mail_without_uuid.eml') }
|
||||
let(:described_subject) { described_class.receive reply_mail_without_uuid }
|
||||
@@ -67,6 +78,27 @@ RSpec.describe ReplyMailbox do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when forwarded email inbox is disabled' do
|
||||
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||
let(:forwarded_mail) { create_inbound_email_from_mail(from: 'sender@example.com', to: email_channel.email, subject: 'Hello') }
|
||||
|
||||
before do
|
||||
email_channel.inbox.update!(active: false)
|
||||
end
|
||||
|
||||
it 'does not create contacts, conversations, or messages' do
|
||||
contact_count = Contact.count
|
||||
conversation_count = Conversation.count
|
||||
message_count = Message.count
|
||||
|
||||
described_class.receive forwarded_mail
|
||||
|
||||
expect(Contact.count).to eq(contact_count)
|
||||
expect(Conversation.count).to eq(conversation_count)
|
||||
expect(Message.count).to eq(message_count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when new conversation email contains null bytes' do
|
||||
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||
let(:null_byte_mail) { create_inbound_email_from_mail(from: 'sender@example.com', to: email_channel.email, subject: 'Hello') }
|
||||
|
||||
@@ -142,6 +142,17 @@ RSpec.describe Campaign do
|
||||
campaign.trigger!
|
||||
end
|
||||
|
||||
it 'does not trigger the campaign when the inbox is disabled' do
|
||||
campaign.save!
|
||||
twilio_inbox.update!(active: false)
|
||||
|
||||
expect(Twilio::OneoffSmsCampaignService).not_to receive(:new)
|
||||
|
||||
campaign.trigger!
|
||||
|
||||
expect(campaign.reload.active?).to be true
|
||||
end
|
||||
|
||||
it 'keeps the campaign processing when triggering fails' do
|
||||
campaign.save!
|
||||
sms_service = double
|
||||
|
||||
@@ -252,6 +252,18 @@ describe Whatsapp::IncomingMessageService do
|
||||
expect(message.reload.status).to eq('read')
|
||||
end
|
||||
|
||||
it 'updates message status when the inbox is disabled' do
|
||||
status_params = {
|
||||
'statuses' => [{ 'recipient_id' => from, 'id' => from, 'status' => 'read' }]
|
||||
}.with_indifferent_access
|
||||
message = Message.find_by!(source_id: from)
|
||||
whatsapp_channel.inbox.update!(active: false)
|
||||
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: status_params).perform
|
||||
|
||||
expect(message.reload.status).to eq('read')
|
||||
end
|
||||
|
||||
it 'stores BSUID source ids from status contacts' do
|
||||
bsuid = 'IN.2081978709342942'
|
||||
parent_bsuid = 'IN.ENT.9081726354'
|
||||
|
||||
Reference in New Issue
Block a user