diff --git a/.changeset/purple-boxes-shout.md b/.changeset/purple-boxes-shout.md new file mode 100644 index 0000000000000..9e15fbd28faaf --- /dev/null +++ b/.changeset/purple-boxes-shout.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/models': minor +'@rocket.chat/meteor': minor +--- + +Updates omnichannel routing so agents with `offline` status are always excluded from assignment. The `Livechat_enabled_when_agent_idle` setting now only affects agents with `away` status. diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-assign-room-tags.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-assign-room-tags.spec.ts index 1689b26bcd606..97b089f882c46 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-assign-room-tags.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-assign-room-tags.spec.ts @@ -53,16 +53,25 @@ test.describe('OC - Tags Visibility', () => { }); }); - test.beforeAll('Create conversations', async ({ api }) => { - conversations = await Promise.all([ - createConversation(api, { visitorName: visitorA.name, agentId: 'user1', departmentId: departmentA.data._id }), - createConversation(api, { visitorName: visitorB.name, agentId: 'user1', departmentId: departmentB.data._id }), - ]); - }); - - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, api }) => { poOmnichannel = new HomeOmnichannel(page); await page.goto('/'); + await poOmnichannel.waitForHome(); + + if (conversations.length === 0) { + conversations = await Promise.all([ + createConversation(api, { + visitorName: visitorA.name, + agentId: 'user1', + departmentId: departmentA.data._id, + }), + createConversation(api, { + visitorName: visitorB.name, + agentId: 'user1', + departmentId: departmentB.data._id, + }), + ]); + } }); test.afterAll(async ({ api }) => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-field-usage.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-field-usage.spec.ts index ff9e1ff384e74..2c537eae01aca 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-field-usage.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-field-usage.spec.ts @@ -34,7 +34,7 @@ test.describe.serial('OC - Custom fields usage, scope : room and visitor', () => test.beforeAll('Set up agent, manager and custom fields', async ({ api }) => { [agent, manager] = await Promise.all([createAgent(api, 'user1'), createManager(api, 'user1')]); - [roomCustomField, visitorCustomField, conversation] = await Promise.all([ + [roomCustomField, visitorCustomField] = await Promise.all([ createCustomField(api, { field: roomCustomFieldLabel, label: roomCustomFieldName, @@ -45,12 +45,19 @@ test.describe.serial('OC - Custom fields usage, scope : room and visitor', () => label: visitorCustomFieldName, scope: 'visitor', }), - createConversation(api, { - visitorName: visitor.name, - agentId: 'user1', - visitorToken, - }), ]); + }); + + test.beforeEach(async ({ page, api }) => { + poHomeChannel = new HomeOmnichannel(page); + await page.goto('/'); + await poHomeChannel.waitForHome(); + + conversation = await createConversation(api, { + visitorName: visitor.name, + agentId: 'user1', + visitorToken, + }); await setVisitorCustomFieldValue(api, { token: visitorToken, @@ -59,12 +66,6 @@ test.describe.serial('OC - Custom fields usage, scope : room and visitor', () => }); }); - test.beforeEach(async ({ page }) => { - poHomeChannel = new HomeOmnichannel(page); - await page.goto('/'); - await poHomeChannel.waitForHome(); - }); - test.afterAll('Remove agent, manager, custom fields and conversation', async () => { await Promise.all([agent.delete(), manager.delete(), roomCustomField.delete(), visitorCustomField.delete(), conversation.delete()]); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-agent-idle-setting.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-agent-idle-setting.spec.ts new file mode 100644 index 0000000000000..3f83ac989480e --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-agent-idle-setting.spec.ts @@ -0,0 +1,124 @@ +import type { Page } from '@playwright/test'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { createAuxContext } from '../fixtures/createAuxContext'; +import { Users } from '../fixtures/userStates'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; +import { setSettingValueById } from '../utils'; +import { createAgent } from '../utils/omnichannel/agents'; +import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments'; +import { test, expect } from '../utils/test'; + +test.use({ storageState: Users.user1.state }); + +test.describe('OC - Routing to Idle Agents', () => { + test.skip(!IS_EE, 'Enterprise Edition Only'); + + let poHomeOmnichannel: HomeOmnichannel; + let poLivechat: OmnichannelLiveChat; + + let livechatPage: Page; + + let agent: Awaited>; + let testDepartment: Awaited>; + let visitor: { name: string; email: string }; + + const routingMethods = ['Auto_Selection', 'Load_Balancing', 'Load_Rotation']; + + test.beforeAll(async ({ api }) => { + [agent, testDepartment] = await Promise.all([ + createAgent(api, 'user1'), + createDepartment(api, { name: 'Idle Routing Dept' }), + setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 300), + expect(await setSettingValueById(api, 'Omnichannel_enable_department_removal', true)).toBeOK(), + ]); + + await Promise.all([addAgentToDepartment(api, { department: testDepartment.data, agentId: 'user1' })]); + }); + + test.beforeEach(async ({ page, browser, api }) => { + visitor = createFakeVisitor(); + poHomeOmnichannel = new HomeOmnichannel(page); + await page.goto('/'); + await page.locator('#main-content').waitFor(); + + ({ page: livechatPage } = await createAuxContext(browser, Users.user1, '/livechat', false)); + poLivechat = new OmnichannelLiveChat(livechatPage, api); + }); + + test.afterEach(async ({ api }) => { + if (livechatPage) { + await livechatPage.context().close(); + } + + await setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 300); + }); + + test.afterAll(async ({ api }) => { + await Promise.all([ + testDepartment.delete(), + agent.delete(), + setSettingValueById(api, 'Livechat_Routing_Method', 'Auto_Selection'), + setSettingValueById(api, 'Livechat_enabled_when_agent_idle', false), + expect(await setSettingValueById(api, 'Omnichannel_enable_department_removal', false)).toBeOK(), + ]); + }); + + routingMethods.forEach((routingMethod) => { + test.describe(`Routing method: ${routingMethod}`, () => { + test(`should not route to idle agents`, async ({ api }) => { + await test.step(`Setup routing method to ${routingMethod} and ignore idle agents`, async () => { + await setSettingValueById(api, 'Livechat_Routing_Method', routingMethod); + await setSettingValueById(api, 'Livechat_enabled_when_agent_idle', false); + }); + + await test.step('Visitor tries to initiate a conversation with the away agent', async () => { + await poLivechat.page.reload(); + await poLivechat.openAnyLiveChat(); + await poLivechat.sendMessage(visitor, false); + await poLivechat.onlineAgentMessage.fill('Hello from visitor'); + + // Force Agent to become away by idle timeout + await setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 1); + await poHomeOmnichannel.page.reload(); + await expect(poHomeOmnichannel.navbar.getUserStatusBadge('away')).toBeVisible(); + + await poLivechat.btnSendMessageToOnlineAgent.click(); + }); + + await test.step('Verify visitor is not taken by agent', async () => { + await expect( + poLivechat.alertMessage('Error starting a new conversation: Sorry, no online agents [no-agent-online]'), + ).toBeVisible(); + }); + }); + + test(`should route to agents even if they are idle when setting is enabled`, async ({ api }) => { + await test.step(`Setup routing method to ${routingMethod} and allow idle agents`, async () => { + await setSettingValueById(api, 'Livechat_Routing_Method', routingMethod); + await setSettingValueById(api, 'Livechat_enabled_when_agent_idle', true); + }); + + await test.step('Force agent to become away by idle timeout', async () => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 1); + await poHomeOmnichannel.page.reload(); + await expect(poHomeOmnichannel.navbar.getUserStatusBadge('away')).toBeVisible(); + }); + + await test.step('Visitor initiates chat', async () => { + await poLivechat.page.reload(); + await poLivechat.openAnyLiveChat(); + await poLivechat.sendMessage(visitor, false); + await poLivechat.onlineAgentMessage.fill('test message'); + await poLivechat.btnSendMessageToOnlineAgent.click(); + }); + + await test.step('Verify chat is served to an agent', async () => { + await expect(poLivechat.headerTitle).toHaveText(agent.data.username); + }); + }); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts index 2822a9c691321..336a0cbc355aa 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts @@ -265,8 +265,10 @@ test.describe('OC - Livechat - Close chat using widget', () => { let poLiveChat: OmnichannelLiveChat; let agent: Awaited>; - test.beforeAll(async ({ api }) => { + test.beforeAll(async ({ api, browser }) => { agent = await createAgent(api, 'user1'); + + await createAuxContext(browser, Users.user1, '/', true); }); test.beforeEach(async ({ page, api }) => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-rooms-forward.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-rooms-forward.spec.ts new file mode 100644 index 0000000000000..f7540a9cc09b9 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-rooms-forward.spec.ts @@ -0,0 +1,271 @@ +import type { Page } from '@playwright/test'; +import type { IRoom } from '@rocket.chat/core-typings'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { createAuxContext } from '../fixtures/createAuxContext'; +import { Users } from '../fixtures/userStates'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; +import { setSettingValueById } from '../utils'; +import { createAgent } from '../utils/omnichannel/agents'; +import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments'; +import { createManager } from '../utils/omnichannel/managers'; +import { test, expect } from '../utils/test'; + +test.use({ storageState: Users.user1.state }); + +test.describe('OC - Forwarding to away agents (EE)', () => { + test.skip(!IS_EE, 'Enterprise Edition Only'); + + let poHomeOmnichannelOnlineAgent: HomeOmnichannel; + let poHomeOmnichannelAwayAgent: HomeOmnichannel; + let poLivechat: OmnichannelLiveChat; + + let livechatPage: Page; + let omnichannelPage: Page; + + let manager: Awaited>; + let onlineAgent: Awaited>; + let awayAgent: Awaited>; + let initialDepartment: Awaited>; + let forwardToOfflineDepartment: Awaited>; + let visitor: { name: string; email: string }; + + test.beforeAll(async ({ api }) => { + [manager, onlineAgent, awayAgent, initialDepartment, forwardToOfflineDepartment] = await Promise.all([ + createManager(api, 'user1'), + createAgent(api, 'user1'), + createAgent(api, 'user2'), + createDepartment(api, { name: 'Initial Dept' }), + createDepartment(api, { name: 'Forward Dept', allowReceiveForwardOffline: true }), + setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 300), + expect(await setSettingValueById(api, 'Omnichannel_enable_department_removal', true)).toBeOK(), + ]); + + await Promise.all([ + addAgentToDepartment(api, { department: initialDepartment.data, agentId: 'user1' }), + addAgentToDepartment(api, { department: forwardToOfflineDepartment.data, agentId: 'user2' }), + ]); + }); + + test.beforeEach(async ({ page, browser, api }) => { + visitor = createFakeVisitor(); + poHomeOmnichannelOnlineAgent = new HomeOmnichannel(page); + await page.goto('/'); + await page.locator('#main-content').waitFor(); + + ({ page: livechatPage } = await createAuxContext(browser, Users.user1, '/livechat', false)); + poLivechat = new OmnichannelLiveChat(livechatPage, api); + }); + + test.afterEach(async ({ api }) => { + if (livechatPage) { + await livechatPage.context().close(); + } + if (omnichannelPage) { + await omnichannelPage.context().close(); + } + + await setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 300); + }); + + test.afterAll(async ({ api }) => { + await Promise.all([ + initialDepartment.delete(), + forwardToOfflineDepartment.delete(), + manager.delete(), + onlineAgent.delete(), + awayAgent.delete(), + setSettingValueById(api, 'Livechat_Routing_Method', 'Auto_Selection'), + setSettingValueById(api, 'Livechat_enabled_when_agent_idle', false), + expect(await setSettingValueById(api, 'Omnichannel_enable_department_removal', false)).toBeOK(), + ]); + }); + + test('when manager forward to offline (agent away, accept when agent idle off) department the inquiry should be set to the queue', async ({ + api, + browser, + }) => { + await test.step('Setup routing settings', async () => { + await setSettingValueById(api, 'Livechat_Routing_Method', 'Manual_Selection'); + await setSettingValueById(api, 'Livechat_enabled_when_agent_idle', false); + }); + + await test.step('Visitor initiates chat', async () => { + await poLivechat.page.reload(); + await poLivechat.openAnyLiveChat(); + await poLivechat.sendMessage(visitor, false); + await poLivechat.onlineAgentMessage.fill('test'); + await poLivechat.btnSendMessageToOnlineAgent.click(); + }); + + await test.step('Set user2 agent away by idle timeout', async () => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 1); + ({ page: omnichannelPage } = await createAuxContext(browser, Users.user2, '/', false)); + poHomeOmnichannelAwayAgent = new HomeOmnichannel(omnichannelPage); + await expect(poHomeOmnichannelAwayAgent.navbar.getUserStatusBadge('away')).toBeVisible(); + }); + + await test.step('Manager forwards chat', async () => { + await poHomeOmnichannelOnlineAgent.sidebar.getSidebarItemByName(visitor.name).click(); + await poHomeOmnichannelOnlineAgent.quickActionsRoomToolbar.forwardChat(); + await poHomeOmnichannelOnlineAgent.content.forwardChatModal.selectDepartment('Forward Dept'); + await poHomeOmnichannelOnlineAgent.content.forwardChatModal.btnForward.click(); + await expect(poHomeOmnichannelOnlineAgent.content.forwardChatModal.btnForward).not.toBeVisible(); + }); + + await test.step('Check inquiry status via API is queued', async () => { + const roomInfoResp = await api.get(`/livechat/rooms`); + const roomBody = await roomInfoResp.json(); + const roomId = roomBody.rooms.find((room: IRoom) => room.fname === visitor.name)._id; + + const inquiryResp = await api.get(`/livechat/inquiries.getOne?roomId=${roomId}`); + const inquiryBody = await inquiryResp.json(); + + expect(inquiryBody.inquiry.status).toBe('queued'); + expect(inquiryBody.inquiry.department).toBe(forwardToOfflineDepartment.data._id); + }); + }); + + test('when manager forward to a department while waiting_queue is active and allowReceiveForwardOffline is true, chat should end in departments queue', async ({ + api, + browser, + }) => { + await test.step('Setup routing settings', async () => { + await setSettingValueById(api, 'Livechat_Routing_Method', 'Auto_Selection'); + }); + + await test.step('Visitor initiates chat', async () => { + await poLivechat.page.reload(); + await poLivechat.openAnyLiveChat(); + await poLivechat.sendMessage(visitor, false); + await poLivechat.onlineAgentMessage.fill('test'); + await poLivechat.btnSendMessageToOnlineAgent.click(); + }); + + await test.step('Set user2 agent away by idle timeout', async () => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 1); + ({ page: omnichannelPage } = await createAuxContext(browser, Users.user2, '/', false)); + poHomeOmnichannelAwayAgent = new HomeOmnichannel(omnichannelPage); + await expect(poHomeOmnichannelAwayAgent.navbar.getUserStatusBadge('away')).toBeVisible(); + }); + + await test.step('Manager enables queue and forwards chat', async () => { + await poHomeOmnichannelOnlineAgent.sidebar.getSidebarItemByName(visitor.name).click(); + await setSettingValueById(api, 'Livechat_waiting_queue', true); + + await poHomeOmnichannelOnlineAgent.quickActionsRoomToolbar.forwardChat(); + await poHomeOmnichannelOnlineAgent.content.forwardChatModal.selectDepartment('Forward Dept'); + await poHomeOmnichannelOnlineAgent.content.forwardChatModal.btnForward.click(); + await expect(poHomeOmnichannelOnlineAgent.content.forwardChatModal.btnForward).not.toBeVisible(); + }); + + await test.step('Check inquiry status via API is queued', async () => { + const roomInfoResp = await api.get(`/livechat/rooms`); + const roomBody = await roomInfoResp.json(); + const roomId = roomBody.rooms.find((room: IRoom) => room.fname === visitor.name)._id; + + const inquiryResp = await api.get(`/livechat/inquiries.getOne?roomId=${roomId}`); + const inquiryBody = await inquiryResp.json(); + + expect(inquiryBody.inquiry.status).toBe('queued'); + expect(inquiryBody.inquiry.department).toBe(forwardToOfflineDepartment.data._id); + }); + + await test.step('Disable waiting queue', async () => { + await setSettingValueById(api, 'Livechat_waiting_queue', false); + }); + }); + + test('when manager forward to a department while waiting_queue is active and allowReceiveForwardOffline is false, transfer should fail', async ({ + api, + browser, + }) => { + await test.step('Setup routing and department settings', async () => { + await setSettingValueById(api, 'Livechat_Routing_Method', 'Auto_Selection'); + await api.put(`/livechat/department/${forwardToOfflineDepartment.data._id}`, { + department: { ...forwardToOfflineDepartment.data, allowReceiveForwardOffline: false }, + }); + }); + + await test.step('Visitor initiates chat', async () => { + await poLivechat.page.reload(); + await poLivechat.openAnyLiveChat(); + await poLivechat.sendMessage(visitor, false); + await poLivechat.onlineAgentMessage.fill('test'); + await poLivechat.btnSendMessageToOnlineAgent.click(); + }); + + await test.step('Set user2 agent away by idle timeout', async () => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 1); + ({ page: omnichannelPage } = await createAuxContext(browser, Users.user2, '/', false)); + poHomeOmnichannelAwayAgent = new HomeOmnichannel(omnichannelPage); + await expect(poHomeOmnichannelAwayAgent.navbar.getUserStatusBadge('away')).toBeVisible(); + }); + + await test.step('Manager attempts to forward and sees error', async () => { + await poHomeOmnichannelOnlineAgent.sidebar.getSidebarItemByName(visitor.name).click(); + await setSettingValueById(api, 'Livechat_waiting_queue', true); + + await poHomeOmnichannelOnlineAgent.quickActionsRoomToolbar.forwardChat(); + await poHomeOmnichannelOnlineAgent.content.forwardChatModal.selectDepartment('Forward Dept'); + await poHomeOmnichannelOnlineAgent.content.forwardChatModal.btnForward.click(); + await expect(poHomeOmnichannelOnlineAgent.page.locator('role=alert')).toContainText( + 'No agents are available for service on this department.', + ); + }); + + await test.step('Restore department setting', async () => { + await api.put(`/livechat/department/${forwardToOfflineDepartment.data._id}`, { + department: { ...forwardToOfflineDepartment.data, allowReceiveForwardOffline: true }, + }); + }); + + await test.step('Disable waiting queue', async () => { + await setSettingValueById(api, 'Livechat_waiting_queue', false); + }); + }); + + test('when manager forward to online (agent away, accept when agent idle on) department the inquiry should not be set to the queue', async ({ + api, + browser, + }) => { + await test.step('Setup routing settings', async () => { + await setSettingValueById(api, 'Livechat_Routing_Method', 'Auto_Selection'); + await setSettingValueById(api, 'Livechat_enabled_when_agent_idle', true); + }); + + await test.step('Visitor initiates chat', async () => { + await poLivechat.page.reload(); + await poLivechat.openAnyLiveChat(); + await poLivechat.sendMessage(visitor, false); + await poLivechat.onlineAgentMessage.fill('test'); + await poLivechat.btnSendMessageToOnlineAgent.click(); + }); + + await test.step('Set user2 agent away by idle timeout', async () => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_idleTimeLimit', 1); + ({ page: omnichannelPage } = await createAuxContext(browser, Users.user2, '/', false)); + poHomeOmnichannelAwayAgent = new HomeOmnichannel(omnichannelPage); + await expect(poHomeOmnichannelAwayAgent.navbar.getUserStatusBadge('away')).toBeVisible(); + }); + + await test.step('Manager forwards chat successfully', async () => { + await poHomeOmnichannelOnlineAgent.sidebar.getSidebarItemByName(visitor.name).click(); + await poHomeOmnichannelOnlineAgent.quickActionsRoomToolbar.forwardChat(); + await poHomeOmnichannelOnlineAgent.content.forwardChatModal.selectDepartment('Forward Dept'); + await poHomeOmnichannelOnlineAgent.content.forwardChatModal.btnForward.click(); + await expect(poHomeOmnichannelOnlineAgent.content.forwardChatModal.btnForward).not.toBeVisible(); + }); + + await test.step('Check room routing via API serves to away agent', async () => { + const roomInfoResp = await api.get(`/livechat/rooms`); + const roomBody = await roomInfoResp.json(); + const room = roomBody.rooms.find((room: IRoom) => room.fname === visitor.name); + + expect(room.servedBy._id).toBe(awayAgent.data._id); + expect(room.departmentId).toBe(forwardToOfflineDepartment.data._id); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts index 7a3ad3a8c0e7c..5a6a653645298 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts @@ -268,4 +268,8 @@ export class Navbar { const newStatus = await this.btnSwitchOmnichannelStatus.getAttribute('title'); expect(newStatus).toBe(status === 'offline' ? StatusTitleMap.offline : StatusTitleMap.online); } + + getUserStatusBadge(status: 'online' | 'away' | 'busy' | 'offline'): Locator { + return this.btnUserMenu.locator(`svg[class*="${status}"]`); + } } diff --git a/apps/meteor/tests/e2e/utils/omnichannel/departments.ts b/apps/meteor/tests/e2e/utils/omnichannel/departments.ts index fb7d8cd14ec32..e268ff8a52ae9 100644 --- a/apps/meteor/tests/e2e/utils/omnichannel/departments.ts +++ b/apps/meteor/tests/e2e/utils/omnichannel/departments.ts @@ -18,6 +18,7 @@ type CreateDepartmentParams = { departmentsAllowedToForward?: string[]; fallbackForwardDepartment?: string; maxNumberSimultaneousChat?: number; + allowReceiveForwardOffline?: boolean; }; export const createDepartment = async ( @@ -37,6 +38,7 @@ export const createDepartment = async ( departmentsAllowedToForward = [], fallbackForwardDepartment = '', maxNumberSimultaneousChat, + allowReceiveForwardOffline, }: CreateDepartmentParams = {}, ) => { const response = await api.post('/livechat/department', { @@ -55,6 +57,7 @@ export const createDepartment = async ( departmentsAllowedToForward, fallbackForwardDepartment, maxNumberSimultaneousChat, + allowReceiveForwardOffline, }, }); 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 d606feac77a87..848e47c5546f7 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 @@ -25,7 +25,6 @@ import { createCustomField, deleteCustomField } from '../../../data/livechat/cus import type { OnlineAgent } from '../../../data/livechat/department'; import { createDepartmentWith2OnlineAgents, - createDepartmentWithAnAwayAgent, createDepartmentWithAnOfflineAgent, createDepartmentWithAnOnlineAgent, deleteDepartment, @@ -1541,57 +1540,6 @@ describe('LIVECHAT - rooms', () => { }, ); - (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), - closeOmnichannelRoom(newRoom._id), - deleteVisitor(newVisitor.token), - deleteUser(manager), - ]); - }, - ); - (IS_EE ? it : it.skip)( 'when manager forwards a chat that hasnt been assigned to a user to another department with no online agents, chat should end ready in department (not queued)', async () => { @@ -1670,82 +1618,6 @@ describe('LIVECHAT - rooms', () => { }, ); - (IS_EE ? it : it.skip)( - 'when manager forward to a department while waiting_queue is active and allowReceiveForwardOffline is true, chat should end in departments queue', - async () => { - await updateSetting('Livechat_Routing_Method', 'Auto_Selection'); - 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); - - // Waiting queue enabled after assignement but before transfer, otherwise, chat will fall on previous test case - await updateSetting('Livechat_waiting_queue', true); - await request.post(api('livechat/room.forward')).set(managerCredentials).send({ - roomId: newRoom._id, - departmentId: forwardToOfflineDepartment._id, - clientAction: true, - comment: 'test comment', - }); - - const inquiry = await fetchInquiry(newRoom._id); - - expect(inquiry.status).to.equal('queued'); - expect(inquiry.department).to.equal(forwardToOfflineDepartment._id); - - await Promise.all([ - deleteDepartment(initialDepartment._id), - deleteDepartment(forwardToOfflineDepartment._id), - closeOmnichannelRoom(newRoom._id), - deleteVisitor(newVisitor.token), - deleteUser(manager), - updateSetting('Livechat_waiting_queue', false), - ]); - }, - ); - - (IS_EE ? it : it.skip)( - 'when manager forward to a department while waiting_queue is active and allowReceiveForwardOffline is false, transfer should fail', - async () => { - await updateSetting('Livechat_Routing_Method', 'Auto_Selection'); - const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent(); - const { department: forwardToOfflineDepartment } = await createDepartmentWithAnAwayAgent({ allowReceiveForwardOffline: false }); - - 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); - - // Waiting queue enabled after assignement but before transfer, otherwise, chat will fall on previous test case - await updateSetting('Livechat_waiting_queue', true); - const res = await request.post(api('livechat/room.forward')).set(managerCredentials).send({ - roomId: newRoom._id, - departmentId: forwardToOfflineDepartment._id, - clientAction: true, - comment: 'test comment', - }); - - expect(res.status).to.equal(400); - expect(res.body).to.have.property('error', 'error-no-agents-available-for-service-on-department'); - - await Promise.all([ - deleteDepartment(initialDepartment._id), - deleteDepartment(forwardToOfflineDepartment._id), - updateSetting('Livechat_waiting_queue', false), - closeOmnichannelRoom(newRoom._id), - deleteVisitor(newVisitor.token), - deleteUser(manager), - ]); - }, - ); - (IS_EE ? it : it.skip)( 'when manager forward to a department while waiting_queue is disabled and allowReceiveForwardOffline is false, but department is online, transfer should succeed', async () => { @@ -1785,46 +1657,6 @@ describe('LIVECHAT - rooms', () => { }, ); - (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), - closeOmnichannelRoom(newRoom._id), - deleteVisitor(newVisitor.token), - deleteUser(manager), - updateSetting('Livechat_enabled_when_agent_idle', false), - ]); - }, - ); - (IS_EE ? it : it.skip)( 'when manager forward to a department while waiting_queue is enabled, but department is online, transfer should succeed but it should end queued on target', async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/24-routing.ts b/apps/meteor/tests/end-to-end/api/livechat/24-routing.ts index 3bc9c8d9f218a..3be747694cd8d 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/24-routing.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/24-routing.ts @@ -1,6 +1,5 @@ import { faker } from '@faker-js/faker'; import type { Credentials } from '@rocket.chat/api-client'; -import { UserStatus } from '@rocket.chat/core-typings'; import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -14,11 +13,10 @@ import { createLivechatRoom, getLivechatRoomInfo, makeAgentUnavailable, - switchLivechatStatus, } from '../../../data/livechat/rooms'; import { updateSetting } from '../../../data/permissions.helper'; import { password } from '../../../data/user'; -import { createUser, deleteUser, login, setUserActiveStatus, setUserAway, setUserStatus } from '../../../data/users.helper'; +import { createUser, deleteUser, login, setUserActiveStatus } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; (IS_EE ? describe : describe.skip)('Omnichannel - Routing', () => { @@ -347,39 +345,6 @@ import { IS_EE } from '../../../e2e/config/constants'; const roomInfo = await getLivechatRoomInfo(room._id); expect(roomInfo.servedBy).to.be.undefined; }); - it('should ignore offline users when Livechat_enabled_when_agent_idle is false', async () => { - await updateSetting('Livechat_enabled_when_agent_idle', false); - await setUserStatus(testUser.credentials, UserStatus.OFFLINE); - - const visitor = await createVisitor(testDepartment._id); - const room = await createLivechatRoom(visitor.token); - - const roomInfo = await getLivechatRoomInfo(room._id); - expect(roomInfo.servedBy).to.be.undefined; - }); - it('should not route to an idle user', async () => { - await setUserStatus(testUser.credentials, UserStatus.AWAY); - await setUserAway(testUser.credentials); - await setUserStatus(testUser3.credentials, UserStatus.AWAY); - await setUserAway(testUser3.credentials); - // Agent is available but should be ignored - await switchLivechatStatus('available', testUser.credentials); - - const visitor = await createVisitor(testDepartment._id); - const room = await createLivechatRoom(visitor.token); - - const roomInfo = await getLivechatRoomInfo(room._id); - expect(roomInfo.servedBy).to.be.undefined; - }); - it('should route to an idle user', async () => { - await updateSetting('Livechat_enabled_when_agent_idle', true); - - const visitor = await createVisitor(testDepartment._id); - const room = await createLivechatRoom(visitor.token); - - const roomInfo = await getLivechatRoomInfo(room._id); - expect(roomInfo.servedBy).to.be.an('object'); - }); it('should route to another available agent if contact manager is unavailable and Omnichannel_contact_manager_routing is enabled', async () => { await makeAgentAvailable(testUser.credentials); const visitor = await createVisitor(testDepartment._id, faker.person.fullName(), visitorEmail); @@ -509,33 +474,6 @@ import { IS_EE } from '../../../e2e/config/constants'; expect(roomInfo.servedBy).to.be.an('object'); expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id); }); - it('should not route to an idle user', async () => { - await updateSetting('Livechat_enabled_when_agent_idle', false); - await setUserStatus(testUser.credentials, UserStatus.AWAY); - await setUserAway(testUser.credentials); - await setUserStatus(testUser2.credentials, UserStatus.AWAY); - await setUserAway(testUser2.credentials); - // Agent is available but should be ignored - await switchLivechatStatus('available', testUser.credentials); - - const visitor = await createVisitor(testDepartment._id); - const room = await createLivechatRoom(visitor.token); - - const roomInfo = await getLivechatRoomInfo(room._id); - expect(roomInfo.servedBy).to.be.undefined; - }); - it('should route to agents even if theyre idle when setting is enabled', async () => { - await updateSetting('Livechat_enabled_when_agent_idle', true); - await setUserStatus(testUser.credentials, UserStatus.AWAY); - await setUserStatus(testUser2.credentials, UserStatus.AWAY); - - const visitor = await createVisitor(testDepartment._id); - const room = await createLivechatRoom(visitor.token); - - const roomInfo = await getLivechatRoomInfo(room._id); - // Not checking who, just checking it's served - expect(roomInfo.servedBy).to.be.an('object'); - }); }); describe('Load Rotation', () => { before(async () => { @@ -617,32 +555,5 @@ import { IS_EE } from '../../../e2e/config/constants'; expect(roomInfo.servedBy).to.be.an('object'); expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); }); - it('should not route to an idle user', async () => { - await updateSetting('Livechat_enabled_when_agent_idle', false); - await setUserStatus(testUser.credentials, UserStatus.AWAY); - await setUserAway(testUser.credentials); - await setUserStatus(testUser2.credentials, UserStatus.AWAY); - await setUserAway(testUser2.credentials); - // Agent is available but should be ignored - await switchLivechatStatus('available', testUser.credentials); - - const visitor = await createVisitor(testDepartment._id); - const room = await createLivechatRoom(visitor.token); - - const roomInfo = await getLivechatRoomInfo(room._id); - expect(roomInfo.servedBy).to.be.undefined; - }); - it('should route to agents even if theyre idle when setting is enabled', async () => { - await updateSetting('Livechat_enabled_when_agent_idle', true); - await setUserStatus(testUser.credentials, UserStatus.AWAY); - await setUserStatus(testUser2.credentials, UserStatus.AWAY); - - const visitor = await createVisitor(testDepartment._id); - const room = await createLivechatRoom(visitor.token); - - const roomInfo = await getLivechatRoomInfo(room._id); - // Not checking who, just checking it's served - expect(roomInfo.servedBy).to.be.an('object'); - }); }); }); diff --git a/packages/models/src/helpers/omnichannel/agentStatus.ts b/packages/models/src/helpers/omnichannel/agentStatus.ts index 020751a29d35a..235956871379b 100644 --- a/packages/models/src/helpers/omnichannel/agentStatus.ts +++ b/packages/models/src/helpers/omnichannel/agentStatus.ts @@ -2,27 +2,29 @@ import { UserStatus } from '@rocket.chat/core-typings'; import type { IUser } from '@rocket.chat/core-typings'; import type { Filter } from 'mongodb'; +/** + * Builds the query to find online and available agents for livechat auto-assignment. + * * According to product rules, the primary conditions for auto-assignment are: + * - User must have the 'livechat-agent' role. + * - Livechat service status (`statusLivechat`) must be 'available'. + * - User's global status must NOT be 'offline' (Bots are exempt from this rule). + * - If the "Accept new omnichannel requests when the agent is idle" (aka `Livechat_enabled_when_agent_idle`) setting is OFF, + * then the statusConnection must NOT be 'away'. + */ export const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle?: boolean): Filter => ({ statusLivechat: 'available', roles: 'livechat-agent', // ignore deactivated users active: true, - ...(!isLivechatEnabledWhenAgentIdle && { - $or: [ - { - status: { - $exists: true, - $ne: UserStatus.OFFLINE, - }, - roles: { - $ne: 'bot', - }, - }, - { - roles: 'bot', + $or: [ + { roles: 'bot' }, + { + status: { + $exists: true, + $ne: UserStatus.OFFLINE, }, - ], - }), + }, + ], ...extraFilters, ...(isLivechatEnabledWhenAgentIdle === false && { statusConnection: { $ne: 'away' },