Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions .changeset/cool-coins-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
"@rocket.chat/models": patch
---

Fixes a bug that caused routing algorithms to ignore the `Livechat_enabled_when_agent_idle` setting, effectively ignoring idle users from being assigned to inquiries.
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ class LivechatClass {

async checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise<boolean> {
if (agent?.agentId) {
return Users.checkOnlineAgents(agent.agentId);
return Users.checkOnlineAgents(agent.agentId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}

if (department) {
Expand All @@ -452,7 +452,7 @@ class LivechatClass {
return this.checkOnlineAgents(dep?.fallbackForwardDepartment);
}

return Users.checkOnlineAgents();
return Users.checkOnlineAgents(undefined, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}

async removeRoom(rid: string) {
Expand Down
6 changes: 4 additions & 2 deletions apps/meteor/app/livechat/server/lib/getOnlineAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import type { ILivechatAgent, SelectedAgent } from '@rocket.chat/core-typings';
import { Users, LivechatDepartmentAgents } from '@rocket.chat/models';
import type { FindCursor } from 'mongodb';

import { settings } from '../../../settings/server';

export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise<FindCursor<ILivechatAgent> | undefined> {
if (agent?.agentId) {
return Users.findOnlineAgents(agent.agentId);
return Users.findOnlineAgents(agent.agentId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}

if (department) {
Expand All @@ -20,5 +22,5 @@ export async function getOnlineAgents(department?: string, agent?: SelectedAgent

return Users.findByIds<ILivechatAgent>([...new Set(agentIds)]);
}
return Users.findOnlineAgents();
return Users.findOnlineAgents(undefined, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AutoSelection implements IRoutingMethod {
);
}

return Users.getNextAgent(ignoreAgentId, extraQuery);
return Users.getNextAgent(ignoreAgentId, extraQuery, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Users } from '@rocket.chat/models';

import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager';
import { settings } from '../../../../../../app/settings/server';
import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig';

/* Load Balancing Queuing method:
Expand Down Expand Up @@ -28,7 +29,11 @@ class LoadBalancing {
}

async getNextAgent(department?: string, ignoreAgentId?: string) {
const nextAgent = await Users.getNextLeastBusyAgent(department, ignoreAgentId);
const nextAgent = await Users.getNextLeastBusyAgent(
department,
ignoreAgentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
);
if (!nextAgent) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IOmnichannelCustomAgent } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';

import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager';
import { settings } from '../../../../../../app/settings/server';
import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig';

/* Load Rotation Queuing method:
Expand All @@ -27,7 +28,11 @@ class LoadRotation {
}

public async getNextAgent(department?: string, ignoreAgentId?: string): Promise<IOmnichannelCustomAgent | undefined> {
const nextAgent = await Users.getLastAvailableAgentRouted(department, ignoreAgentId);
const nextAgent = await Users.getLastAvailableAgentRouted(
department,
ignoreAgentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
);
if (!nextAgent) {
return;
}
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/tests/data/livechat/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@ export const createManager = (overrideUsername?: string): Promise<ILivechatAgent
});
});

export const switchLivechatStatus = async (status: 'available' | 'not-available', overrideCredentials?: Credentials): Promise<void> => {
await request
.post(api('livechat/agent.status'))
.set(overrideCredentials || credentials)
.send({ status })
.expect(200);
};

export const makeAgentAvailable = async (overrideCredentials?: Credentials): Promise<Response> => {
await restorePermissionToRoles('view-l-room');
await request
Expand Down
28 changes: 27 additions & 1 deletion apps/meteor/tests/data/users.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';

import { api, credentials, request } from './api-data';
import { api, credentials, methodCall, request } from './api-data';
import { password } from './user';

export type TestUser<TUser extends IUser> = TUser & { username: string; emails: string[] };
Expand Down Expand Up @@ -100,3 +100,29 @@ export const setUserStatus = (overrideCredentials = credentials, status = UserSt
message: '',
status,
});

export const setUserAway = (overrideCredentials = credentials) =>
request
.post(methodCall('UserPresence:away'))
.set(overrideCredentials)
.send({
message: JSON.stringify({
method: 'UserPresence:away',
params: [],
id: 'id',
msg: 'method',
}),
});

export const setUserOnline = (overrideCredentials = credentials) =>
request
.post(methodCall('UserPresence:online'))
.set(overrideCredentials)
.send({
message: JSON.stringify({
method: 'UserPresence:online',
params: [],
id: 'id',
msg: 'method',
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ test.describe('OC - Livechat API', () => {
await addAgentToDepartment(api, { department: departmentA, agentId: agent.data._id });
await addAgentToDepartment(api, { department: departmentB, agentId: agent2.data._id });
expect((await api.post('/settings/Livechat_offline_email', { value: 'test@testing.com' })).status()).toBe(200);
await api.post('/settings/Livechat_enabled_when_agent_idle', { value: false });
});

test.beforeEach(async ({ browser }, testInfo) => {
Expand Down Expand Up @@ -266,6 +267,7 @@ test.describe('OC - Livechat API', () => {
await expect((await api.post('/settings/Omnichannel_enable_department_removal', { value: true })).status()).toBe(200);
await Promise.all([...departments.map((department) => department.delete())]);
await expect((await api.post('/settings/Omnichannel_enable_department_removal', { value: false })).status()).toBe(200);
await api.post('/settings/Livechat_enabled_when_agent_idle', { value: true });
});

// clearBusinessUnit
Expand Down Expand Up @@ -698,6 +700,7 @@ test.describe('OC - Livechat API', () => {
test.beforeAll(async ({ api }) => {
agent = await createAgent(api, 'user1');
expect((await api.post('/settings/Livechat_offline_email', { value: 'test@testing.com' })).status()).toBe(200);
await api.post('/settings/Livechat_enabled_when_agent_idle', { value: false });
});

test.beforeEach(async ({ browser }, testInfo) => {
Expand All @@ -723,8 +726,9 @@ test.describe('OC - Livechat API', () => {
await page.close();
});

test.afterAll(async () => {
test.afterAll(async ({ api }) => {
await agent.delete();
await api.post('/settings/Livechat_enabled_when_agent_idle', { value: true });
});

test('OC - Livechat API - onChatMaximized & onChatMinimized', async () => {
Expand Down Expand Up @@ -856,6 +860,7 @@ test.describe('OC - Livechat API', () => {
const newVisitor = createFakeVisitor();

await poAuxContext.poHomeOmnichannel.sidenav.switchStatus('offline');
await poAuxContext.poHomeOmnichannel.sidenav.switchOmnichannelStatus('offline');

const watchForTrigger = page.waitForFunction(() => window.onOfflineFormSubmit === true);

Expand Down
115 changes: 113 additions & 2 deletions apps/meteor/tests/end-to-end/api/livechat/24-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import {
createLivechatRoom,
getLivechatRoomInfo,
makeAgentUnavailable,
switchLivechatStatus,
} from '../../../data/livechat/rooms';
import { sleep } from '../../../data/livechat/utils';
import { updateSetting } from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, login, setUserActiveStatus, setUserStatus } from '../../../data/users.helper';
import { createUser, deleteUser, login, setUserActiveStatus, setUserAway, setUserStatus } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';

(IS_EE ? describe : describe.skip)('Omnichannel - Routing', () => {
Expand Down Expand Up @@ -271,7 +272,14 @@ import { IS_EE } from '../../../e2e/config/constants';
});
});

after(async () => Promise.all([deleteUser(testUser.user), deleteUser(testUser2.user), deleteUser(testUser3.user)]));
after(async () =>
Promise.all([
deleteUser(testUser.user),
deleteUser(testUser2.user),
deleteUser(testUser3.user),
updateSetting('Livechat_enabled_when_agent_idle', true),
]),
);

it('should route a room to an available agent', async () => {
const visitor = await createVisitor(testDepartment._id);
Expand Down Expand Up @@ -364,6 +372,29 @@ import { IS_EE } from '../../../e2e/config/constants';
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');
});
Comment thread
KevLehman marked this conversation as resolved.
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);
Expand Down Expand Up @@ -405,6 +436,30 @@ import { IS_EE } from '../../../e2e/config/constants';
});
});

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');
});

Comment thread
KevLehman marked this conversation as resolved.
Outdated
it('should not route to contact manager if it is online but setting is off', async () => {
const visitor = await createVisitor(testDepartment2._id, faker.person.fullName(), visitorEmail);
const room = await createLivechatRoom(visitor.token);
Expand Down Expand Up @@ -505,6 +560,35 @@ 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);

await sleep(5000);

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 () => {
Expand Down Expand Up @@ -594,5 +678,32 @@ 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');
});
});
});
13 changes: 9 additions & 4 deletions packages/model-typings/src/models/IUsersModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ export interface IUsersModel extends IBaseModel<IUser> {
getNextLeastBusyAgent(
department: any,
ignoreAgentId: any,
isEnabledWhenAgentIdle?: boolean,
): Promise<{ agentId: string; username: string; lastRoutingTime: Date; departments: any[]; count: number }>;
getLastAvailableAgentRouted(
department: any,
ignoreAgentId: any,
isEnabledWhenAgentIdle?: boolean,
): Promise<{ agentId: string; username: string; lastRoutingTime: Date; departments: any[] }>;

setLastRoutingTime(userId: any): Promise<number>;
Expand Down Expand Up @@ -263,9 +265,8 @@ export interface IUsersModel extends IBaseModel<IUser> {
}): Promise<UpdateResult>;
findPersonalAccessTokenByTokenNameAndUserId(data: { userId: string; tokenName: string }): Promise<IPersonalAccessToken | null>;
setOperator(userId: string, operator: boolean): Promise<UpdateResult>;
checkOnlineAgents(agentId?: string): Promise<boolean>;
findOnlineAgents(agentId?: string): FindCursor<ILivechatAgent>;
countOnlineAgents(agentId: string): Promise<number>;
checkOnlineAgents(agentId?: string, isLivechatEnabledWhenIdle?: boolean): Promise<boolean>;
findOnlineAgents(agentId?: string, isLivechatEnabledWhenIdle?: boolean): FindCursor<ILivechatAgent>;
findOneBotAgent(): Promise<ILivechatAgent | null>;
findOneOnlineAgentById(
agentId: string,
Expand All @@ -274,7 +275,11 @@ export interface IUsersModel extends IBaseModel<IUser> {
): Promise<ILivechatAgent | null>;
findAgents(): FindCursor<ILivechatAgent>;
countAgents(): Promise<number>;
getNextAgent(ignoreAgentId?: string, extraQuery?: Filter<IUser>): Promise<{ agentId: string; username: string } | null>;
getNextAgent(
ignoreAgentId?: string,
extraQuery?: Filter<IUser>,
enabledWhenAgentIdle?: boolean,
): Promise<{ agentId: string; username: string } | null>;
getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username: string } | null>;
setLivechatStatus(userId: string, status: ILivechatAgentStatus): Promise<UpdateResult>;
makeAgentUnavailableAndUnsetExtension(userId: string): Promise<UpdateResult>;
Expand Down
Loading