Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/four-clocks-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
"@rocket.chat/models": patch
---

Fixes the behavior of "Maximum number of simultaneous chats" settings, making them more predictable. Previously, we applied a single limit per operation, being the order: `Department > Agent > Global`. This caused the department limit to take prescedence over agent's specific limit, causing some unwanted side effects.

The new way of applying the filter is as follows:
- An agent can accept chats from multiple departments, respecting each department’s limit individually.
- The total number of active chats (across all departments) must not exceed the configured Agent-Level or Global limit.
- If neither the Agent-Level nor Global Limit is set, only department-specific limits apply.
- If no limits are set at any level, there is no restriction on the number of chats an agent can handle.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class AutoSelection implements IRoutingMethod {
}

async getNextAgent(department?: string, ignoreAgentId?: string): Promise<SelectedAgent | null | undefined> {
// TODO: apply this extra query to other routing algorithms
const extraQuery = await callbacks.run('livechat.applySimultaneousChatRestrictions', undefined, {
...(department ? { departmentId: department } : {}),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment } from '@rocket.chat/models';
import type { Document } from 'mongodb';

import { settings } from '../../../../../app/settings/server';
import { callbacks } from '../../../../../lib/callbacks';

callbacks.add(
'livechat.applySimultaneousChatRestrictions',
async (_: any, { departmentId }: { departmentId?: string } = {}) => {
const limitFilter: Document[] = [];

if (departmentId) {
const departmentLimit =
(
Expand All @@ -15,39 +18,22 @@ callbacks.add(
})
)?.maxNumberSimultaneousChat || 0;
if (departmentLimit > 0) {
return { $match: { 'queueInfo.chats': { $gte: Number(departmentLimit) } } };
limitFilter.push({ 'queueInfo.chatsForDepartment': { $gte: Number(departmentLimit) } });
}
}

const maxChatsPerSetting = settings.get('Livechat_maximum_chats_per_agent') as number;
const agentFilter = {
$and: [
{ 'livechat.maxNumberSimultaneousChat': { $gt: 0 } },
{ $expr: { $gte: ['queueInfo.chats', 'livechat.maxNumberSimultaneousChat'] } },
],
};
// apply filter only if agent setting is 0 or is disabled
const globalFilter =
maxChatsPerSetting > 0
? {
$and: [
{
$or: [
{
'livechat.maxNumberSimultaneousChat': { $exists: false },
},
{ 'livechat.maxNumberSimultaneousChat': 0 },
{ 'livechat.maxNumberSimultaneousChat': '' },
{ 'livechat.maxNumberSimultaneousChat': null },
],
},
{ 'queueInfo.chats': { $gte: maxChatsPerSetting } },
],
}
: // dummy filter meaning: don't match anything
{ _id: '' };
limitFilter.push({
$and: [{ maxChatsForAgent: { $gt: 0 } }, { $expr: { $gte: ['$queueInfo.chats', '$maxChatsForAgent'] } }],
});

const maxChatsPerSetting = settings.get<number>('Livechat_maximum_chats_per_agent');
if (maxChatsPerSetting > 0) {
limitFilter.push({
$and: [{ maxChatsForAgent: { $eq: 0 } }, { 'queueInfo.chats': { $gte: maxChatsPerSetting } }],
});
}

return { $match: { $or: [agentFilter, globalFilter] } };
return { $match: { $or: limitFilter } };
},
callbacks.priority.HIGH,
'livechat-apply-simultaneous-restrictions',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import type { IUser, IRoom } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { settings } from '../../../../../app/settings/server';
import { callbacks } from '../../../../../lib/callbacks';
import { getMaxNumberSimultaneousChat } from '../lib/Helper';
import { isAgentWithinChatLimits } from '../lib/Helper';

callbacks.add(
'beforeJoinRoom',
Expand All @@ -17,28 +16,18 @@ callbacks.add(
if (!room || !isOmnichannelRoom(room)) {
return user;
}

const { departmentId } = room;
const maxNumberSimultaneousChat = await getMaxNumberSimultaneousChat({
agentId: user._id,
departmentId,
});

if (maxNumberSimultaneousChat === 0) {
return user;
}

const userSubs = await Users.getAgentAndAmountOngoingChats(user._id);
const userSubs = await Users.getAgentAndAmountOngoingChats(user._id, departmentId);
if (!userSubs) {
return user;
}
const { queueInfo: { chats = 0, chatsForDepartment = 0 } = {} } = userSubs;

const { queueInfo: { chats = 0 } = {} } = userSubs;
if (maxNumberSimultaneousChat <= chats) {
throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed');
if (await isAgentWithinChatLimits({ agentId: user._id, departmentId, totalChats: chats, departmentChats: chatsForDepartment })) {
return user;
}

return user;
throw new Error('error-max-number-simultaneous-chats-reached');
},
callbacks.priority.MEDIUM,
'livechat-before-join-room',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { allowAgentSkipQueue } from '../../../../../app/livechat/server/lib/Help
import { checkOnlineAgents } from '../../../../../app/livechat/server/lib/service-status';
import { settings } from '../../../../../app/settings/server';
import { callbacks } from '../../../../../lib/callbacks';
import { getMaxNumberSimultaneousChat } from '../lib/Helper';
import { isAgentWithinChatLimits } from '../lib/Helper';
import { cbLogger } from '../lib/logger';

const validateMaxChats = async ({
Expand Down Expand Up @@ -48,34 +48,18 @@ const validateMaxChats = async ({
}

const { department: departmentId } = inquiry;

const maxNumberSimultaneousChat = await getMaxNumberSimultaneousChat({
agentId,
departmentId,
});

if (maxNumberSimultaneousChat === 0) {
cbLogger.debug(`Chat can be taken by Agent ${agentId}: max number simultaneous chats on range`);
return agent;
}

const user = await Users.getAgentAndAmountOngoingChats(agentId);
const user = await Users.getAgentAndAmountOngoingChats(agentId, departmentId);
if (!user) {
cbLogger.debug({ msg: 'No valid agent found', agentId });
throw new Error('No valid agent found');
}

const { queueInfo: { chats = 0 } = {} } = user;
const maxChats = typeof maxNumberSimultaneousChat === 'number' ? maxNumberSimultaneousChat : parseInt(maxNumberSimultaneousChat, 10);
const { queueInfo: { chats = 0, chatsForDepartment = 0 } = {} } = user;

cbLogger.debug({ msg: 'Validating agent is within max number of chats', agentId, user, maxChats });
if (maxChats <= chats) {
await callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry);
throw new Error('error-max-number-simultaneous-chats-reached');
if (await isAgentWithinChatLimits({ agentId, departmentId, totalChats: chats, departmentChats: chatsForDepartment })) {
return user;
}

cbLogger.debug(`Agent ${agentId} can take inquiry ${inquiry._id}`);
return agent;
throw new Error('error-max-number-simultaneous-chats-reached');
};

callbacks.add('livechat.checkAgentBeforeTakeInquiry', validateMaxChats, callbacks.priority.MEDIUM, 'livechat-before-take-inquiry');
125 changes: 112 additions & 13 deletions apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { api } from '@rocket.chat/core-services';
import type { IOmnichannelRoom, IOmnichannelServiceLevelAgreements, InquiryWithAgentInfo } from '@rocket.chat/core-typings';
import type {
ILivechatDepartment,
IOmnichannelRoom,
IOmnichannelServiceLevelAgreements,
InquiryWithAgentInfo,
} from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import {
Rooms as RoomRaw,
Expand Down Expand Up @@ -33,24 +38,118 @@ type QueueInfo = {
numberMostRecentChats: number;
};

export const getMaxNumberSimultaneousChat = async ({ agentId, departmentId }: { agentId?: string; departmentId?: string }) => {
if (departmentId) {
const department = await LivechatDepartmentRaw.findOneById(departmentId);
const { maxNumberSimultaneousChat = 0 } = department || { maxNumberSimultaneousChat: 0 };
if (maxNumberSimultaneousChat > 0) {
return Number(maxNumberSimultaneousChat);
export const isAgentWithinChatLimits = async ({
agentId,
departmentId,
totalChats,
departmentChats,
}: {
agentId: string;
departmentId?: string;
totalChats: number;
departmentChats: number;
}): Promise<boolean> => {
let agentLimit = 0;
let globalLimit = 0;

const user = await Users.getAgentInfo(agentId, settings.get('Livechat_show_agent_info'));
const rawAgentLimit = user?.livechat?.maxNumberSimultaneousChat;

if (rawAgentLimit !== undefined && rawAgentLimit !== null) {
const numericAgentLimit = Number(rawAgentLimit);
if (numericAgentLimit > 0) {
agentLimit = numericAgentLimit;
}
}

if (agentLimit === 0) {
const settingLimit = settings.get<number>('Livechat_maximum_chats_per_agent');
if (settingLimit > 0) {
globalLimit = settingLimit;
}
}

if (agentId) {
const user = await Users.getAgentInfo(agentId, settings.get('Livechat_show_agent_info'));
const { livechat: { maxNumberSimultaneousChat = 0 } = {} } = user || {};
if (maxNumberSimultaneousChat > 0) {
return Number(maxNumberSimultaneousChat);
if (departmentId) {
const department = await LivechatDepartmentRaw.findOneById<Pick<ILivechatDepartment, 'maxNumberSimultaneousChat'>>(departmentId, {
projection: { maxNumberSimultaneousChat: 1 },
});
let departmentLimit = 0;

if (department?.maxNumberSimultaneousChat !== undefined && department.maxNumberSimultaneousChat !== null) {
const numericDeptLimit = Number(department.maxNumberSimultaneousChat);
if (numericDeptLimit > 0) {
departmentLimit = numericDeptLimit;
}
}

if (departmentLimit > 0) {
if (agentLimit > 0) {
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['department', 'agent'],
totalChats,
departmentChats,
agentLimit,
departmentLimit,
});
return departmentChats < departmentLimit && totalChats < agentLimit;
}

if (globalLimit > 0) {
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['department', 'global'],
totalChats,
departmentChats,
globalLimit,
departmentLimit,
});
return departmentChats < departmentLimit && totalChats < globalLimit;
}

logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['department'],
totalChats,
departmentChats,
departmentLimit,
});
return departmentChats < departmentLimit;
}
}

if (agentLimit > 0) {
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['agent'],
totalChats,
agentLimit,
});
return totalChats < agentLimit;
}

if (globalLimit > 0) {
logger.debug({
msg: 'Applying chat limits',
departmentId,
agentId,
limits: ['global'],
totalChats,
globalLimit,
});
return totalChats < globalLimit;
}

return settings.get<number>('Livechat_maximum_chats_per_agent');
logger.debug({ msg: 'No applicable limit found for user', agentId });
return true;
};

const getWaitingQueueMessage = async (departmentId?: string) => {
Expand Down
Loading
Loading