Skip to content

Commit

Permalink
Merge branch 'develop' into move-roles-to-func
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored Dec 23, 2024
2 parents 09582e1 + b4ce579 commit d114990
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 30 deletions.
6 changes: 6 additions & 0 deletions .changeset/popular-cameras-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
---

Fixes livechat conversations not being assigned to the contact manager even when the "Assign new conversations to the contact manager" setting is enabled
7 changes: 6 additions & 1 deletion apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,12 @@ class LivechatClass {
throw new Error('error-contact-channel-blocked');
}

const defaultAgent = await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, visitor);
const defaultAgent =
agent ??
(await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, {
visitorId: visitor._id,
source: roomInfo.source,
}));
// if no department selected verify if there is at least one active and pick the first
if (!defaultAgent && !visitor.department) {
const department = await getRequiredDepartment();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import type { IUser, SelectedAgent } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
import { LivechatVisitors, LivechatContacts, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';

import { notifyOnLivechatInquiryChanged } from '../../../../../app/lib/server/lib/notifyListener';
import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager';
import { migrateVisitorIfMissingContact } from '../../../../../app/livechat/server/lib/contacts/migrateVisitorIfMissingContact';
import { settings } from '../../../../../app/settings/server';
import { callbacks } from '../../../../../lib/callbacks';

let contactManagerPreferred = false;
let lastChattedAgentPreferred = false;

const normalizeDefaultAgent = (agent?: Pick<IUser, '_id' | 'username'> | null): SelectedAgent | null => {
const normalizeDefaultAgent = (agent?: Pick<IUser, '_id' | 'username'> | null): SelectedAgent | undefined => {
if (!agent) {
return null;
return undefined;
}

const { _id: agentId, username } = agent;
return { agentId, username };
};

const getDefaultAgent = async (username?: string): Promise<SelectedAgent | null> => {
if (!username) {
return null;
const getDefaultAgent = async ({ username, id }: { username?: string; id?: string }): Promise<SelectedAgent | undefined> => {
if (!username && !id) {
return undefined;
}

return normalizeDefaultAgent(await Users.findOneOnlineAgentByUserList(username, { projection: { _id: 1, username: 1 } }));
if (id) {
return normalizeDefaultAgent(await Users.findOneOnlineAgentById(id, undefined, { projection: { _id: 1, username: 1 } }));
}
return normalizeDefaultAgent(await Users.findOneOnlineAgentByUserList(username || [], { projection: { _id: 1, username: 1 } }));
};

settings.watch<boolean>('Livechat_last_chatted_agent_routing', (value) => {
Expand Down Expand Up @@ -88,30 +92,32 @@ settings.watch<boolean>('Omnichannel_contact_manager_routing', (value) => {

callbacks.add(
'livechat.checkDefaultAgentOnNewRoom',
async (defaultAgent, defaultGuest) => {
if (defaultAgent || !defaultGuest) {
async (defaultAgent, { visitorId, source } = {}) => {
if (defaultAgent || !visitorId || !source) {
return defaultAgent;
}

const { _id: guestId } = defaultGuest;
const guest = await LivechatVisitors.findOneEnabledById(guestId, {
const guest = await LivechatVisitors.findOneEnabledById(visitorId, {
projection: { lastAgent: 1, token: 1, contactManager: 1 },
});
if (!guest) {
return defaultAgent;
return undefined;
}

const { lastAgent, token, contactManager } = guest;
const guestManager = contactManager?.username && contactManagerPreferred && getDefaultAgent(contactManager?.username);
const contactId = await migrateVisitorIfMissingContact(visitorId, source);
const contact = contactId ? await LivechatContacts.findOneById(contactId, { projection: { contactManager: 1 } }) : undefined;

const guestManager = contactManagerPreferred && (await getDefaultAgent({ id: contact?.contactManager }));
if (guestManager) {
return guestManager;
}

if (!lastChattedAgentPreferred) {
return defaultAgent;
return undefined;
}

const guestAgent = lastAgent?.username && getDefaultAgent(lastAgent?.username);
const { lastAgent, token } = guest;
const guestAgent = await getDefaultAgent({ username: lastAgent?.username });
if (guestAgent) {
return guestAgent;
}
Expand All @@ -120,19 +126,19 @@ callbacks.add(
projection: { servedBy: 1 },
});
if (!room?.servedBy) {
return defaultAgent;
return undefined;
}

const {
servedBy: { username: usernameByRoom },
} = room;
if (!usernameByRoom) {
return defaultAgent;
return undefined;
}
const lastRoomAgent = normalizeDefaultAgent(
await Users.findOneOnlineAgentByUserList(usernameByRoom, { projection: { _id: 1, username: 1 } }),
);
return lastRoomAgent ?? defaultAgent;
return lastRoomAgent;
},
callbacks.priority.MEDIUM,
'livechat-check-default-agent-new-room',
Expand Down
6 changes: 5 additions & 1 deletion apps/meteor/lib/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
IOmnichannelRoomInfo,
IOmnichannelInquiryExtraData,
IOmnichannelRoomExtraData,
IOmnichannelSource,
} from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import type { FilterOperators } from 'mongodb';
Expand Down Expand Up @@ -118,7 +119,10 @@ type ChainedCallbackSignatures = {
) => Promise<T>;

'livechat.beforeRouteChat': (inquiry: ILivechatInquiryRecord, agent?: { agentId: string; username: string }) => ILivechatInquiryRecord;
'livechat.checkDefaultAgentOnNewRoom': (agent: SelectedAgent, visitor?: ILivechatVisitor) => SelectedAgent | null;
'livechat.checkDefaultAgentOnNewRoom': (
defaultAgent?: SelectedAgent,
params?: { visitorId?: string; source?: IOmnichannelSource },
) => SelectedAgent | undefined;

'livechat.onLoadForwardDepartmentRestrictions': (params: { departmentId: string }) => Record<string, unknown>;

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/models/raw/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -1657,11 +1657,11 @@ export class UsersRaw extends BaseRaw {
return this.findOne(query);
}

findOneOnlineAgentById(_id, isLivechatEnabledWhenAgentIdle) {
findOneOnlineAgentById(_id, isLivechatEnabledWhenAgentIdle, options) {
// TODO: Create class Agent
const query = queryStatusAgentOnline({ _id }, isLivechatEnabledWhenAgentIdle);

return this.findOne(query);
return this.findOne(query, options);
}

findAgents() {
Expand Down
75 changes: 69 additions & 6 deletions apps/meteor/tests/end-to-end/api/livechat/24-routing.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { faker } from '@faker-js/faker';
import type { Credentials } from '@rocket.chat/api-client';
import { UserStatus, type ILivechatDepartment, type IUser } from '@rocket.chat/core-typings';
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';

import { getCredentials, request, api } from '../../../data/api-data';
import { getCredentials, request, api, credentials } from '../../../data/api-data';
import {
createAgent,
makeAgentAvailable,
Expand Down Expand Up @@ -33,7 +35,9 @@ import { IS_EE } from '../../../e2e/config/constants';

let testUser: { user: IUser; credentials: Credentials };
let testUser2: { user: IUser; credentials: Credentials };
let testUser3: { user: IUser; credentials: Credentials };
let testDepartment: ILivechatDepartment;
let visitorEmail: string;

before(async () => {
const user = await createUser();
Expand All @@ -60,14 +64,43 @@ import { IS_EE } from '../../../e2e/config/constants';
});

before(async () => {
testDepartment = await createDepartment([{ agentId: testUser.user._id }]);
const user = await createUser();
await createAgent(user.username);
const credentials3 = await login(user.username, password);
await makeAgentAvailable(credentials3);

testUser3 = {
user,
credentials: credentials3,
};
});

after(async () => {
await deleteUser(testUser.user);
await deleteUser(testUser2.user);
before(async () => {
testDepartment = await createDepartment([{ agentId: testUser.user._id }, { agentId: testUser3.user._id }]);
await updateSetting('Livechat_assign_new_conversation_to_bot', true);

const visitorName = faker.person.fullName();
visitorEmail = faker.internet.email().toLowerCase();
await request
.post(api('omnichannel/contacts'))
.set(credentials)
.send({
name: visitorName,
emails: [visitorEmail],
phones: [],
contactManager: testUser3.user._id,
});
});

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

it('should route a room to an available agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
Expand All @@ -91,9 +124,24 @@ import { IS_EE } from '../../../e2e/config/constants';
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.not.be.equal(testUser2.user._id);
});
(IS_EE ? it : it.skip)(
'should route to contact manager if it is online and Livechat_assign_new_conversation_to_bot is enabled',
async () => {
const visitor = await createVisitor(testDepartment._id, faker.person.fullName(), visitorEmail);
const room = await createLivechatRoom(visitor.token);

await sleep(5000);

const roomInfo = await getLivechatRoomInfo(room._id);

expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser3.user._id);
},
);
it('should fail to start a conversation if there is noone available and Livechat_accept_chats_with_no_agents is false', async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', false);
await makeAgentUnavailable(testUser.credentials);
await makeAgentUnavailable(testUser3.credentials);

const visitor = await createVisitor(testDepartment._id);
const { body } = await request.get(api('livechat/room')).query({ token: visitor.token }).expect(400);
Expand Down Expand Up @@ -147,6 +195,21 @@ import { IS_EE } from '../../../e2e/config/constants';
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.undefined;
});
(IS_EE ? it : it.skip)(
'should route to another available agent if contact manager is unavailable and Livechat_assign_new_conversation_to_bot is enabled',
async () => {
await makeAgentAvailable(testUser.credentials);
const visitor = await createVisitor(testDepartment._id, faker.person.fullName(), visitorEmail);
const room = await createLivechatRoom(visitor.token);

await sleep(5000);

const roomInfo = await getLivechatRoomInfo(room._id);

expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id);
},
);
});
describe('Load Balancing', () => {
before(async () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/model-typings/src/models/IUsersModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,11 @@ export interface IUsersModel extends IBaseModel<IUser> {
findOnlineAgents(agentId?: string): FindCursor<ILivechatAgent>;
countOnlineAgents(agentId: string): Promise<number>;
findOneBotAgent(): Promise<ILivechatAgent | null>;
findOneOnlineAgentById(agentId: string, isLivechatEnabledWhenAgentIdle?: boolean): Promise<ILivechatAgent | null>;
findOneOnlineAgentById(
agentId: string,
isLivechatEnabledWhenAgentIdle?: boolean,
options?: FindOptions<IUser>,
): Promise<ILivechatAgent | null>;
findAgents(): FindCursor<ILivechatAgent>;
countAgents(): Promise<number>;
getNextAgent(ignoreAgentId?: string, extraQuery?: Filter<IUser>): Promise<{ agentId: string; username: string } | null>;
Expand Down

0 comments on commit d114990

Please sign in to comment.