Skip to content

Commit

Permalink
[FIX] Agents (with user status offline & omni-status as available) no…
Browse files Browse the repository at this point in the history
…t able to take or forward chat (#26575)
  • Loading branch information
murtaza98 authored and csuarez committed Aug 26, 2022
1 parent fc3fa21 commit d9ac20d
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 53 deletions.
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ Meteor.startup(function () {
type: 'boolean',
group: 'Omnichannel',
i18nLabel: 'Accept_new_livechats_when_agent_is_idle',
public: true,
enableQuery: omnichannelEnabledQuery,
});

Expand Down
15 changes: 0 additions & 15 deletions apps/meteor/app/livechat/server/lib/Helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,21 +548,6 @@ export const checkServiceStatus = ({ guest, agent }) => {
return users && users.count() > 0;
};

export const userCanTakeInquiry = (user) => {
check(
user,
Match.ObjectIncluding({
status: String,
statusLivechat: String,
roles: [String],
}),
);

const { roles, status, statusLivechat } = user;
// TODO: hasRole when the user has already been fetched from DB
return (status !== 'offline' && statusLivechat === 'available') || roles.includes('bot');
};

export const updateDepartmentAgents = (departmentId, agents, departmentEnabled) => {
check(departmentId, String);
check(
Expand Down
5 changes: 2 additions & 3 deletions apps/meteor/app/livechat/server/methods/takeInquiry.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../authorization';
import { Users, LivechatInquiry } from '../../../models/server';
import { RoutingManager } from '../lib/RoutingManager';
import { userCanTakeInquiry } from '../lib/Helper';

Meteor.methods({
'livechat:takeInquiry'(inquiryId, options) {
Expand All @@ -27,10 +26,10 @@ Meteor.methods({
});
}

const user = Users.findOneById(Meteor.userId(), {
const user = Users.findOneOnlineAgentById(Meteor.userId(), {
fields: { _id: 1, username: 1, roles: 1, status: 1, statusLivechat: 1 },
});
if (!userCanTakeInquiry(user)) {
if (!user) {
throw new Meteor.Error('error-agent-status-service-offline', 'Agent status is offline or Omnichannel service is not active', {
method: 'livechat:takeInquiry',
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { Field, Button, TextAreaInput, Modal, Box, PaginatedSelectFiltered } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import { useEndpoint, useSetting, useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';

Expand All @@ -23,6 +23,7 @@ const ForwardChatModal = ({
}): ReactElement => {
const t = useTranslation();
const getUserData = useEndpoint('GET', '/v1/users.info');
const idleAgentsAllowedForForwarding = useSetting('Livechat_enabled_when_agent_idle') as boolean;

const { getValues, handleSubmit, register, setFocus, setValue, watch } = useForm();

Expand All @@ -43,7 +44,26 @@ const ForwardChatModal = ({
const hasDepartments = useMemo(() => departments && departments.length > 0, [departments]);

const _id = { $ne: room.servedBy?._id };
const conditions = { _id, status: { $ne: 'offline' }, statusLivechat: 'available' };
const conditions = {
_id,
...(!idleAgentsAllowedForForwarding && {
$or: [
{
status: {
$exists: true,
$ne: 'offline',
},
roles: {
$ne: 'bot',
},
},
{
roles: 'bot',
},
],
}),
statusLivechat: 'available',
};

const endReached = useCallback(
(start) => {
Expand Down
90 changes: 90 additions & 0 deletions apps/meteor/tests/e2e/omnichannel-takeChat.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { faker } from '@faker-js/faker';
import type { Browser, Page } from '@playwright/test';

import { test, expect } from './utils/test';
import { OmnichannelLiveChat, HomeChannel } from './page-objects';

const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => {
const page = await browser.newPage({ storageState });
const poHomeChannel = new HomeChannel(page);
await page.goto('/');
await page.locator('.main-content').waitFor();

return { page, poHomeChannel };
};

test.describe('omnichannel-takeChat', () => {
let poLiveChat: OmnichannelLiveChat;
let newVisitor: { email: string; name: string };

let agent: { page: Page; poHomeChannel: HomeChannel };

test.beforeAll(async ({ api, browser }) => {
// make "user-1" an agent and manager
let statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status();
expect(statusCode).toBe(200);

// turn on manual selection routing
statusCode = (await api.post('/settings/Livechat_Routing_Method', { value: 'Manual_Selection' })).status();
expect(statusCode).toBe(200);

// turn off setting which allows offline agents to chat
statusCode = (await api.post('/settings/Livechat_enabled_when_agent_idle', { value: false })).status();
expect(statusCode).toBe(200);

agent = await createAuxContext(browser, 'user1-session.json');
});

test.beforeEach(async ({ page }) => {
// make "user-1" online
await agent.poHomeChannel.sidenav.switchStatus('online');

// start a new chat for each test
newVisitor = {
name: faker.name.firstName(),
email: faker.internet.email(),
};
poLiveChat = new OmnichannelLiveChat(page);
await page.goto('/livechat');
await poLiveChat.btnOpenLiveChat('R').click();
await poLiveChat.sendMessage(newVisitor, false);
await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_user');
await poLiveChat.btnSendMessageToOnlineAgent.click();
});

test.afterAll(async ({ api }) => {
// turn off manual selection routing
let statusCode = (await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' })).status();
expect(statusCode).toBe(200);

// turn on setting which allows offline agents to chat
statusCode = (await api.post('/settings/Livechat_enabled_when_agent_idle', { value: true })).status();
expect(statusCode).toBe(200);

// delete "user-1" from agents
statusCode = (await api.delete('/livechat/users/agent/user1')).status();
expect(statusCode).toBe(200);
});

test('expect "user1" to be able to take the chat from the queue', async () => {
await agent.poHomeChannel.sidenav.openQueuedOmnichannelChat(newVisitor.name);
await expect(agent.poHomeChannel.content.takeOmnichannelChatButton).toBeVisible();
await agent.poHomeChannel.content.takeOmnichannelChatButton.click();

await agent.poHomeChannel.sidenav.openChat(newVisitor.name);
await expect(agent.poHomeChannel.content.takeOmnichannelChatButton).not.toBeVisible();
await expect(agent.poHomeChannel.content.inputMessage).toBeVisible();
});

test('expect "user1" to not able able to take chat from queue in-case its user status is offline', async () => {
// make "user-1" offline
await agent.poHomeChannel.sidenav.switchStatus('offline');

await agent.poHomeChannel.sidenav.openQueuedOmnichannelChat(newVisitor.name);
await expect(agent.poHomeChannel.content.takeOmnichannelChatButton).toBeVisible();
await agent.poHomeChannel.content.takeOmnichannelChatButton.click();

// expect to see error message
await expect(agent.page.locator('text=Agent status is offline or Omnichannel service is not active')).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,79 @@ const createAuxContext = async (browser: Browser, storageState: string): Promise
return { page, poHomeChannel };
};

test.describe('omnichannel-departments', () => {
test.describe('omnichannel-transfer-to-another-agent', () => {
let poLiveChat: OmnichannelLiveChat;
let newUser: { email: string; name: string };
let newVisitor: { email: string; name: string };

let agent1: { page: Page; poHomeChannel: HomeChannel };
let agent2: { page: Page; poHomeChannel: HomeChannel };
test.beforeAll(async ({ api, browser }) => {
newUser = {
name: faker.name.firstName(),
email: faker.internet.email(),
};

// Set user user 1 as manager and agent
await api.post('/livechat/users/agent', { username: 'user1' });
await api.post('/livechat/users/manager', { username: 'user1' });
let statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status();
expect(statusCode).toBe(200);
statusCode = (await api.post('/livechat/users/agent', { username: 'user2' })).status();
expect(statusCode).toBe(200);
statusCode = (await api.post('/livechat/users/manager', { username: 'user1' })).status();
expect(statusCode).toBe(200);

// turn off setting which allows offline agents to chat
statusCode = (await api.post('/settings/Livechat_enabled_when_agent_idle', { value: false })).status();
expect(statusCode).toBe(200);

agent1 = await createAuxContext(browser, 'user1-session.json');
agent2 = await createAuxContext(browser, 'user2-session.json');
});
test.beforeEach(async ({ page }) => {
// make "user-1" online & "user-2" offline so that chat can be automatically routed to "user-1"
await agent1.poHomeChannel.sidenav.switchStatus('online');
await agent2.poHomeChannel.sidenav.switchStatus('offline');

// start a new chat for each test
newVisitor = {
name: faker.name.firstName(),
email: faker.internet.email(),
};
poLiveChat = new OmnichannelLiveChat(page);
await page.goto('/livechat');
await poLiveChat.btnOpenLiveChat('R').click();
await poLiveChat.sendMessage(newVisitor, false);
await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor');
await poLiveChat.btnSendMessageToOnlineAgent.click();
});

test.afterAll(async ({ api }) => {
await api.delete('/livechat/users/agent/user1');
await api.delete('/livechat/users/manager/user1');
// delete "user-1" & "user-2" from agents & managers
let statusCode = (await api.delete('/livechat/users/agent/user1')).status();
expect(statusCode).toBe(200);
statusCode = (await api.delete('/livechat/users/manager/user1')).status();
expect(statusCode).toBe(200);
statusCode = (await api.delete('/livechat/users/agent/user2')).status();
expect(statusCode).toBe(200);

await api.delete('/livechat/users/agent/user2');
// turn on setting which allows offline agents to chat
statusCode = (await api.post('/settings/Livechat_enabled_when_agent_idle', { value: true })).status();
expect(statusCode).toBe(200);
});

test('Receiving a message from visitor', async ({ browser, api, page }) => {
await test.step('Expect send a message as a visitor', async () => {
await page.goto('/livechat');
await poLiveChat.btnOpenLiveChat('R').click();
await poLiveChat.sendMessage(newUser, false);
await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor');
await poLiveChat.btnSendMessageToOnlineAgent.click();
// Set user user 2 as agent
});

test('transfer omnichannel chat to another agent', async () => {
await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => {
await agent1.poHomeChannel.sidenav.openChat(newUser.name);
await agent1.poHomeChannel.sidenav.openChat(newVisitor.name);
});

await test.step('Expect to connect as agent 2', async () => {
agent2 = await createAuxContext(browser, 'user2-session.json');
// TODO: We cannot assign a user as agent before, because now the agent can be assigned even offline, since we dont have endpoint to turn agent offline I'm doing this :x
await api.post('/livechat/users/agent', { username: 'user2' });
await test.step('Expect to not be able to transfer chat to "user-2" when that user is offline', async () => {
await agent2.poHomeChannel.sidenav.switchStatus('offline');

await agent1.poHomeChannel.content.btnForwardChat.click();
await agent1.poHomeChannel.content.inputModalAgentUserName.type('user2');
await expect(agent1.page.locator('text=Empty')).toBeVisible();

await agent1.page.goto('/');
});
await test.step('Expect to be able to transfer an omnichannel to conversation to agent 2 as agent 1', async () => {

await test.step('Expect to be able to transfer an omnichannel to conversation to agent 2 as agent 1 when agent 2 is online', async () => {
await agent2.poHomeChannel.sidenav.switchStatus('online');

await agent1.poHomeChannel.sidenav.openChat(newVisitor.name);
await agent1.poHomeChannel.content.btnForwardChat.click();
await agent1.poHomeChannel.content.inputModalAgentUserName.type('user2');
await agent1.page.locator('.rcx-option .rcx-option__wrapper >> text="user2"').click();
Expand All @@ -70,7 +95,7 @@ test.describe('omnichannel-departments', () => {
});

await test.step('Expect to have 1 omnichannel assigned to agent 2', async () => {
await agent2.poHomeChannel.sidenav.openChat(newUser.name);
await agent2.poHomeChannel.sidenav.openChat(newVisitor.name);
});
});
});
4 changes: 4 additions & 0 deletions apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,8 @@ export class HomeContent {
await this.page.locator('[data-qa-type="message"]').last().locator('[data-qa-type="message-action-menu"][data-qa-id="menu"]').waitFor();
await this.page.locator('[data-qa-type="message"]').last().locator('[data-qa-type="message-action-menu"][data-qa-id="menu"]').click();
}

get takeOmnichannelChatButton(): Locator {
return this.page.locator('button.rc-button >> text=Take it!');
}
}
10 changes: 10 additions & 0 deletions apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,22 @@ export class HomeSidenav {
await this.page.locator('//li[@class="rcx-option"]//div[contains(text(), "My Account")]').click();
}

async switchStatus(status: 'offline' | 'online'): Promise<void> {
await this.page.locator('[data-qa="sidebar-avatar-button"]').click();
await this.page.locator(`//li[@class="rcx-option"]//div[contains(text(), "${status}")]`).click();
}

async openChat(name: string): Promise<void> {
await this.page.locator('[data-qa="sidebar-search"]').click();
await this.page.locator('[data-qa="sidebar-search-input"]').type(name);
await this.page.locator('[data-qa="sidebar-item-title"]', { hasText: name }).first().click();
}

// Note: this is a workaround for now since queued omnichannel chats are not searchable yet so we can't use openChat() :(
async openQueuedOmnichannelChat(name: string): Promise<void> {
await this.page.locator('[data-qa="sidebar-item-title"]', { hasText: name }).first().click();
}

async createPublicChannel(name: string) {
await this.openNewByLabel('Channel');
await this.checkboxPrivateChannel.click();
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/tests/e2e/utils/apps/remove-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import { appsEndpoint } from './apps-data';
*/

export const removeAppById = async (api: BaseTest['api'], id: string) => {
await api.delete(appsEndpoint(`/${id}`), '');
await api.delete(appsEndpoint(`/${id}`));
};
6 changes: 3 additions & 3 deletions apps/meteor/tests/e2e/utils/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,16 @@ export const test = baseTest.extend<BaseTest>({

await use({
get(uri: string, prefix = API_PREFIX) {
return request.get(BASE_API_URL + prefix + uri, { headers });
return request.get(BASE_URL + prefix + uri, { headers });
},
post(uri: string, data: AnyObj, prefix = API_PREFIX) {
return request.post(BASE_URL + prefix + uri, { headers, data });
},
put(uri: string, data: AnyObj, prefix = API_PREFIX) {
return request.put(BASE_API_URL + prefix + uri, { headers, data });
return request.put(BASE_URL + prefix + uri, { headers, data });
},
delete(uri: string, prefix = API_PREFIX) {
return request.delete(BASE_API_URL + prefix + uri, { headers });
return request.delete(BASE_URL + prefix + uri, { headers });
},
});
},
Expand Down

0 comments on commit d9ac20d

Please sign in to comment.