Merge branch 'develop' into feat/voice-as-twilio-capability
This commit is contained in:
+3
-3
@@ -20,11 +20,11 @@ const excludedLabels = defineModel('excludedLabels', {
|
||||
|
||||
const excludeOlderThanMinutes = defineModel('excludeOlderThanMinutes', {
|
||||
type: Number,
|
||||
default: 10,
|
||||
default: null,
|
||||
});
|
||||
|
||||
// Duration limits: 10 minutes to 999 days (in minutes)
|
||||
const MIN_DURATION_MINUTES = 10;
|
||||
// Duration limits: 1 minute to 999 days (in minutes)
|
||||
const MIN_DURATION_MINUTES = 1;
|
||||
const MAX_DURATION_MINUTES = 1438560; // 999 days * 24 hours * 60 minutes
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
+2
-1
@@ -27,7 +27,7 @@ const { t } = useI18n();
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY';
|
||||
const DEFAULT_CONVERSATION_LIMIT = 10;
|
||||
const MIN_CONVERSATION_LIMIT = 1;
|
||||
const MIN_CONVERSATION_LIMIT = 0;
|
||||
const MAX_CONVERSATION_LIMIT = 100000;
|
||||
|
||||
const selectedInboxIds = computed(
|
||||
@@ -42,6 +42,7 @@ const availableInboxes = computed(() =>
|
||||
|
||||
const isLimitValid = limit => {
|
||||
return (
|
||||
Number.isInteger(limit.conversationLimit) &&
|
||||
limit.conversationLimit >= MIN_CONVERSATION_LIMIT &&
|
||||
limit.conversationLimit <= MAX_CONVERSATION_LIMIT
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ const convertToMinutes = newValue => {
|
||||
|
||||
const transformedValue = computed({
|
||||
get() {
|
||||
if (duration.value == null) return null;
|
||||
if (unit.value === DURATION_UNITS.MINUTES) return duration.value;
|
||||
if (unit.value === DURATION_UNITS.HOURS)
|
||||
return Math.floor(duration.value / 60);
|
||||
@@ -41,6 +42,10 @@ const transformedValue = computed({
|
||||
return 0;
|
||||
},
|
||||
set(newValue) {
|
||||
if (newValue == null || newValue === '') {
|
||||
duration.value = null;
|
||||
return;
|
||||
}
|
||||
let minuteValue = convertToMinutes(newValue);
|
||||
|
||||
duration.value = Math.min(Math.max(minuteValue, props.min), props.max);
|
||||
@@ -53,6 +58,7 @@ const transformedValue = computed({
|
||||
// this might create some confusion, especially when saving
|
||||
// this watcher fixes it by rounding the duration basically, to the nearest unit value
|
||||
watch(unit, () => {
|
||||
if (duration.value == null) return;
|
||||
let adjustedValue = convertToMinutes(transformedValue.value);
|
||||
duration.value = Math.min(Math.max(adjustedValue, props.min), props.max);
|
||||
});
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ const formData = computed(() => ({
|
||||
...(selectedPolicy.value?.exclusionRules?.excludedLabels || []),
|
||||
],
|
||||
excludeOlderThanHours:
|
||||
selectedPolicy.value?.exclusionRules?.excludeOlderThanHours || 10,
|
||||
selectedPolicy.value?.exclusionRules?.excludeOlderThanHours ?? null,
|
||||
},
|
||||
inboxCapacityLimits:
|
||||
selectedPolicy.value?.inboxCapacityLimits?.map(limit => ({
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ const props = defineProps({
|
||||
enabled: false,
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
excludeOlderThanHours: null,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
}),
|
||||
@@ -84,7 +84,7 @@ const state = reactive({
|
||||
description: '',
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
excludeOlderThanHours: null,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
});
|
||||
@@ -120,7 +120,7 @@ const resetForm = () => {
|
||||
description: '',
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
excludeOlderThanHours: null,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
});
|
||||
|
||||
@@ -17,9 +17,10 @@ class AgentBotListener < BaseListener
|
||||
|
||||
def conversation_updated(event)
|
||||
conversation = extract_conversation_and_account(event)[0]
|
||||
changed_attributes = extract_changed_attributes(event)
|
||||
inbox = conversation.inbox
|
||||
event_name = __method__.to_s
|
||||
payload = conversation.webhook_data.merge(event: event_name)
|
||||
payload = conversation.webhook_data.merge(event: event_name, changed_attributes: changed_attributes)
|
||||
agent_bots_for(inbox, conversation).each { |agent_bot| process_webhook_bot_event(agent_bot, payload) }
|
||||
end
|
||||
|
||||
|
||||
@@ -19,6 +19,6 @@ class InboxCapacityLimit < ApplicationRecord
|
||||
belongs_to :agent_capacity_policy
|
||||
belongs_to :inbox
|
||||
|
||||
validates :conversation_limit, presence: true, numericality: { greater_than: 0, only_integer: true }
|
||||
validates :conversation_limit, presence: true, numericality: { greater_than_or_equal_to: 0, only_integer: true }
|
||||
validates :inbox_id, uniqueness: { scope: :agent_capacity_policy_id }
|
||||
end
|
||||
|
||||
@@ -9,10 +9,22 @@ RSpec.describe InboxCapacityLimit, type: :model do
|
||||
subject { create(:inbox_capacity_limit, agent_capacity_policy: policy, inbox: inbox) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:conversation_limit) }
|
||||
it { is_expected.to validate_numericality_of(:conversation_limit).is_greater_than(0).only_integer }
|
||||
it { is_expected.to validate_numericality_of(:conversation_limit).is_greater_than_or_equal_to(0).only_integer }
|
||||
it { is_expected.to validate_uniqueness_of(:inbox_id).scoped_to(:agent_capacity_policy_id) }
|
||||
end
|
||||
|
||||
describe 'zero conversation limit (exclusion policy)' do
|
||||
it 'allows conversation_limit of 0' do
|
||||
limit = build(:inbox_capacity_limit, agent_capacity_policy: policy, inbox: inbox, conversation_limit: 0)
|
||||
expect(limit).to be_valid
|
||||
end
|
||||
|
||||
it 'rejects negative conversation_limit' do
|
||||
limit = build(:inbox_capacity_limit, agent_capacity_policy: policy, inbox: inbox, conversation_limit: -1)
|
||||
expect(limit).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe 'uniqueness constraint' do
|
||||
it 'prevents duplicate inbox limits for the same policy' do
|
||||
create(:inbox_capacity_limit, agent_capacity_policy: policy, inbox: inbox)
|
||||
|
||||
@@ -86,6 +86,55 @@ RSpec.describe Enterprise::AutoAssignment::CapacityService, type: :service do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'exclusion policy (zero conversation limit)' do
|
||||
let(:excluded_agent) { create(:user, account: account, role: :agent, availability: :online) }
|
||||
let(:exclusion_policy) { create(:agent_capacity_policy, account: account, name: 'Exclusion Policy') }
|
||||
|
||||
before do
|
||||
create(:inbox_capacity_limit,
|
||||
agent_capacity_policy: exclusion_policy,
|
||||
inbox: inbox,
|
||||
conversation_limit: 0)
|
||||
|
||||
excluded_agent.account_users.find_by(account: account)
|
||||
.update!(agent_capacity_policy: exclusion_policy)
|
||||
|
||||
create(:inbox_member, inbox: inbox, user: excluded_agent)
|
||||
|
||||
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({
|
||||
excluded_agent.id.to_s => 'online',
|
||||
agent_with_capacity.id.to_s => 'online',
|
||||
agent_without_capacity.id.to_s => 'online',
|
||||
agent_at_capacity.id.to_s => 'online'
|
||||
})
|
||||
end
|
||||
|
||||
it 'always denies capacity for agents with zero limit' do
|
||||
capacity_service = described_class.new
|
||||
expect(capacity_service.agent_has_capacity?(excluded_agent, inbox)).to be false
|
||||
end
|
||||
|
||||
it 'denies capacity even when agent has no existing conversations' do
|
||||
capacity_service = described_class.new
|
||||
# Agent has 0 open conversations but limit is 0, so 0 < 0 is false
|
||||
expect(excluded_agent.assigned_conversations.where(inbox: inbox, status: :open).count).to eq(0)
|
||||
expect(capacity_service.agent_has_capacity?(excluded_agent, inbox)).to be false
|
||||
end
|
||||
|
||||
it 'excludes zero-limit agents from available agents list' do
|
||||
capacity_service = described_class.new
|
||||
online_agents = inbox.available_agents
|
||||
filtered_agents = online_agents.select do |inbox_member|
|
||||
capacity_service.agent_has_capacity?(inbox_member.user, inbox)
|
||||
end
|
||||
available_users = filtered_agents.map(&:user)
|
||||
|
||||
expect(available_users).not_to include(excluded_agent)
|
||||
expect(available_users).to include(agent_with_capacity)
|
||||
expect(available_users).to include(agent_without_capacity)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'assignment with capacity' do
|
||||
let(:service) { AutoAssignment::AssignmentService.new(inbox: inbox) }
|
||||
|
||||
|
||||
@@ -59,9 +59,10 @@ describe AgentBotListener do
|
||||
|
||||
describe '#conversation_updated' do
|
||||
let(:event_name) { 'conversation.updated' }
|
||||
let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) }
|
||||
|
||||
context 'when agent bot is not configured' do
|
||||
let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) }
|
||||
|
||||
it 'does not send webhook' do
|
||||
expect(AgentBots::WebhookJob).not_to receive(:perform_later)
|
||||
listener.conversation_updated(event)
|
||||
@@ -69,22 +70,34 @@ describe AgentBotListener do
|
||||
end
|
||||
|
||||
context 'when agent bot is configured on inbox' do
|
||||
it 'sends webhook to the inbox agent bot' do
|
||||
let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) }
|
||||
|
||||
it 'sends webhook to the inbox agent bot with changed_attributes' do
|
||||
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
||||
expect(AgentBots::WebhookJob).to receive(:perform_later).with(agent_bot.outgoing_url,
|
||||
conversation.webhook_data.merge(event: 'conversation_updated')).once
|
||||
conversation.webhook_data.merge(event: 'conversation_updated',
|
||||
changed_attributes: nil)).once
|
||||
listener.conversation_updated(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation is assigned to an agent bot' do
|
||||
let!(:event) do
|
||||
Events::Base.new(event_name, Time.zone.now, conversation: conversation,
|
||||
changed_attributes: { 'assignee_agent_bot_id' => [nil, agent_bot.id] })
|
||||
end
|
||||
|
||||
before do
|
||||
conversation.update!(assignee_agent_bot: agent_bot, assignee: nil)
|
||||
end
|
||||
|
||||
it 'sends webhook to the assigned agent bot' do
|
||||
it 'sends webhook with changed_attributes to the assigned agent bot' do
|
||||
expected_changed_attributes = [{ 'assignee_agent_bot_id' => { previous_value: nil, current_value: agent_bot.id } }]
|
||||
expect(AgentBots::WebhookJob).to receive(:perform_later).with(agent_bot.outgoing_url,
|
||||
conversation.webhook_data.merge(event: 'conversation_updated')).once
|
||||
conversation.webhook_data.merge(
|
||||
event: 'conversation_updated',
|
||||
changed_attributes: expected_changed_attributes
|
||||
)).once
|
||||
listener.conversation_updated(event)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user