diff --git a/.changeset/cuddly-bugs-relax.md b/.changeset/cuddly-bugs-relax.md new file mode 100644 index 0000000000000..dd05eac2388b9 --- /dev/null +++ b/.changeset/cuddly-bugs-relax.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes the usage of `Livechat_enabled_when_agent_idle` setting across the codebase. Goal is to use it wherever is applicable making the feature more predictable. diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 330d37eae47b0..9fe6ba3c56d41 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -480,7 +480,7 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T if (!agentId) { throw new Error('error-invalid-agent'); } - const user = await Users.findOneOnlineAgentById(agentId); + const user = await Users.findOneOnlineAgentById(agentId, settings.get('Livechat_enabled_when_agent_idle')); if (!user) { logger.debug(`Agent ${agentId} is offline. Cannot forward`); throw new Error('error-user-is-offline'); @@ -603,7 +603,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi const { userId: agentId, clientAction } = transferData; if (agentId) { logger.debug(`Forwarding room ${room._id} to department ${departmentId} (to user ${agentId})`); - const user = await Users.findOneOnlineAgentById(agentId); + const user = await Users.findOneOnlineAgentById(agentId, settings.get('Livechat_enabled_when_agent_idle')); if (!user) { throw new Error('error-user-is-offline'); } diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 4149fb4fd4b8f..4a2422b2143fb 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -422,10 +422,14 @@ export class QueueManager { }; let defaultAgent: SelectedAgent | undefined; - if (servedBy?.username && (await Users.findOneOnlineAgentByUserList(servedBy.username))) { + const isAgentAvailable = (username: string) => + Users.findOneOnlineAgentByUserList(username, { projection: { _id: 1 } }, settings.get('Livechat_enabled_when_agent_idle')); + + if (servedBy?.username && (await isAgentAvailable(servedBy.username))) { defaultAgent = { agentId: servedBy._id, username: servedBy.username }; } + // TODO: unarchive to return updated room await LivechatRooms.unarchiveOneById(rid); const room = await LivechatRooms.findOneById(rid); if (!room) { diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 8ff8a76a1b67a..6d422819a88aa 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -97,7 +97,12 @@ export const RoutingManager: Routing = { async delegateInquiry(inquiry, agent, options = {}, room) { const { department, rid } = inquiry; logger.debug(`Attempting to delegate inquiry ${inquiry._id}`); - if (!agent || (agent.username && !(await Users.findOneOnlineAgentByUserList(agent.username)) && !(await allowAgentSkipQueue(agent)))) { + if ( + !agent || + (agent.username && + !(await Users.findOneOnlineAgentByUserList(agent.username, {}, settings.get('Livechat_enabled_when_agent_idle'))) && + !(await allowAgentSkipQueue(agent))) + ) { logger.debug(`Agent offline or invalid. Using routing method to get next agent for inquiry ${inquiry._id}`); agent = await this.getNextAgent(department); logger.debug(`Routing method returned agent ${agent?.agentId} for inquiry ${inquiry._id}`); diff --git a/apps/meteor/app/livechat/server/lib/routing/External.ts b/apps/meteor/app/livechat/server/lib/routing/External.ts index b9f9e99e84686..9bd3965f7326f 100644 --- a/apps/meteor/app/livechat/server/lib/routing/External.ts +++ b/apps/meteor/app/livechat/server/lib/routing/External.ts @@ -55,7 +55,11 @@ class ExternalQueue implements IRoutingMethod { const result = (await request.json()) as { username?: string }; if (result?.username) { - const agent = await Users.findOneOnlineAgentByUserList(result.username); + const agent = await Users.findOneOnlineAgentByUserList( + result.username, + {}, + settings.get('Livechat_enabled_when_agent_idle'), + ); if (!agent?.username) { return; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts index 75b6b3687a05f..89c3d8c452bcb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts @@ -23,7 +23,9 @@ const getDefaultAgent = async ({ username, id }: { username?: string; id?: strin } if (id) { - const agent = await Users.findOneOnlineAgentById(id, undefined, { projection: { _id: 1, username: 1 } }); + const agent = await Users.findOneOnlineAgentById(id, settings.get('Livechat_enabled_when_agent_idle'), { + projection: { _id: 1, username: 1 }, + }); if (agent) { return normalizeDefaultAgent(agent); } @@ -36,7 +38,13 @@ const getDefaultAgent = async ({ username, id }: { username?: string; id?: strin return undefined; } - return normalizeDefaultAgent(await Users.findOneOnlineAgentByUserList(username || [], { projection: { _id: 1, username: 1 } })); + return normalizeDefaultAgent( + await Users.findOneOnlineAgentByUserList( + username || [], + { projection: { _id: 1, username: 1 } }, + settings.get('Livechat_enabled_when_agent_idle'), + ), + ); }; settings.watch('Livechat_last_chatted_agent_routing', (value) => { @@ -119,7 +127,11 @@ checkDefaultAgentOnNewRoom.patch(async (_next, defaultAgent, { visitorId, source return defaultAgent; } const lastRoomAgent = normalizeDefaultAgent( - await Users.findOneOnlineAgentByUserList(usernameByRoom, { projection: { _id: 1, username: 1 } }), + await Users.findOneOnlineAgentByUserList( + usernameByRoom, + { projection: { _id: 1, username: 1 } }, + settings.get('Livechat_enabled_when_agent_idle'), + ), ); return lastRoomAgent; }); diff --git a/apps/meteor/tests/data/livechat/department.ts b/apps/meteor/tests/data/livechat/department.ts index 5e3402df051d6..97d49db0f14f6 100644 --- a/apps/meteor/tests/data/livechat/department.ts +++ b/apps/meteor/tests/data/livechat/department.ts @@ -4,7 +4,7 @@ import type { ILivechatDepartment, IUser, LivechatDepartmentDTO } from '@rocket. import { expect } from 'chai'; import { api, credentials, methodCall, request } from '../api-data'; -import { createAnOnlineAgent, createAnOfflineAgent } from './users'; +import { createAnOnlineAgent, createAnOfflineAgent, createAnAwayAgent } from './users'; import type { WithRequiredProperty } from './utils'; const NewDepartmentData = ((): Partial => ({ @@ -183,6 +183,40 @@ export const createDepartmentWithAnOfflineAgent = async ({ }; }; +export const createDepartmentWithAnAwayAgent = async ({ + allowReceiveForwardOffline = false, + fallbackForwardDepartment, + departmentsAllowedToForward, +}: { + allowReceiveForwardOffline?: boolean; + fallbackForwardDepartment?: string; + departmentsAllowedToForward?: string[]; +}): Promise<{ + department: ILivechatDepartment; + agent: { + credentials: Credentials; + user: WithRequiredProperty; + }; +}> => { + const { user, credentials } = await createAnAwayAgent(); + + const department = (await createDepartmentWithMethod({ + allowReceiveForwardOffline, + fallbackForwardDepartment, + departmentsAllowedToForward, + })) as ILivechatDepartment; + + await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true); + + return { + department, + agent: { + credentials, + user, + }, + }; +}; + export const archiveDepartment = async (departmentId: string): Promise => { await request .post(api(`livechat/department/${departmentId}/archive`)) diff --git a/apps/meteor/tests/data/livechat/users.ts b/apps/meteor/tests/data/livechat/users.ts index b4298ff962c80..e6cce02f4c513 100644 --- a/apps/meteor/tests/data/livechat/users.ts +++ b/apps/meteor/tests/data/livechat/users.ts @@ -1,10 +1,10 @@ import { faker } from '@faker-js/faker'; import type { Credentials } from '@rocket.chat/api-client'; -import type { ILivechatAgent, IUser } from '@rocket.chat/core-typings'; +import { UserStatus, type ILivechatAgent, type IUser } from '@rocket.chat/core-typings'; import { api, credentials, request, methodCall } from '../api-data'; import { password } from '../user'; -import { createUser, login } from '../users.helper'; +import { createUser, login, setUserAway, setUserStatus } from '../users.helper'; import { createAgent, makeAgentAvailable, makeAgentUnavailable } from './rooms'; export const createBotAgent = async (): Promise<{ @@ -77,6 +77,26 @@ export const createAnOfflineAgent = async (): Promise<{ }; }; +export const createAnAwayAgent = async (): Promise<{ + credentials: Credentials; + user: IUser & { username: string }; +}> => { + const username = `user.test.${Date.now()}.away`; + const email = `${username}.offline@rocket.chat`; + const { body } = await request.post(api('users.create')).set(credentials).send({ email, name: username, username, password }); + const agent = body.user; + const createdUserCredentials = await login(agent.username, password); + await createAgent(agent.username); + await makeAgentAvailable(createdUserCredentials); + await setUserStatus(createdUserCredentials, UserStatus.AWAY); + await setUserAway(createdUserCredentials); + + return { + credentials: createdUserCredentials, + user: agent, + }; +}; + export const updateLivechatSettingsForUser = async ( agentId: string, livechatSettings: Record, diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index c577bd4aa34f1..1a49ab5bf33be 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -22,7 +22,12 @@ import type { SuccessResult } from '../../../../app/api/server/definition'; import { getCredentials, api, request, credentials, methodCall } from '../../../data/api-data'; import { apps, APP_URL } from '../../../data/apps/apps-data'; import { createCustomField } from '../../../data/livechat/custom-fields'; -import { createDepartmentWithAnOfflineAgent, createDepartmentWithAnOnlineAgent, deleteDepartment } from '../../../data/livechat/department'; +import { + createDepartmentWithAnAwayAgent, + createDepartmentWithAnOfflineAgent, + createDepartmentWithAnOnlineAgent, + deleteDepartment, +} from '../../../data/livechat/department'; import { createSLA, getRandomPriority } from '../../../data/livechat/priorities'; import { createVisitor, @@ -1198,6 +1203,88 @@ describe('LIVECHAT - rooms', () => { await deleteDepartment(forwardToOfflineDepartment._id); }); + (IS_EE ? it : it.skip)( + 'when manager forward to offline (agent away, accept when agent idle off) department the inquiry should be set to the queue', + async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + await updateSetting('Livechat_enabled_when_agent_idle', false); + const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent(); + const { department: forwardToOfflineDepartment } = await createDepartmentWithAnAwayAgent({ + allowReceiveForwardOffline: true, + }); + + const newVisitor = await createVisitor(initialDepartment._id); + const newRoom = await createLivechatRoom(newVisitor.token); + + const manager = await createUser(); + const managerCredentials = await login(manager.username, password); + await createManager(manager.username); + + await request.post(api('livechat/room.forward')).set(managerCredentials).send({ + roomId: newRoom._id, + departmentId: forwardToOfflineDepartment._id, + clientAction: true, + comment: 'test comment', + }); + + await request + .get(api(`livechat/queue`)) + .set(credentials) + .query({ + count: 1, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.queue).to.be.an('array'); + expect(res.body.queue[0].chats).not.to.undefined; + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }); + + await Promise.all([deleteDepartment(initialDepartment._id), deleteDepartment(forwardToOfflineDepartment._id)]); + }, + ); + + (IS_EE ? it : it.skip)( + 'when manager forward to online (agent away, accept when agent idle on) department the inquiry should not be set to the queue', + async () => { + await updateSetting('Livechat_Routing_Method', 'Auto_Selection'); + await updateSetting('Livechat_enabled_when_agent_idle', true); + const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent(); + const { department: forwardToOfflineDepartment, agent } = await createDepartmentWithAnAwayAgent({ + allowReceiveForwardOffline: true, + }); + + const newVisitor = await createVisitor(initialDepartment._id); + const newRoom = await createLivechatRoom(newVisitor.token); + + const manager = await createUser(); + const managerCredentials = await login(manager.username, password); + await createManager(manager.username); + + await request.post(api('livechat/room.forward')).set(managerCredentials).send({ + roomId: newRoom._id, + departmentId: forwardToOfflineDepartment._id, + clientAction: true, + comment: 'test comment', + }); + + const roomInfo = await getLivechatRoomInfo(newRoom._id); + + expect(roomInfo.servedBy).to.have.property('_id', agent.user._id); + expect(roomInfo.departmentId).to.be.equal(forwardToOfflineDepartment._id); + + await Promise.all([ + deleteDepartment(initialDepartment._id), + deleteDepartment(forwardToOfflineDepartment._id), + updateSetting('Livechat_enabled_when_agent_idle', false), + ]); + }, + ); + (IS_EE ? it : it.skip)( 'should update inquiry last message when manager forward to offline department and the inquiry returns to queued', async () => {