diff --git a/app/builders/agent_builder.rb b/app/builders/agent_builder.rb index d2715011c..af68eefc5 100644 --- a/app/builders/agent_builder.rb +++ b/app/builders/agent_builder.rb @@ -2,6 +2,14 @@ # 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. @@ -14,15 +22,23 @@ class AgentBuilder # Creates a user and account user in a transaction. # @return [User] the created user. def perform - ActiveRecord::Base.transaction do - @user = find_or_create_user - create_account_user + account.with_lock do + raise LimitExceededError unless can_add_agent? + + ActiveRecord::Base.transaction do + @user = find_or_create_user + create_account_user + end 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 diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index b8907ff2e..864c50bb4 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -1,30 +1,25 @@ 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 end def create - # Lock the account row so concurrent invites can't both pass the seat-limit check (TOCTOU). - Current.account.with_lock do - next unless can_add_agent? + builder = AgentBuilder.new( + email: new_agent_params['email'], + name: new_agent_params['name'], + role: new_agent_params['role'], + availability: new_agent_params['availability'], + auto_offline: new_agent_params['auto_offline'], + inviter: current_user, + account: Current.account + ) - @agent = AgentBuilder.new( - email: new_agent_params['email'], - name: new_agent_params['name'], - role: new_agent_params['role'], - availability: new_agent_params['availability'], - auto_offline: new_agent_params['auto_offline'], - inviter: current_user, - account: Current.account - ).perform - end - - render_payment_required('Account limit exceeded. Please purchase more licenses') if @agent.blank? + @agent = builder.perform + rescue AgentBuilder::LimitExceededError => e + render_payment_required(e.message) end def update @@ -41,40 +36,17 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController def bulk_create emails = params[:emails] - # Lock the account row so concurrent bulk invites can't collectively exceed the seat limit (TOCTOU). - limit_exceeded = false - Current.account.with_lock do - if emails.count > available_agent_count - limit_exceeded = true - next - end - - invite_agents(emails) - end - return render_payment_required('Account limit exceeded. Please purchase more licenses') if limit_exceeded - + bulk_create_agents(emails) # 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 - Current.account.custom_attributes.delete('onboarding_step') - Current.account.save! + clear_onboarding_step head :ok + rescue AgentBuilder::LimitExceededError => e + render_payment_required(e.message) end private - def invite_agents(emails) - emails.each do |email| - AgentBuilder.new( - email: email, - name: email.split('@').first, - inviter: current_user, - account: Current.account - ).perform - rescue ActiveRecord::RecordInvalid => e - Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}" - end - end - def check_authorization super(User) end @@ -103,22 +75,33 @@ 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 validate_limit_for_bulk_create - limit_available = params[:emails].count <= available_agent_count + def bulk_create_agents(emails) + Current.account.with_lock do + raise AgentBuilder::LimitExceededError if emails.count > available_agent_count - render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available + emails.each { |email| create_agent_from_email(email) } + end end - def validate_limit - render_payment_required('Account limit exceeded. Please purchase more licenses') unless can_add_agent? + 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! end def available_agent_count - Current.account.usage_limits[:agents] - agents.count - end - - def can_add_agent? - available_agent_count.positive? + Current.account.usage_limits[:agents] - Current.account.account_users.count end def delete_user_record(agent) diff --git a/app/javascript/dashboard/api/captain/assistant.js b/app/javascript/dashboard/api/captain/assistant.js index 5af6110ab..806b45bb2 100644 --- a/app/javascript/dashboard/api/captain/assistant.js +++ b/app/javascript/dashboard/api/captain/assistant.js @@ -26,13 +26,20 @@ class CaptainAssistant extends ApiClient { }); } - getStats({ assistantId, range, signal }) { + getMetrics({ assistantId, range, signal }) { const requestConfig = { params: { range, timezone_offset: getTimezoneOffset() }, }; if (signal) requestConfig.signal = signal; - return axios.get(`${this.url}/${assistantId}/stats`, requestConfig); + return axios.get(`${this.url}/${assistantId}/metrics`, requestConfig); + } + + getFaqStats({ assistantId, signal }) { + const requestConfig = {}; + if (signal) requestConfig.signal = signal; + + return axios.get(`${this.url}/${assistantId}/faq_stats`, requestConfig); } getSummary({ assistantId, range, stats }) { diff --git a/app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue b/app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue index 1c7787de6..1f3c03a2e 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue @@ -8,6 +8,8 @@ import { EditorState, Selection, imageResizeView, + toggleMark, + wrapInList, } from '@chatwoot/prosemirror-schema'; import { suggestionsPlugin, @@ -17,8 +19,6 @@ import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image'; import embedPreviewPlugin from '@chatwoot/prosemirror-schema/src/plugins/embedPreview'; import trailingParagraphPlugin from '@chatwoot/prosemirror-schema/src/plugins/trailingParagraph'; import { embeds as markdownEmbeds } from 'dashboard/helper/markdownEmbeds'; -import { toggleMark } from 'prosemirror-commands'; -import { wrapInList } from 'prosemirror-schema-list'; import { toggleBlockType } from '@chatwoot/prosemirror-schema/src/menu/common'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; import { isEscape } from 'shared/helpers/KeyboardHelpers'; diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 6cc2747da..b013a47b9 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -33,9 +33,7 @@ import { // constants import { BUS_EVENTS } from 'shared/constants/busEvents'; import { REPLY_POLICY } from 'shared/constants/links'; -import wootConstants, { - META_RESTRICTION_STATUS_URL, -} from 'dashboard/constants/globals'; +import wootConstants from 'dashboard/constants/globals'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { INBOX_TYPES } from 'dashboard/helper/inbox'; @@ -95,7 +93,6 @@ export default { currentUserId: 'getCurrentUserID', listLoadingStatus: 'getAllMessagesLoaded', currentAccountId: 'getCurrentAccountId', - isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', }), isOpen() { return this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN; @@ -173,13 +170,6 @@ export default { instagramInbox ); }, - isInstagramRestrictionBannerVisible() { - return this.isOnChatwootCloud && this.isAnInstagramChannel; - }, - instagramRestrictionStatusUrl() { - return META_RESTRICTION_STATUS_URL; - }, - replyWindowBannerMessage() { if (this.isAWhatsAppChannel) { return this.$t('CONVERSATION.TWILIO_WHATSAPP_CAN_REPLY'); @@ -464,15 +454,7 @@ export default { >