diff --git a/VERSION_CW b/VERSION_CW
index ecbc3b030..27593c841 100644
--- a/VERSION_CW
+++ b/VERSION_CW
@@ -1 +1 @@
-4.16.0
+4.16.1
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 438944f04..864c50bb4 100644
--- a/app/controllers/api/v1/accounts/agents_controller.rb
+++ b/app/controllers/api/v1/accounts/agents_controller.rb
@@ -1,8 +1,6 @@
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
@@ -20,6 +18,8 @@ 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,25 +36,13 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
def bulk_create
emails = params[: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
-
+ 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
@@ -87,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/controllers/api/v1/accounts/integrations/base_controller.rb b/app/controllers/api/v1/accounts/integrations/base_controller.rb
new file mode 100644
index 000000000..ef1ebb713
--- /dev/null
+++ b/app/controllers/api/v1/accounts/integrations/base_controller.rb
@@ -0,0 +1,9 @@
+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
diff --git a/app/controllers/api/v1/accounts/integrations/hooks_controller.rb b/app/controllers/api/v1/accounts/integrations/hooks_controller.rb
index 087a9b78d..aec105930 100644
--- a/app/controllers/api/v1/accounts/integrations/hooks_controller.rb
+++ b/app/controllers/api/v1/accounts/integrations/hooks_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
+class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Integrations::BaseController
before_action :fetch_hook, except: [:create]
before_action :check_authorization
@@ -35,10 +35,6 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
@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
diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb
index 9ca0c72fd..8ae3109b9 100644
--- a/app/controllers/api/v1/accounts/integrations/linear_controller.rb
+++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb
@@ -1,6 +1,7 @@
-class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
+class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Integrations::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
diff --git a/app/controllers/api/v1/accounts/integrations/notion_controller.rb b/app/controllers/api/v1/accounts/integrations/notion_controller.rb
index ecf6bae6e..29343e4f5 100644
--- a/app/controllers/api/v1/accounts/integrations/notion_controller.rb
+++ b/app/controllers/api/v1/accounts/integrations/notion_controller.rb
@@ -1,5 +1,6 @@
-class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::BaseController
+class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::Integrations::BaseController
before_action :fetch_hook, only: [:destroy]
+ before_action :check_authorization, only: [:destroy]
def destroy
@hook.destroy!
diff --git a/app/controllers/api/v1/accounts/integrations/shopify_controller.rb b/app/controllers/api/v1/accounts/integrations/shopify_controller.rb
index 7fe31889b..c847a85df 100644
--- a/app/controllers/api/v1/accounts/integrations/shopify_controller.rb
+++ b/app/controllers/api/v1/accounts/integrations/shopify_controller.rb
@@ -1,7 +1,8 @@
-class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::BaseController
+class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::Integrations::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
diff --git a/app/controllers/webhooks/whatsapp_controller.rb b/app/controllers/webhooks/whatsapp_controller.rb
index ee71f3c92..8fd18678c 100644
--- a/app/controllers/webhooks/whatsapp_controller.rb
+++ b/app/controllers/webhooks/whatsapp_controller.rb
@@ -47,18 +47,10 @@ class Webhooks::WhatsappController < ActionController::API
metadata = params.dig(:entry, 0, :changes, 0, :value, :metadata)
return if metadata.blank?
- 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}"
+ Whatsapp::WebhookChannelFinderService.new(
+ display_phone_number: metadata[:display_phone_number],
+ phone_number_id: metadata[:phone_number_id]
+ ).perform
end
def inactive_whatsapp_number?
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-next/NewConversation/ComposeConversation.vue b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue
index 02a00c703..2446e0e2b 100644
--- a/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue
@@ -234,6 +234,7 @@ onMounted(() => resetContacts());
ref="popoverRef"
:align="align"
:show-content-border="false"
+ :close-on-scroll="false"
@show="onPopoverShow"
@hide="onPopoverHide"
>
diff --git a/app/javascript/dashboard/components-next/popover/Popover.vue b/app/javascript/dashboard/components-next/popover/Popover.vue
index 5b67e572f..9d369133f 100644
--- a/app/javascript/dashboard/components-next/popover/Popover.vue
+++ b/app/javascript/dashboard/components-next/popover/Popover.vue
@@ -1,7 +1,11 @@
-