feat (ee): APIs to configure an auto assignment limit for inboxes (#4672)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
ae72757d23
commit
713fdb44ee
20 changed files with 255 additions and 45 deletions
|
@ -42,7 +42,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@inbox.update(permitted_params.except(:channel))
|
@inbox.update!(permitted_params.except(:channel))
|
||||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||||
|
|
||||||
|
@ -109,10 +109,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
@inbox.channel.save!
|
@inbox.channel.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inbox_attributes
|
||||||
|
[: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]
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params(channel_attributes = [])
|
def permitted_params(channel_attributes = [])
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
*inbox_attributes,
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
|
||||||
channel: [:type, *channel_attributes]
|
channel: [:type, *channel_attributes]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -129,18 +133,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
}[permitted_params[:channel][:type]]
|
}[permitted_params[:channel][:type]]
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_channels_method
|
|
||||||
{
|
|
||||||
'web_widget' => Current.account.web_widgets,
|
|
||||||
'api' => Current.account.api_channels,
|
|
||||||
'email' => Current.account.email_channels,
|
|
||||||
'line' => Current.account.line_channels,
|
|
||||||
'telegram' => Current.account.telegram_channels,
|
|
||||||
'whatsapp' => Current.account.whatsapp_channels,
|
|
||||||
'sms' => Current.account.sms_channels
|
|
||||||
}[permitted_params[:channel][:type]]
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_channel_attributes(channel_type)
|
def get_channel_attributes(channel_type)
|
||||||
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
|
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
|
||||||
channel_type.constantize::EDITABLE_ATTRS.presence
|
channel_type.constantize::EDITABLE_ATTRS.presence
|
||||||
|
@ -148,10 +140,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_limit
|
|
||||||
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
|
||||||
|
|
||||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include DeviseTokenAuth::Concerns::SetUserByToken
|
include DeviseTokenAuth::Concerns::SetUserByToken
|
||||||
include RequestExceptionHandler
|
include RequestExceptionHandler
|
||||||
include Pundit
|
include Pundit::Authorization
|
||||||
include SwitchLocale
|
include SwitchLocale
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
|
|
@ -43,7 +43,8 @@ class DashboardController < ActionController::Base
|
||||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||||
FACEBOOK_API_VERSION: 'v14.0'
|
FACEBOOK_API_VERSION: 'v14.0',
|
||||||
|
IS_ENTERPRISE: ChatwootApp.enterprise?
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -74,4 +74,22 @@ module Api::V1::InboxesHelper
|
||||||
context.verify_mode = openssl_verify_mode
|
context.verify_mode = openssl_verify_mode
|
||||||
context
|
context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def account_channels_method
|
||||||
|
{
|
||||||
|
'web_widget' => Current.account.web_widgets,
|
||||||
|
'api' => Current.account.api_channels,
|
||||||
|
'email' => Current.account.email_channels,
|
||||||
|
'line' => Current.account.line_channels,
|
||||||
|
'telegram' => Current.account.telegram_channels,
|
||||||
|
'whatsapp' => Current.account.whatsapp_channels,
|
||||||
|
'sms' => Current.account.sms_channels
|
||||||
|
}[permitted_params[:channel][:type]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_limit
|
||||||
|
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
||||||
|
|
||||||
|
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -421,6 +421,11 @@
|
||||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
|
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
|
||||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved."
|
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved."
|
||||||
},
|
},
|
||||||
|
"AUTO_ASSIGNMENT":{
|
||||||
|
"MAX_ASSIGNMENT_LIMIT": "Auto assignment limit",
|
||||||
|
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "Please enter a value greater than 0",
|
||||||
|
"MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "Limit the maximum number of conversations from this inbox that can be auto assigned to an agent"
|
||||||
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Reauthorize",
|
"TITLE": "Reauthorize",
|
||||||
"SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
"SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
||||||
|
|
|
@ -48,20 +48,47 @@
|
||||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }}
|
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }}
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<!-- disabling this block temporarily -->
|
||||||
|
<div
|
||||||
|
v-if="enableAutoAssignment && isEnterprise && false"
|
||||||
|
class="max-assignment-container"
|
||||||
|
>
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="maxAssignmentLimit"
|
||||||
|
type="number"
|
||||||
|
:class="{ error: $v.maxAssignmentLimit.$error }"
|
||||||
|
:error="maxAssignmentLimitErrors"
|
||||||
|
:label="$t('INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT')"
|
||||||
|
@blur="$v.maxAssignmentLimit.$touch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p class="help-text">
|
||||||
|
{{ $t('INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT_SUB_TEXT') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<woot-submit-button
|
||||||
|
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||||
|
:disabled="$v.maxAssignmentLimit.$invalid"
|
||||||
|
@click="updateInbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</settings-section>
|
</settings-section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { minValue } from 'vuelidate/lib/validators';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import configMixin from 'shared/mixins/configMixin';
|
||||||
import SettingsSection from '../../../../../components/SettingsSection';
|
import SettingsSection from '../../../../../components/SettingsSection';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
mixins: [alertMixin, configMixin],
|
||||||
props: {
|
props: {
|
||||||
inbox: {
|
inbox: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -73,12 +100,21 @@ export default {
|
||||||
selectedAgents: [],
|
selectedAgents: [],
|
||||||
isAgentListUpdating: false,
|
isAgentListUpdating: false,
|
||||||
enableAutoAssignment: false,
|
enableAutoAssignment: false,
|
||||||
|
maxAssignmentLimit: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
agentList: 'agents/getAgents',
|
agentList: 'agents/getAgents',
|
||||||
}),
|
}),
|
||||||
|
maxAssignmentLimitErrors() {
|
||||||
|
if (this.$v.maxAssignmentLimit.$error) {
|
||||||
|
return this.$t(
|
||||||
|
'INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT_RANGE_ERROR'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
inbox() {
|
inbox() {
|
||||||
|
@ -91,6 +127,8 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
setDefaults() {
|
setDefaults() {
|
||||||
this.enableAutoAssignment = this.inbox.enable_auto_assignment;
|
this.enableAutoAssignment = this.inbox.enable_auto_assignment;
|
||||||
|
this.maxAssignmentLimit =
|
||||||
|
this.inbox.auto_assignment_config.max_assignment_limit || null;
|
||||||
this.fetchAttachedAgents();
|
this.fetchAttachedAgents();
|
||||||
},
|
},
|
||||||
async fetchAttachedAgents() {
|
async fetchAttachedAgents() {
|
||||||
|
@ -129,6 +167,9 @@ export default {
|
||||||
id: this.inbox.id,
|
id: this.inbox.id,
|
||||||
formData: false,
|
formData: false,
|
||||||
enable_auto_assignment: this.enableAutoAssignment,
|
enable_auto_assignment: this.enableAutoAssignment,
|
||||||
|
auto_assignment_config: {
|
||||||
|
max_assignment_limit: this.maxAssignmentLimit,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
await this.$store.dispatch('inboxes/updateInbox', payload);
|
await this.$store.dispatch('inboxes/updateInbox', payload);
|
||||||
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
|
@ -143,6 +184,19 @@ export default {
|
||||||
return !!this.selectedAgents.length;
|
return !!this.selectedAgents.length;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
maxAssignmentLimit: {
|
||||||
|
minValue: minValue(1),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import '~dashboard/assets/scss/variables';
|
||||||
|
@import '~dashboard/assets/scss/mixins';
|
||||||
|
|
||||||
|
.max-assignment-container {
|
||||||
|
padding-top: var(--space-slab);
|
||||||
|
padding-bottom: var(--space-slab);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -9,5 +9,8 @@ export default {
|
||||||
enabledLanguages() {
|
enabledLanguages() {
|
||||||
return window.chatwootConfig.enabledLanguages;
|
return window.chatwootConfig.enabledLanguages;
|
||||||
},
|
},
|
||||||
|
isEnterprise() {
|
||||||
|
return window.chatwootConfig.isEnterprise === 'true';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,6 @@ class Account < ApplicationRecord
|
||||||
include FlagShihTzu
|
include FlagShihTzu
|
||||||
include Reportable
|
include Reportable
|
||||||
include Featurable
|
include Featurable
|
||||||
prepend_mod_with('Account')
|
|
||||||
|
|
||||||
DEFAULT_QUERY_SETTING = {
|
DEFAULT_QUERY_SETTING = {
|
||||||
flag_query_mode: :bit_operator,
|
flag_query_mode: :bit_operator,
|
||||||
|
@ -146,3 +145,5 @@ class Account < ApplicationRecord
|
||||||
ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS conv_dpid_seq_#{id}")
|
ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS conv_dpid_seq_#{id}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Account.prepend_mod_with('Account')
|
||||||
|
|
|
@ -14,7 +14,7 @@ module RoundRobinHandler
|
||||||
return unless conversation_status_changed_to_open?
|
return unless conversation_status_changed_to_open?
|
||||||
return unless should_round_robin?
|
return unless should_round_robin?
|
||||||
|
|
||||||
::RoundRobin::AssignmentService.new(conversation: self).perform
|
::RoundRobin::AssignmentService.new(conversation: self, allowed_member_ids: inbox.member_ids_with_assignment_capacity).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def should_round_robin?
|
def should_round_robin?
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# allow_messages_after_resolved :boolean default(TRUE)
|
# allow_messages_after_resolved :boolean default(TRUE)
|
||||||
|
# auto_assignment_config :jsonb
|
||||||
# channel_type :string
|
# channel_type :string
|
||||||
# csat_survey_enabled :boolean default(FALSE)
|
# csat_survey_enabled :boolean default(FALSE)
|
||||||
# email_address :string
|
# email_address :string
|
||||||
|
@ -35,6 +36,7 @@ class Inbox < ApplicationRecord
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }
|
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }
|
||||||
|
validate :ensure_valid_max_assignment_limit
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
|
@ -118,9 +120,19 @@ class Inbox < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def member_ids_with_assignment_capacity
|
||||||
|
members.ids
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def ensure_valid_max_assignment_limit
|
||||||
|
# overridden in enterprise/app/models/enterprise/inbox.rb
|
||||||
|
end
|
||||||
|
|
||||||
def delete_round_robin_agents
|
def delete_round_robin_agents
|
||||||
::RoundRobin::ManageService.new(inbox: self).clear_queue
|
::RoundRobin::ManageService.new(inbox: self).clear_queue
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Inbox.prepend_mod_with('Inbox')
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# NOTE: available agent method now expect allowed_member_ids to be passed in always because of inbox limits feature
|
||||||
|
# need to refactor this class and split the queue managment into a seperate class
|
||||||
|
|
||||||
# If allowed_member_ids are supplied round robin service will only fetch a member from member id
|
# If allowed_member_ids are supplied round robin service will only fetch a member from member id
|
||||||
# This is used in case of team assignment
|
# This is used in case of team assignment
|
||||||
class RoundRobin::ManageService
|
class RoundRobin::ManageService
|
||||||
|
@ -18,6 +21,13 @@ class RoundRobin::ManageService
|
||||||
::Redis::Alfred.lrem(round_robin_key, user_id)
|
::Redis::Alfred.lrem(round_robin_key, user_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reset_queue
|
||||||
|
clear_queue
|
||||||
|
add_agent_to_queue(inbox.inbox_members.map(&:user_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
# end of queue management functions
|
||||||
|
|
||||||
def available_agent(priority_list: [])
|
def available_agent(priority_list: [])
|
||||||
reset_queue unless validate_queue?
|
reset_queue unless validate_queue?
|
||||||
user_id = get_member_via_priority_list(priority_list)
|
user_id = get_member_via_priority_list(priority_list)
|
||||||
|
@ -26,29 +36,22 @@ class RoundRobin::ManageService
|
||||||
inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present?
|
inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_queue
|
|
||||||
clear_queue
|
|
||||||
add_agent_to_queue(inbox.inbox_members.map(&:user_id))
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_user_id
|
def fetch_user_id
|
||||||
if allowed_member_ids_in_str.present?
|
return nil if allowed_member_ids_in_str.blank?
|
||||||
user_id = queue.intersection(allowed_member_ids_in_str).pop
|
|
||||||
pop_push_to_queue(user_id)
|
user_id = queue.intersection(allowed_member_ids_in_str).pop
|
||||||
user_id
|
pop_push_to_queue(user_id)
|
||||||
else
|
user_id
|
||||||
::Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# priority list is usually the members who are online passed from assignmebt service
|
# priority list is usually the members who are online passed from assignment service
|
||||||
def get_member_via_priority_list(priority_list)
|
def get_member_via_priority_list(priority_list)
|
||||||
return if priority_list.blank?
|
return if priority_list.blank?
|
||||||
|
|
||||||
# when allowed member ids is passed we will be looking to get members from that list alone
|
# When allowed member ids is passed we will be looking to get members from that list alone
|
||||||
priority_list = priority_list.intersection(allowed_member_ids_in_str) if allowed_member_ids_in_str.present?
|
priority_list = priority_list.intersection(allowed_member_ids_in_str)
|
||||||
return if priority_list.blank?
|
return if priority_list.blank?
|
||||||
|
|
||||||
user_id = queue.intersection(priority_list.map(&:to_s)).pop
|
user_id = queue.intersection(priority_list.map(&:to_s)).pop
|
||||||
|
|
|
@ -9,6 +9,7 @@ json.working_hours_enabled resource.working_hours_enabled
|
||||||
json.enable_email_collect resource.enable_email_collect
|
json.enable_email_collect resource.enable_email_collect
|
||||||
json.csat_survey_enabled resource.csat_survey_enabled
|
json.csat_survey_enabled resource.csat_survey_enabled
|
||||||
json.enable_auto_assignment resource.enable_auto_assignment
|
json.enable_auto_assignment resource.enable_auto_assignment
|
||||||
|
json.auto_assignment_config resource.auto_assignment_config
|
||||||
json.out_of_office_message resource.out_of_office_message
|
json.out_of_office_message resource.out_of_office_message
|
||||||
json.working_hours resource.weekly_schedule
|
json.working_hours resource.weekly_schedule
|
||||||
json.timezone resource.timezone
|
json.timezone resource.timezone
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
|
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
|
||||||
fbApiVersion: '<%= @global_config['FACEBOOK_API_VERSION'] %>',
|
fbApiVersion: '<%= @global_config['FACEBOOK_API_VERSION'] %>',
|
||||||
signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>',
|
signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>',
|
||||||
|
isEnterprise: '<%= @global_config['IS_ENTERPRISE'] %>',
|
||||||
<% if @global_config['VAPID_PUBLIC_KEY'] %>
|
<% if @global_config['VAPID_PUBLIC_KEY'] %>
|
||||||
vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(@global_config['VAPID_PUBLIC_KEY']).bytes %>),
|
vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(@global_config['VAPID_PUBLIC_KEY']).bytes %>),
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddAutoAssignmentConfigurationToInboxes < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :inboxes, :auto_assignment_config, :jsonb, default: {}
|
||||||
|
end
|
||||||
|
end
|
|
@ -513,6 +513,7 @@ ActiveRecord::Schema.define(version: 2022_06_10_091206) do
|
||||||
t.boolean "enable_email_collect", default: true
|
t.boolean "enable_email_collect", default: true
|
||||||
t.boolean "csat_survey_enabled", default: false
|
t.boolean "csat_survey_enabled", default: false
|
||||||
t.boolean "allow_messages_after_resolved", default: true
|
t.boolean "allow_messages_after_resolved", default: true
|
||||||
|
t.jsonb "auto_assignment_config", default: {}
|
||||||
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module Enterprise::Api::V1::Accounts::InboxesController
|
||||||
|
def inbox_attributes
|
||||||
|
super + ee_inbox_attributes
|
||||||
|
end
|
||||||
|
|
||||||
|
def ee_inbox_attributes
|
||||||
|
[auto_assignment_config: [:max_assignment_limit]]
|
||||||
|
end
|
||||||
|
end
|
20
enterprise/app/models/enterprise/inbox.rb
Normal file
20
enterprise/app/models/enterprise/inbox.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
module Enterprise::Inbox
|
||||||
|
def member_ids_with_assignment_capacity
|
||||||
|
max_assignment_limit = auto_assignment_config['max_assignment_limit']
|
||||||
|
overloaded_agent_ids = max_assignment_limit.present? ? get_agent_ids_over_assignment_limit(max_assignment_limit) : []
|
||||||
|
super - overloaded_agent_ids
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get_agent_ids_over_assignment_limit(limit)
|
||||||
|
conversations.open.select(:assignee_id).group(:assignee_id).having("count(*) >= #{limit.to_i}").filter_map(&:assignee_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_valid_max_assignment_limit
|
||||||
|
return if auto_assignment_config['max_assignment_limit'].blank?
|
||||||
|
return if auto_assignment_config['max_assignment_limit'].to_i.positive?
|
||||||
|
|
||||||
|
errors.add(:auto_assignment_config, 'max_assignment_limit must be greater than 0')
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,46 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Enterprise Inboxes API', type: :request do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
|
||||||
|
describe 'POST /api/v1/accounts/{account.id}/inboxes' do
|
||||||
|
let(:inbox) { create(:inbox, account: account) }
|
||||||
|
|
||||||
|
context 'when it is an authenticated user' do
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:valid_params) do
|
||||||
|
{ name: 'test', auto_assignment_config: { max_assignment_limit: 10 }, channel: { type: 'web_widget', website_url: 'test.com' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a webwidget inbox with auto assignment config' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/inboxes",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
params: valid_params,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(JSON.parse(response.body)['auto_assignment_config']['max_assignment_limit']).to eq 10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /api/v1/accounts/{account.id}/inboxes/:id' do
|
||||||
|
let(:inbox) { create(:inbox, account: account, auto_assignment_config: { max_assignment_limit: 5 }) }
|
||||||
|
|
||||||
|
context 'when it is an authenticated user' do
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:valid_params) { { name: 'new test inbox', auto_assignment_config: { max_assignment_limit: 10 } } }
|
||||||
|
|
||||||
|
it 'updates inbox with auto assignment config' do
|
||||||
|
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
params: valid_params,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(JSON.parse(response.body)['auto_assignment_config']['max_assignment_limit']).to eq 10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
38
spec/enterprise/models/inbox_spec.rb
Normal file
38
spec/enterprise/models/inbox_spec.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Inbox do
|
||||||
|
describe 'member_ids_with_assignment_capacity' do
|
||||||
|
let!(:inbox) { create(:inbox) }
|
||||||
|
let!(:inbox_member_1) { create(:inbox_member, inbox: inbox) }
|
||||||
|
let!(:inbox_member_2) { create(:inbox_member, inbox: inbox) }
|
||||||
|
let!(:inbox_member_3) { create(:inbox_member, inbox: inbox) }
|
||||||
|
let!(:inbox_member_4) { create(:inbox_member, inbox: inbox) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:conversation, inbox: inbox, assignee: inbox_member_1.user)
|
||||||
|
# to test conversations in other inboxes won't impact
|
||||||
|
create_list(:conversation, 3, assignee: inbox_member_1.user)
|
||||||
|
create_list(:conversation, 2, inbox: inbox, assignee: inbox_member_2.user)
|
||||||
|
create_list(:conversation, 3, inbox: inbox, assignee: inbox_member_3.user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'validated max_assignment_limit' do
|
||||||
|
account = create(:account)
|
||||||
|
expect(build(:inbox, account: account, auto_assignment_config: { max_assignment_limit: 0 })).not_to be_valid
|
||||||
|
expect(build(:inbox, account: account, auto_assignment_config: {})).to be_valid
|
||||||
|
expect(build(:inbox, account: account, auto_assignment_config: { max_assignment_limit: 1 })).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns member ids with assignment capacity with inbox max_assignment_limit is configured' do
|
||||||
|
# agent 1 has 1 conversations, agent 2 has 2 conversations, agent 3 has 3 conversations and agent 4 with none
|
||||||
|
inbox.update(auto_assignment_config: { max_assignment_limit: 2 })
|
||||||
|
expect(inbox.member_ids_with_assignment_capacity).to contain_exactly(inbox_member_1.user_id, inbox_member_4.user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns all member ids when inbox max_assignment_limit is not configured' do
|
||||||
|
expect(inbox.member_ids_with_assignment_capacity).to eq(inbox.members.ids)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,10 +8,14 @@ describe RoundRobin::ManageService do
|
||||||
let!(:inbox_members) { create_list(:inbox_member, 5, inbox: inbox) }
|
let!(:inbox_members) { create_list(:inbox_member, 5, inbox: inbox) }
|
||||||
|
|
||||||
describe '#available_agent' do
|
describe '#available_agent' do
|
||||||
it 'gets the first available agent and move agent to end of the list' do
|
it 'returns nil if allowed_member_ids is empty' do
|
||||||
|
expect(described_class.new(inbox: inbox, allowed_member_ids: []).available_agent).to eq nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the first available agent in allowed_member_ids and move agent to end of the list' do
|
||||||
expected_queue = [inbox_members[0].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[2].user_id,
|
expected_queue = [inbox_members[0].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[2].user_id,
|
||||||
inbox_members[1].user_id].map(&:to_s)
|
inbox_members[1].user_id].map(&:to_s)
|
||||||
round_robin_manage_service.available_agent
|
described_class.new(inbox: inbox, allowed_member_ids: [inbox_members[0].user_id, inbox_members[4].user_id]).available_agent
|
||||||
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
|
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -19,8 +23,8 @@ describe RoundRobin::ManageService do
|
||||||
expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id,
|
expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id,
|
||||||
inbox_members[0].user_id].map(&:to_s)
|
inbox_members[0].user_id].map(&:to_s)
|
||||||
# prority list will be ids in string, since thats what redis supplies to us
|
# prority list will be ids in string, since thats what redis supplies to us
|
||||||
expect(round_robin_manage_service.available_agent(priority_list: [inbox_members[3].user_id.to_s,
|
expect(described_class.new(inbox: inbox, allowed_member_ids: [inbox_members[2].user_id])
|
||||||
inbox_members[2].user_id.to_s])).to eq inbox_members[2].user
|
.available_agent(priority_list: [inbox_members[3].user_id.to_s, inbox_members[2].user_id.to_s])).to eq inbox_members[2].user
|
||||||
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
|
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue