Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e926cc4c6b | ||
|
|
b1acd38c10 | ||
|
|
7b732e6a16 | ||
|
|
c349785bb9 |
@@ -2,14 +2,6 @@
|
||||
# It initializes with necessary attributes and provides a perform method
|
||||
# to create a user and account user in a transaction.
|
||||
class AgentBuilder
|
||||
LIMIT_EXCEEDED_MESSAGE = 'Account limit exceeded. Please purchase more licenses'.freeze
|
||||
|
||||
class LimitExceededError < StandardError
|
||||
def initialize
|
||||
super(AgentBuilder::LIMIT_EXCEEDED_MESSAGE)
|
||||
end
|
||||
end
|
||||
|
||||
# Initializes an AgentBuilder with necessary attributes.
|
||||
# @param email [String] the email of the user.
|
||||
# @param name [String] the name of the user.
|
||||
@@ -22,23 +14,15 @@ class AgentBuilder
|
||||
# Creates a user and account user in a transaction.
|
||||
# @return [User] the created user.
|
||||
def perform
|
||||
account.with_lock do
|
||||
raise LimitExceededError unless can_add_agent?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@user = find_or_create_user
|
||||
create_account_user
|
||||
end
|
||||
ActiveRecord::Base.transaction do
|
||||
@user = find_or_create_user
|
||||
create_account_user
|
||||
end
|
||||
@user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_add_agent?
|
||||
account.usage_limits[:agents] > account.account_users.count
|
||||
end
|
||||
|
||||
# Finds a user by email or creates a new one with a temporary password.
|
||||
# @return [User] the found or created user.
|
||||
def find_or_create_user
|
||||
|
||||
@@ -3,8 +3,6 @@ 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,8 +2,6 @@ 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,7 +20,6 @@ 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,7 +10,6 @@ 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,15 +22,14 @@ 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.tap(&:save!)
|
||||
@message.save!
|
||||
@message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_agent, except: [:create, :index, :bulk_create]
|
||||
before_action :check_authorization
|
||||
before_action :validate_limit, only: [:create]
|
||||
before_action :validate_limit_for_bulk_create, only: [:bulk_create]
|
||||
|
||||
def index
|
||||
@agents = agents
|
||||
@@ -18,8 +20,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
)
|
||||
|
||||
@agent = builder.perform
|
||||
rescue AgentBuilder::LimitExceededError => e
|
||||
render_payment_required(e.message)
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -36,13 +36,25 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
def bulk_create
|
||||
emails = params[:emails]
|
||||
|
||||
bulk_create_agents(emails)
|
||||
emails.each do |email|
|
||||
builder = AgentBuilder.new(
|
||||
email: email,
|
||||
name: email.split('@').first,
|
||||
inviter: current_user,
|
||||
account: Current.account
|
||||
)
|
||||
begin
|
||||
builder.perform
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
||||
end
|
||||
end
|
||||
|
||||
# This endpoint is used to bulk create agents during onboarding
|
||||
# onboarding_step key in present in Current account custom attributes, since this is a one time operation
|
||||
clear_onboarding_step
|
||||
Current.account.custom_attributes.delete('onboarding_step')
|
||||
Current.account.save!
|
||||
head :ok
|
||||
rescue AgentBuilder::LimitExceededError => e
|
||||
render_payment_required(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -75,33 +87,22 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
||||
end
|
||||
|
||||
def bulk_create_agents(emails)
|
||||
Current.account.with_lock do
|
||||
raise AgentBuilder::LimitExceededError if emails.count > available_agent_count
|
||||
def validate_limit_for_bulk_create
|
||||
limit_available = params[:emails].count <= available_agent_count
|
||||
|
||||
emails.each { |email| create_agent_from_email(email) }
|
||||
end
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available
|
||||
end
|
||||
|
||||
def create_agent_from_email(email)
|
||||
builder = AgentBuilder.new(
|
||||
email: email,
|
||||
name: email.split('@').first,
|
||||
inviter: current_user,
|
||||
account: Current.account
|
||||
)
|
||||
builder.perform
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
||||
end
|
||||
|
||||
def clear_onboarding_step
|
||||
Current.account.custom_attributes.delete('onboarding_step')
|
||||
Current.account.save!
|
||||
def validate_limit
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') unless can_add_agent?
|
||||
end
|
||||
|
||||
def available_agent_count
|
||||
Current.account.usage_limits[:agents] - Current.account.account_users.count
|
||||
Current.account.usage_limits[:agents] - agents.count
|
||||
end
|
||||
|
||||
def can_add_agent?
|
||||
available_agent_count.positive?
|
||||
end
|
||||
|
||||
def delete_user_record(agent)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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
|
||||
@@ -10,8 +9,6 @@ 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
|
||||
@@ -35,8 +32,6 @@ 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
|
||||
@@ -85,8 +80,4 @@ 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,7 +198,6 @@ 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
|
||||
@@ -215,7 +214,6 @@ 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
|
||||
@@ -223,7 +221,12 @@ 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, :active, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||
[:name, :avatar, :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,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
class Api::V1::Accounts::Integrations::BaseController < Api::V1::Accounts::BaseController
|
||||
private
|
||||
|
||||
# Managing an integration hook (create/update/destroy) is admin-only, enforced via HookPolicy.
|
||||
# Subclasses opt in per action with `before_action :check_authorization, only: [...]`.
|
||||
def check_authorization
|
||||
authorize(:hook)
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Integrations::BaseController
|
||||
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_hook, except: [:create]
|
||||
before_action :check_authorization
|
||||
|
||||
@@ -35,6 +35,10 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Inte
|
||||
@hook = Current.account.hooks.find(params[:id])
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(:hook)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.require(:hook).permit(:app_id, :inbox_id, :status, settings: {})
|
||||
end
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Integrations::BaseController
|
||||
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_conversation, only: [:create_issue, :link_issue, :unlink_issue, :linked_issues]
|
||||
before_action :fetch_hook, only: [:destroy]
|
||||
before_action :check_authorization, only: [:destroy]
|
||||
|
||||
def destroy
|
||||
revoke_linear_token
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::Integrations::BaseController
|
||||
class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_hook, only: [:destroy]
|
||||
before_action :check_authorization, only: [:destroy]
|
||||
|
||||
def destroy
|
||||
@hook.destroy!
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::Integrations::BaseController
|
||||
class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::BaseController
|
||||
include Shopify::IntegrationHelper
|
||||
before_action :setup_shopify_context, only: [:orders]
|
||||
before_action :fetch_hook, except: [:auth]
|
||||
before_action :check_authorization, only: [:destroy]
|
||||
before_action :validate_contact, only: [:orders]
|
||||
|
||||
def auth
|
||||
|
||||
@@ -90,8 +90,4 @@ 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,5 +1,4 @@
|
||||
class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
||||
before_action :ensure_inbox_active
|
||||
before_action :set_global_config
|
||||
|
||||
def create
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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,12 +1,6 @@
|
||||
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,7 +1,6 @@
|
||||
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
|
||||
@@ -9,15 +8,4 @@ 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,8 +1,6 @@
|
||||
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,5 +1,4 @@
|
||||
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,6 +1,4 @@
|
||||
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,5 +1,4 @@
|
||||
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,7 +9,6 @@ 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
|
||||
|
||||
@@ -46,13 +45,6 @@ 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
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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,6 +1,5 @@
|
||||
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
|
||||
@@ -51,8 +50,4 @@ 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,7 +1,6 @@
|
||||
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
|
||||
@@ -65,8 +64,4 @@ 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,6 +1,5 @@
|
||||
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
|
||||
@@ -71,8 +70,4 @@ 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
|
||||
|
||||
@@ -47,10 +47,18 @@ class Webhooks::WhatsappController < ActionController::API
|
||||
metadata = params.dig(:entry, 0, :changes, 0, :value, :metadata)
|
||||
return if metadata.blank?
|
||||
|
||||
Whatsapp::WebhookChannelFinderService.new(
|
||||
display_phone_number: metadata[:display_phone_number],
|
||||
phone_number_id: metadata[:phone_number_id]
|
||||
).perform
|
||||
phone_number = normalized_phone_number(metadata[:display_phone_number])
|
||||
phone_number_id = metadata[:phone_number_id]
|
||||
channel = Channel::Whatsapp.find_by(phone_number: phone_number)
|
||||
|
||||
return channel if channel && channel.provider_config['phone_number_id'] == phone_number_id
|
||||
end
|
||||
|
||||
def normalized_phone_number(phone_number)
|
||||
return if phone_number.blank?
|
||||
|
||||
phone_number = phone_number.to_s
|
||||
phone_number.start_with?('+') ? phone_number : "+#{phone_number}"
|
||||
end
|
||||
|
||||
def inactive_whatsapp_number?
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -63,10 +62,6 @@ 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,5 +1,4 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||
|
||||
defineProps({
|
||||
@@ -8,21 +7,13 @@ defineProps({
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :title="inbox.name" class="flex items-center gap-1 min-w-0">
|
||||
<div :title="inbox.name" class="flex items-center gap-0.5 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,5 +1,4 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||
|
||||
defineProps({
|
||||
@@ -8,21 +7,13 @@ defineProps({
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :title="inbox.name" class="flex items-center gap-1 min-w-0">
|
||||
<div :title="inbox.name" class="flex items-center gap-0.5 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>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import * as agentHelper from 'dashboard/helper/agentHelper';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useAgentsList } from '../useAgentsList';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { allAgentsData, formattedAgentsData } from './fixtures/agentFixtures';
|
||||
import * as agentHelper from 'dashboard/helper/agentHelper';
|
||||
|
||||
// Mock vue-i18n
|
||||
vi.mock('vue-i18n', () => ({
|
||||
@@ -94,32 +94,6 @@ describe('useAgentsList', () => {
|
||||
expect(agentsList.value.length).toBe(formattedAgentsData.slice(1).length);
|
||||
});
|
||||
|
||||
it('keeps nameless agent bots and applies a fallback label', () => {
|
||||
const namelessBot = {
|
||||
id: 91,
|
||||
name: null,
|
||||
assignee_type: 'AgentBot',
|
||||
availability_status: 'offline',
|
||||
};
|
||||
mockUseMapGetter({
|
||||
'inboxAssignableAgents/getAssignableAgents': ref(() => [
|
||||
...allAgentsData,
|
||||
namelessBot,
|
||||
]),
|
||||
});
|
||||
|
||||
const { agentsList } = useAgentsList();
|
||||
// access the computed to trigger evaluation
|
||||
expect(agentsList.value).toBeDefined();
|
||||
|
||||
const passedAgents =
|
||||
agentHelper.getAgentsByUpdatedPresence.mock.calls[0][0];
|
||||
expect(passedAgents).toContainEqual({
|
||||
...namelessBot,
|
||||
name: '-',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty assignable agents', () => {
|
||||
mockUseMapGetter({
|
||||
'inboxAssignableAgents/getAssignableAgents': ref(() => []),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
getAgentsByUpdatedPresence,
|
||||
getSortedAgentsByAvailability,
|
||||
} from 'dashboard/helper/agentHelper';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
/**
|
||||
* A composable function that provides a list of agents for assignment.
|
||||
@@ -53,11 +53,7 @@ export function useAgentsList(
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const agentsList = computed(() => {
|
||||
const agents = (assignableAgents.value || []).map(agent =>
|
||||
!agent.name && agent.assignee_type === 'AgentBot'
|
||||
? { ...agent, name: '-' }
|
||||
: agent
|
||||
);
|
||||
const agents = assignableAgents.value || [];
|
||||
const agentsByUpdatedPresence = getAgentsByUpdatedPresence(
|
||||
agents,
|
||||
currentUser.value,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export const getAgentsByAvailability = (agents, availability) => {
|
||||
return agents
|
||||
.filter(agent => agent.availability_status === availability)
|
||||
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,18 +26,6 @@ describe('agentHelper', () => {
|
||||
offlineAgentsData
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw when an agent has a null name', () => {
|
||||
const agents = [
|
||||
{ id: 1, name: null, availability_status: 'offline' },
|
||||
{ id: 2, name: 'Zoe', availability_status: 'offline' },
|
||||
];
|
||||
|
||||
expect(() => getAgentsByAvailability(agents, 'offline')).not.toThrow();
|
||||
expect(
|
||||
getAgentsByAvailability(agents, 'offline').map(agent => agent.id)
|
||||
).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortedAgentsByAvailability', () => {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"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.",
|
||||
|
||||
@@ -135,7 +135,7 @@ onMounted(() => {
|
||||
<BaseTableCell class="max-w-0">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<Avatar
|
||||
:name="bot.name || ''"
|
||||
:name="bot.name"
|
||||
:src="bot.thumbnail"
|
||||
:size="40"
|
||||
class="flex-shrink-0"
|
||||
|
||||
@@ -142,20 +142,10 @@ const openDelete = inbox => {
|
||||
>
|
||||
<ChannelIcon class="size-6 text-n-slate-10" :inbox="inbox" />
|
||||
</div>
|
||||
<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>
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="block text-heading-3 text-n-slate-12 capitalize">
|
||||
{{ inbox.name }}
|
||||
</span>
|
||||
<ChannelName
|
||||
:channel-type="inbox.channel_type"
|
||||
:medium="inbox.medium"
|
||||
|
||||
@@ -68,10 +68,6 @@ const isAgentBot = computed(
|
||||
() => props.selectedItem?.assignee_type === 'AgentBot'
|
||||
);
|
||||
|
||||
const selectedItemName = computed(() =>
|
||||
!props.selectedItem?.name && isAgentBot.value ? '-' : props.selectedItem?.name
|
||||
);
|
||||
|
||||
const selectedThumbnail = computed(
|
||||
() => props.selectedItem?.thumbnail || props.selectedItem?.avatar_url
|
||||
);
|
||||
@@ -99,16 +95,16 @@ const selectedThumbnail = computed(
|
||||
<h4
|
||||
v-else
|
||||
class="items-center overflow-hidden text-sm leading-tight whitespace-nowrap text-ellipsis text-n-slate-12"
|
||||
:title="selectedItemName"
|
||||
:title="selectedItem.name"
|
||||
>
|
||||
{{ selectedItemName }}
|
||||
{{ selectedItem.name }}
|
||||
</h4>
|
||||
</div>
|
||||
<Avatar
|
||||
v-if="hasValue && hasThumbnail && (isAgentBot || !hasIcon)"
|
||||
:src="selectedThumbnail"
|
||||
:status="selectedItem.availability_status"
|
||||
:name="selectedItemName"
|
||||
:name="selectedItem.name"
|
||||
:icon-name="isAgentBot ? 'i-lucide-bot' : undefined"
|
||||
:size="24"
|
||||
hide-offline-status
|
||||
|
||||
@@ -53,9 +53,7 @@ export default {
|
||||
computed: {
|
||||
filteredOptions() {
|
||||
return this.options.filter(option => {
|
||||
return (option.name || '')
|
||||
.toLowerCase()
|
||||
.includes(this.search.toLowerCase());
|
||||
return option.name.toLowerCase().includes(this.search.toLowerCase());
|
||||
});
|
||||
},
|
||||
noResult() {
|
||||
|
||||
@@ -11,8 +11,6 @@ import { IFrameHelper } from '../helpers/utils';
|
||||
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const TRANSCRIPT_COOLDOWN_MS = 15000;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatInputWrap,
|
||||
@@ -26,9 +24,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
inReplyTo: null,
|
||||
isSendingTranscript: false,
|
||||
transcriptCooldown: false,
|
||||
transcriptCooldownTimer: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -62,9 +57,6 @@ export default {
|
||||
mounted() {
|
||||
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.transcriptCooldownTimer);
|
||||
},
|
||||
methods: {
|
||||
...mapActions('conversation', ['sendMessage', 'sendAttachment']),
|
||||
...mapActions('conversationAttributes', ['getAttributes']),
|
||||
@@ -98,35 +90,19 @@ export default {
|
||||
toggleReplyTo(message) {
|
||||
this.inReplyTo = message;
|
||||
},
|
||||
startTranscriptCooldown() {
|
||||
this.transcriptCooldown = true;
|
||||
clearTimeout(this.transcriptCooldownTimer);
|
||||
this.transcriptCooldownTimer = setTimeout(() => {
|
||||
this.transcriptCooldown = false;
|
||||
}, TRANSCRIPT_COOLDOWN_MS);
|
||||
},
|
||||
async sendTranscript() {
|
||||
if (
|
||||
!this.hasEmail ||
|
||||
this.isSendingTranscript ||
|
||||
this.transcriptCooldown
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.isSendingTranscript = true;
|
||||
try {
|
||||
await sendEmailTranscript();
|
||||
this.startTranscriptCooldown();
|
||||
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
|
||||
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
|
||||
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'),
|
||||
});
|
||||
} finally {
|
||||
this.isSendingTranscript = false;
|
||||
if (this.hasEmail) {
|
||||
try {
|
||||
await sendEmailTranscript();
|
||||
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
|
||||
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
emitter.$emit(BUS_EVENTS.SHOW_ALERT, {
|
||||
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -168,7 +144,6 @@ export default {
|
||||
v-if="showEmailTranscriptButton"
|
||||
type="clear"
|
||||
class="font-normal"
|
||||
:disabled="isSendingTranscript || transcriptCooldown"
|
||||
@click="sendTranscript"
|
||||
>
|
||||
{{ $t('EMAIL_TRANSCRIPT.BUTTON_TEXT') }}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
class Channels::Whatsapp::WebhookSetupJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
# Meta's Graph API calls (phone registration + webhook subscription) are slow and fail
|
||||
# transiently. Retry a few times, and once retries are exhausted mark the channel for
|
||||
# reauthorization so the inbox has a visible recovery path instead of silently missing its
|
||||
# webhook subscription. Running these off the request thread also keeps them clear of the
|
||||
# 15s Rack::Timeout, which previously aborted inbox creation and rolled it back.
|
||||
retry_on StandardError, wait: :polynomially_longer, attempts: 3 do |job, error|
|
||||
channel = job.arguments.first
|
||||
Rails.logger.error("[WHATSAPP] Webhook setup failed after retries: #{error.message}")
|
||||
channel.prompt_reauthorization! if channel.is_a?(Channel::Whatsapp)
|
||||
end
|
||||
|
||||
# A deleted channel can't be set up; discard instead of retrying (takes precedence over
|
||||
# the StandardError retry above, which would otherwise catch DeserializationError too).
|
||||
discard_on ActiveJob::DeserializationError
|
||||
|
||||
def perform(whatsapp_channel, run_health_check: false)
|
||||
whatsapp_channel.setup_webhooks
|
||||
# Health check runs only after registration so a freshly provisioned number
|
||||
# isn't flagged as pending before setup_webhooks has a chance to register it.
|
||||
whatsapp_channel.check_provisioning_health if run_health_check
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,6 @@ 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,7 +13,6 @@ 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.inbox.active? && channel.imap_enabled? && !channel.reauthorization_required?
|
||||
channel.imap_enabled? && !channel.reauthorization_required?
|
||||
end
|
||||
|
||||
def process_email_for_channel(channel, interval)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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,
|
||||
@@ -20,7 +18,6 @@ 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'
|
||||
|
||||
@@ -32,61 +29,6 @@ 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,11 +57,9 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
||||
|
||||
next if channel.blank?
|
||||
|
||||
event_name = event_name(messaging)
|
||||
next if event_name.blank?
|
||||
next unless channel.inbox.active? || event_name == :read
|
||||
|
||||
send(event_name, messaging, channel)
|
||||
if (event_name = event_name(messaging))
|
||||
send(event_name, messaging, channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -128,7 +126,7 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
||||
end
|
||||
|
||||
def event_name(messaging)
|
||||
SUPPORTED_EVENTS.find { |key| messaging.key?(key) }
|
||||
@event_name ||= SUPPORTED_EVENTS.find { |key| messaging.key?(key) }
|
||||
end
|
||||
|
||||
def message(messaging, channel)
|
||||
|
||||
@@ -4,7 +4,6 @@ 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,7 +8,6 @@ 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
|
||||
@@ -26,8 +25,4 @@ 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, params)
|
||||
if channel_is_inactive?(channel)
|
||||
log_inactive_channel(channel, params)
|
||||
return
|
||||
end
|
||||
@@ -16,18 +16,13 @@ class Webhooks::TelegramEventsJob < ApplicationJob
|
||||
|
||||
private
|
||||
|
||||
def channel_is_inactive?(channel, params)
|
||||
def channel_is_inactive?(channel)
|
||||
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,7 +21,6 @@ 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, params)
|
||||
if channel_is_inactive?(channel)
|
||||
Rails.logger.warn("Inactive WhatsApp channel: #{channel&.phone_number || "unknown - #{params[:phone_number]}"}")
|
||||
return
|
||||
end
|
||||
@@ -124,29 +124,15 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob
|
||||
].compact_blank.first
|
||||
end
|
||||
|
||||
def channel_is_inactive?(channel, params)
|
||||
def channel_is_inactive?(channel)
|
||||
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
|
||||
@@ -167,11 +153,11 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob
|
||||
end
|
||||
|
||||
def get_channel_from_wb_payload(wb_params)
|
||||
metadata = wb_params[:entry].first[:changes].first.dig(:value, :metadata) || {}
|
||||
Whatsapp::WebhookChannelFinderService.new(
|
||||
display_phone_number: metadata[:display_phone_number],
|
||||
phone_number_id: metadata[:phone_number_id]
|
||||
).perform
|
||||
phone_number = "+#{wb_params[:entry].first[:changes].first.dig(:value, :metadata, :display_phone_number)}"
|
||||
phone_number_id = wb_params[:entry].first[:changes].first.dig(:value, :metadata, :phone_number_id)
|
||||
channel = Channel::Whatsapp.find_by(phone_number: phone_number)
|
||||
# validate to ensure the phone number id matches the whatsapp channel
|
||||
return channel if channel && channel.provider_config['phone_number_id'] == phone_number_id
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -63,8 +63,6 @@ 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,7 +18,6 @@ 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)
|
||||
@@ -40,11 +39,10 @@ class AutomationRuleListener < BaseListener
|
||||
def process_conversation_event(event, event_name)
|
||||
return if performed_by_automation?(event)
|
||||
|
||||
return if auto_reply_event?(event, event_name)
|
||||
auto_reply_skip_events = %w[conversation_created conversation_opened]
|
||||
return if auto_reply_skip_events.include?(event_name) && ignore_auto_reply_event?(event)
|
||||
|
||||
conversation = event.data[:conversation]
|
||||
return unless conversation.inbox.active?
|
||||
|
||||
account = conversation.account
|
||||
changed_attributes = event.data[:changed_attributes]
|
||||
|
||||
@@ -76,10 +74,6 @@ 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,7 +3,6 @@ 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,7 +6,6 @@ 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,7 +58,6 @@ class Campaign < ApplicationRecord
|
||||
def trigger!
|
||||
return unless one_off?
|
||||
return unless feature_enabled?
|
||||
return unless inbox.active?
|
||||
return unless mark_processing!
|
||||
|
||||
execute_campaign
|
||||
|
||||
@@ -35,7 +35,7 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
after_create :sync_templates
|
||||
after_update_commit :log_credentials_transfer, if: :saved_change_to_provider_config?
|
||||
before_destroy :teardown_webhooks
|
||||
after_commit :setup_webhooks, on: :create, if: :should_auto_setup_webhooks?
|
||||
after_commit :enqueue_webhook_setup, on: :create, if: :should_auto_setup_webhooks?
|
||||
|
||||
def name
|
||||
'Whatsapp'
|
||||
@@ -120,15 +120,47 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
delegate :media_url, to: :provider_service
|
||||
delegate :api_headers, to: :provider_service
|
||||
|
||||
# Runs inside Channels::Whatsapp::WebhookSetupJob, off the request thread so the slow Meta
|
||||
# Graph calls can't trip Rack::Timeout. Raises on failure so the job can retry the flaky
|
||||
# calls and, once retries are exhausted, mark the channel for reauthorization.
|
||||
def setup_webhooks
|
||||
perform_webhook_setup
|
||||
end
|
||||
|
||||
# Enqueue on the same channel record so GlobalID resolves it in the job. If the queue
|
||||
# is unavailable, fall back to prompting reauthorization so the inbox has a visible
|
||||
# recovery path instead of silently committing without its webhook registered.
|
||||
def enqueue_webhook_setup(run_health_check: false)
|
||||
Channels::Whatsapp::WebhookSetupJob.perform_later(self, run_health_check: run_health_check)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "[WHATSAPP] Webhook setup failed: #{e.message}"
|
||||
Rails.logger.error "[WHATSAPP] Failed to enqueue webhook setup: #{e.message}"
|
||||
prompt_reauthorization!
|
||||
end
|
||||
|
||||
# Runs after webhook registration (inside WebhookSetupJob) so it observes the
|
||||
# post-registration provisioning state; prompts reauthorization if Meta still reports
|
||||
# the number as not provisioned. Only used for new embedded-signup channels — running
|
||||
# it before registration would spuriously flag freshly created numbers as pending.
|
||||
def check_provisioning_health
|
||||
health_data = Whatsapp::HealthService.new(self).fetch_health_status
|
||||
return unless health_data
|
||||
|
||||
if provisioning_pending?(health_data)
|
||||
prompt_reauthorization!
|
||||
else
|
||||
Rails.logger.info "[WHATSAPP] Channel #{phone_number} health check passed"
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "[WHATSAPP] Health check failed for channel #{phone_number}: #{e.message}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def provisioning_pending?(health_data)
|
||||
health_data[:platform_type] == 'NOT_APPLICABLE' ||
|
||||
health_data.dig(:throughput, 'level') == 'NOT_APPLICABLE'
|
||||
end
|
||||
|
||||
def ensure_webhook_verify_token
|
||||
provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider == 'whatsapp_cloud'
|
||||
end
|
||||
|
||||
@@ -7,8 +7,6 @@ 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,7 +22,6 @@ 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,8 +4,6 @@ class Sms::IncomingMessageService
|
||||
pattr_initialize [:inbox!, :params!]
|
||||
|
||||
def perform
|
||||
return unless @inbox.active?
|
||||
|
||||
set_contact
|
||||
set_conversation
|
||||
@message = @conversation.messages.create!(
|
||||
|
||||
@@ -7,8 +7,6 @@ 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,8 +4,6 @@ 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,7 +7,6 @@ class Twilio::IncomingMessageService
|
||||
|
||||
def perform
|
||||
return if twilio_channel.blank?
|
||||
return unless inbox.active?
|
||||
|
||||
set_contact
|
||||
set_conversation
|
||||
|
||||
@@ -5,8 +5,6 @@ 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,7 +4,6 @@ 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
|
||||
|
||||
@@ -15,15 +15,14 @@ class Whatsapp::EmbeddedSignupService
|
||||
phone_info = fetch_phone_info(access_token)
|
||||
|
||||
channel = create_or_reauthorize_channel(access_token, phone_info)
|
||||
# NOTE: We call setup_webhooks explicitly here instead of relying on after_commit callback because:
|
||||
# Enqueue webhook setup explicitly instead of relying on the after_commit callback because:
|
||||
# 1. Reauthorization flow updates an existing channel (not a create), so after_commit on: :create won't trigger
|
||||
# 2. We need to run check_channel_health_and_prompt_reauth after webhook setup completes
|
||||
# 3. The channel is marked with source: 'embedded_signup' to skip the after_commit callback
|
||||
channel.setup_webhooks
|
||||
# Skip health check during reauthorization — phone numbers in pending provisioning state
|
||||
# (platform_type: NOT_APPLICABLE) would incorrectly trigger a disconnect email right after
|
||||
# a successful reauth. Only run health check for new channel creation.
|
||||
check_channel_health_and_prompt_reauth(channel) if @inbox_id.blank?
|
||||
# 2. The channel is marked with source: 'embedded_signup' to skip the after_commit callback
|
||||
# The job runs Meta's slow phone-registration/subscription calls off the request thread so they
|
||||
# can't trip Rack::Timeout and roll back the just-created channel. The provisioning health check
|
||||
# runs inside the job, after registration — and only for new channels, since a reauthorized
|
||||
# number in a pending state would otherwise trigger a spurious disconnect right after reauth.
|
||||
channel.enqueue_webhook_setup(run_health_check: @inbox_id.blank?)
|
||||
channel
|
||||
|
||||
rescue StandardError => e
|
||||
@@ -55,24 +54,6 @@ class Whatsapp::EmbeddedSignupService
|
||||
end
|
||||
end
|
||||
|
||||
def check_channel_health_and_prompt_reauth(channel)
|
||||
health_data = Whatsapp::HealthService.new(channel).fetch_health_status
|
||||
return unless health_data
|
||||
|
||||
if channel_in_pending_state?(health_data)
|
||||
channel.prompt_reauthorization!
|
||||
else
|
||||
Rails.logger.info "[WHATSAPP] Channel #{channel.phone_number} health check passed"
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "[WHATSAPP] Health check failed for channel #{channel.phone_number}: #{e.message}"
|
||||
end
|
||||
|
||||
def channel_in_pending_state?(health_data)
|
||||
health_data[:platform_type] == 'NOT_APPLICABLE' ||
|
||||
health_data.dig(:throughput, 'level') == 'NOT_APPLICABLE'
|
||||
end
|
||||
|
||||
def validate_parameters!
|
||||
missing_params = []
|
||||
missing_params << 'code' if @code.blank?
|
||||
|
||||
@@ -13,8 +13,6 @@ class Whatsapp::IncomingMessageBaseService
|
||||
if processed_params.try(:[], :statuses).present?
|
||||
process_statuses
|
||||
elsif messages_data.present?
|
||||
return unless @inbox.active?
|
||||
|
||||
process_messages
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,10 +6,12 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
|
||||
end
|
||||
|
||||
def perform_reply
|
||||
return send_template_message if template_params.present?
|
||||
return send_session_message if message.conversation.can_reply?
|
||||
|
||||
message.update!(status: :failed, external_error: I18n.t('errors.whatsapp.message_outside_messaging_window'))
|
||||
should_send_template_message = template_params.present? || !message.conversation.can_reply?
|
||||
if should_send_template_message
|
||||
send_template_message
|
||||
else
|
||||
send_session_message
|
||||
end
|
||||
end
|
||||
|
||||
def send_template_message
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Resolves the WhatsApp channel for an inbound WhatsApp Cloud webhook. Meta's
|
||||
# display_phone_number can arrive formatted or in a country-specific variant (e.g. Brazil
|
||||
# omits the mobile 9, Argentina adds a digit after the country code), so we try the
|
||||
# raw digits first and then a normalized fallback, accepting only a candidate whose
|
||||
# phone_number_id matches.
|
||||
class Whatsapp::WebhookChannelFinderService
|
||||
def initialize(display_phone_number:, phone_number_id:)
|
||||
@display_phone_number = display_phone_number
|
||||
@phone_number_id = phone_number_id
|
||||
end
|
||||
|
||||
def perform
|
||||
return if digits.blank?
|
||||
|
||||
candidates = [
|
||||
Channel::Whatsapp.find_by(phone_number: "+#{digits}"),
|
||||
channel_by_normalized_number
|
||||
]
|
||||
candidates.compact.find { |channel| channel.provider_config['phone_number_id'] == @phone_number_id }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def digits
|
||||
@digits ||= @display_phone_number.to_s.gsub(/[^0-9]/, '')
|
||||
end
|
||||
|
||||
def channel_by_normalized_number
|
||||
normalizer = Whatsapp::PhoneNumberNormalizationService::NORMALIZERS
|
||||
.lazy.map(&:new).find { |n| n.handles_country?(digits) }
|
||||
return unless normalizer
|
||||
|
||||
Channel::Whatsapp.find_by(phone_number: "+#{normalizer.normalize(digits)}")
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
|
||||
@@ -154,7 +154,6 @@ en:
|
||||
invalid_token_permissions: 'The access token does not have the required permissions for WhatsApp.'
|
||||
phone_info_fetch_failed: 'Failed to fetch phone number information. Please try again.'
|
||||
phone_number_already_exists: 'Channel already exists for this phone number: %{phone_number}, please contact support if the error persists'
|
||||
message_outside_messaging_window: 'Message not sent because the WhatsApp 24-hour customer service window is closed and no template parameters were provided. Send an approved template message instead.'
|
||||
reauthorization:
|
||||
generic: 'Failed to reauthorize WhatsApp. Please try again.'
|
||||
not_supported: 'Reauthorization is not supported for this type of WhatsApp channel.'
|
||||
|
||||
@@ -581,12 +581,6 @@ 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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddActiveToInboxes < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :inboxes, :active, :boolean, default: true, null: false
|
||||
end
|
||||
end
|
||||
@@ -1048,7 +1048,6 @@ 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,7 +1,6 @@
|
||||
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
|
||||
@@ -97,10 +96,6 @@ 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?
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module Enterprise::Api::V1::Accounts::AgentsController
|
||||
def create
|
||||
super
|
||||
return if @agent.blank?
|
||||
|
||||
associate_agent_with_custom_role
|
||||
end
|
||||
|
||||
|
||||
@@ -90,10 +90,10 @@ class Twilio::VoiceController < ApplicationController
|
||||
from_number.start_with?('client:')
|
||||
end
|
||||
|
||||
# A fresh contact-initiated leg on an inbox that cannot receive calls.
|
||||
# A fresh contact-initiated leg on an inbox with inbound calls turned off.
|
||||
# Reject it so no conference, conversation, or Call row is created.
|
||||
def reject_inbound?
|
||||
twilio_direction == 'inbound' && !agent_leg?(twilio_from) && (!inbox.active? || !inbox.channel.inbound_calls_enabled?)
|
||||
twilio_direction == 'inbound' && !agent_leg?(twilio_from) && !inbox.channel.inbound_calls_enabled?
|
||||
end
|
||||
|
||||
def reject_twiml
|
||||
|
||||
@@ -16,7 +16,6 @@ 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.active? && inbox.channel.inbound_calls_enabled?
|
||||
unless 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
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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
|
||||
@@ -23,12 +23,6 @@ RSpec.describe AgentBuilder, type: :model do
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'locks the account while checking and creating the agent' do
|
||||
expect(account).to receive(:with_lock).and_call_original
|
||||
|
||||
agent_builder.perform
|
||||
end
|
||||
|
||||
context 'when user does not exist' do
|
||||
it 'creates a new user' do
|
||||
expect { agent_builder.perform }.to change(User, :count).by(1)
|
||||
@@ -73,17 +67,5 @@ RSpec.describe AgentBuilder, type: :model do
|
||||
expect(user.encrypted_password).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account has reached its agent limit' do
|
||||
before do
|
||||
allow(account).to receive(:usage_limits).and_return({ agents: account.account_users.count })
|
||||
end
|
||||
|
||||
it 'raises a limit exceeded error without creating a user' do
|
||||
expect { agent_builder.perform }.to raise_error(described_class::LimitExceededError, described_class::LIMIT_EXCEEDED_MESSAGE)
|
||||
|
||||
expect(User.from_email(email)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,9 +13,7 @@ RSpec.describe 'Linear Integration API', type: :request do
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/:account_id/integrations/linear' do
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'deletes the linear integration when the user is an administrator' do
|
||||
it 'deletes the linear integration' do
|
||||
# Stub the HTTP call to Linear's revoke endpoint
|
||||
allow(HTTParty).to receive(:post).with(
|
||||
'https://api.linear.app/oauth/revoke',
|
||||
@@ -23,19 +21,11 @@ RSpec.describe 'Linear Integration API', type: :request do
|
||||
).and_return(instance_double(HTTParty::Response, success?: true))
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/linear",
|
||||
headers: admin.create_new_auth_token,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(account.hooks.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for an agent and keeps the integration' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/linear",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
expect(account.hooks.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/:account_id/integrations/linear/teams' do
|
||||
|
||||
@@ -159,33 +159,19 @@ RSpec.describe 'Shopify Integration API', type: :request do
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/:account_id/integrations/shopify' do
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
before do
|
||||
create(:integrations_hook, :shopify, account: account)
|
||||
end
|
||||
|
||||
context 'when it is an administrator' do
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes the shopify integration' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change { account.hooks.count }.by(-1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns unauthorized and keeps the integration' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to(change { account.hooks.count })
|
||||
end.to change { account.hooks.count }.by(-1)
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -31,18 +31,6 @@ 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
|
||||
@@ -60,17 +48,6 @@ 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,18 +37,6 @@ 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
|
||||
@@ -267,18 +255,6 @@ 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
|
||||
|
||||
@@ -287,32 +263,19 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/widget/contact/destroy_custom_attributes', params: { website_token: '' }
|
||||
post '/api/v1/widget/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/contact/destroy_custom_attributes',
|
||||
post '/api/v1/widget/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,18 +43,6 @@ 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
|
||||
@@ -74,19 +62,6 @@ 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 },
|
||||
@@ -269,40 +244,11 @@ 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,24 +34,6 @@ 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,20 +34,6 @@ 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,17 +70,6 @@ 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,18 +39,6 @@ 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
|
||||
@@ -79,18 +67,6 @@ 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,18 +28,6 @@ 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 },
|
||||
@@ -55,20 +43,6 @@ 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 }
|
||||
@@ -239,21 +213,6 @@ 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,18 +139,5 @@ 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
|
||||
|
||||
@@ -21,27 +21,6 @@ RSpec.describe 'Agents API', type: :request do
|
||||
expect(response).to have_http_status(:payment_required)
|
||||
expect(response.body).to include('Account limit exceeded. Please purchase more licenses')
|
||||
end
|
||||
|
||||
it 'prevents adding an agent if the last seat is consumed before creation' do
|
||||
account.update!(limits: { agents: account.account_users.count + 1 })
|
||||
competing_agent_created = false
|
||||
|
||||
allow(AgentBuilder).to receive(:new).and_wrap_original do |method, *args|
|
||||
unless competing_agent_created
|
||||
create(:user, account: account, role: :agent)
|
||||
competing_agent_created = true
|
||||
end
|
||||
|
||||
method.call(*args)
|
||||
end
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/agents", params: params, headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:payment_required)
|
||||
expect(response.body).to include('Account limit exceeded. Please purchase more licenses')
|
||||
expect(User.from_email(params[:email])).to be_nil
|
||||
expect(account.account_users.count).to eq(account.usage_limits[:agents])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -208,18 +208,6 @@ 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,23 +129,6 @@ 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,20 +127,5 @@ 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,19 +44,6 @@ 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) }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user