Files
leadchat/app/models/account.rb
Pranav 8f95fafff4 feat: Add a setting to keep conversations pending on bot failures (#13512)
Adds an account-level setting `keep_pending_on_bot_failure` to control
whether conversations should move from pending to open when agent bot
webhooks fail.

Some users experience occasional message drops and don't want
conversations to automatically reopen due to transient bot failures.
This setting gives accounts control over that behavior. This is a
temporary setting which will be removed in future once a proper fix for
it is done, so it is not added in the UI.
2026-02-10 17:27:42 -08:00

225 lines
8.7 KiB
Ruby

# == Schema Information
#
# Table name: accounts
#
# id :integer not null, primary key
# auto_resolve_duration :integer
# custom_attributes :jsonb
# domain :string(100)
# feature_flags :bigint default(0), not null
# internal_attributes :jsonb not null
# limits :jsonb
# locale :integer default("en")
# name :string not null
# settings :jsonb
# status :integer default("active")
# support_email :string(100)
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_accounts_on_status (status)
#
class Account < ApplicationRecord
# used for single column multi flags
include FlagShihTzu
include Reportable
include Featurable
include CacheKeys
include CaptainFeaturable
include AccountEmailRateLimitable
SETTINGS_PARAMS_SCHEMA = {
'type': 'object',
'properties':
{
'auto_resolve_after': { 'type': %w[integer null], 'minimum': 10, 'maximum': 1_439_856 },
'auto_resolve_message': { 'type': %w[string null] },
'auto_resolve_ignore_waiting': { 'type': %w[boolean null] },
'audio_transcriptions': { 'type': %w[boolean null] },
'auto_resolve_label': { 'type': %w[string null] },
'keep_pending_on_bot_failure': { 'type': %w[boolean null] },
'conversation_required_attributes': {
'type': %w[array null],
'items': { 'type': 'string' }
},
'captain_models': {
'type': %w[object null],
'properties': {
'editor': { 'type': %w[string null] },
'assistant': { 'type': %w[string null] },
'copilot': { 'type': %w[string null] },
'label_suggestion': { 'type': %w[string null] },
'audio_transcription': { 'type': %w[string null] },
'help_center_search': { 'type': %w[string null] }
},
'additionalProperties': false
},
'captain_features': {
'type': %w[object null],
'properties': {
'editor': { 'type': %w[boolean null] },
'assistant': { 'type': %w[boolean null] },
'copilot': { 'type': %w[boolean null] },
'label_suggestion': { 'type': %w[boolean null] },
'audio_transcription': { 'type': %w[boolean null] },
'help_center_search': { 'type': %w[boolean null] }
},
'additionalProperties': false
}
},
'required': [],
'additionalProperties': true
}.to_json.freeze
DEFAULT_QUERY_SETTING = {
flag_query_mode: :bit_operator,
check_for_column: false
}.freeze
validates :name, presence: true
validates :domain, length: { maximum: 100 }
validates_with JsonSchemaValidator,
schema: SETTINGS_PARAMS_SCHEMA,
attribute_resolver: ->(record) { record.settings }
store_accessor :settings, :auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
store_accessor :settings, :captain_models, :captain_features
store_accessor :settings, :keep_pending_on_bot_failure
has_many :account_users, dependent: :destroy_async
has_many :agent_bot_inboxes, dependent: :destroy_async
has_many :agent_bots, dependent: :destroy_async
has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api'
has_many :articles, dependent: :destroy_async, class_name: '::Article'
has_many :assignment_policies, dependent: :destroy_async
has_many :automation_rules, dependent: :destroy_async
has_many :macros, dependent: :destroy_async
has_many :campaigns, dependent: :destroy_async
has_many :canned_responses, dependent: :destroy_async
has_many :categories, dependent: :destroy_async, class_name: '::Category'
has_many :contacts, dependent: :destroy_async
has_many :conversations, dependent: :destroy_async
has_many :csat_survey_responses, dependent: :destroy_async
has_many :custom_attribute_definitions, dependent: :destroy_async
has_many :custom_filters, dependent: :destroy_async
has_many :dashboard_apps, dependent: :destroy_async
has_many :data_imports, dependent: :destroy_async
has_many :email_channels, dependent: :destroy_async, class_name: '::Channel::Email'
has_many :facebook_pages, dependent: :destroy_async, class_name: '::Channel::FacebookPage'
has_many :instagram_channels, dependent: :destroy_async, class_name: '::Channel::Instagram'
has_many :tiktok_channels, dependent: :destroy_async, class_name: '::Channel::Tiktok'
has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook'
has_many :inboxes, dependent: :destroy_async
has_many :labels, dependent: :destroy_async
has_many :line_channels, dependent: :destroy_async, class_name: '::Channel::Line'
has_many :mentions, dependent: :destroy_async
has_many :messages, dependent: :destroy_async
has_many :notes, dependent: :destroy_async
has_many :notification_settings, dependent: :destroy_async
has_many :notifications, dependent: :destroy_async
has_many :portals, dependent: :destroy_async, class_name: '::Portal'
has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms'
has_many :teams, dependent: :destroy_async
has_many :telegram_channels, dependent: :destroy_async, class_name: '::Channel::Telegram'
has_many :twilio_sms, dependent: :destroy_async, class_name: '::Channel::TwilioSms'
has_many :twitter_profiles, dependent: :destroy_async, class_name: '::Channel::TwitterProfile'
has_many :users, through: :account_users
has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget'
has_many :webhooks, dependent: :destroy_async
has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
has_many :working_hours, dependent: :destroy_async
has_one_attached :contacts_export
enum :locale, LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h, prefix: true
enum :status, { active: 0, suspended: 1 }
scope :with_auto_resolve, -> { where("(settings ->> 'auto_resolve_after')::int IS NOT NULL") }
before_validation :validate_limit_keys
after_create_commit :notify_creation
after_destroy :remove_account_sequences
def agents
users.where(account_users: { role: :agent })
end
def administrators
users.where(account_users: { role: :administrator })
end
def all_conversation_tags
# returns array of tags
conversation_ids = conversations.pluck(:id)
ActsAsTaggableOn::Tagging.includes(:tag)
.where(context: 'labels',
taggable_type: 'Conversation',
taggable_id: conversation_ids)
.map { |tagging| tagging.tag.name }
end
def webhook_data
{
id: id,
name: name
}
end
def inbound_email_domain
domain.presence || GlobalConfig.get('MAILER_INBOUND_EMAIL_DOMAIN')['MAILER_INBOUND_EMAIL_DOMAIN'] || ENV.fetch('MAILER_INBOUND_EMAIL_DOMAIN',
false)
end
def support_email
super.presence || ENV.fetch('MAILER_SENDER_EMAIL') { GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] }
end
def usage_limits
{
agents: ChatwootApp.max_limit.to_i,
inboxes: ChatwootApp.max_limit.to_i
}
end
def locale_english_name
# the locale can also be something like pt_BR, en_US, fr_FR, etc.
# the format is `<locale_code>_<country_code>`
# we need to extract the language code from the locale
account_locale = locale&.split('_')&.first
ISO_639.find(account_locale)&.english_name&.downcase || 'english'
end
private
def notify_creation
Rails.configuration.dispatcher.dispatch(ACCOUNT_CREATED, Time.zone.now, account: self)
end
trigger.after(:insert).for_each(:row) do
"execute format('create sequence IF NOT EXISTS conv_dpid_seq_%s', NEW.id);"
end
trigger.name('camp_dpid_before_insert').after(:insert).for_each(:row) do
"execute format('create sequence IF NOT EXISTS camp_dpid_seq_%s', NEW.id);"
end
def validate_limit_keys
# method overridden in enterprise module
end
def remove_account_sequences
ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS camp_dpid_seq_#{id}")
ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS conv_dpid_seq_#{id}")
end
end
Account.prepend_mod_with('Account')
Account.prepend_mod_with('Account::PlanUsageAndLimits')
Account.include_mod_with('Concerns::Account')
Account.include_mod_with('Audit::Account')