Skip to content

Commit

Permalink
[NEW][ENTERPRISE] Omnichannel Last-Chatted Agent Preferred option (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
renatobecker authored May 21, 2020
1 parent 5a6a084 commit 9cde69f
Show file tree
Hide file tree
Showing 15 changed files with 163 additions and 13 deletions.
5 changes: 3 additions & 2 deletions app/livechat/server/lib/Livechat.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ export const Livechat = {
}

if (room == null) {
const defaultAgent = callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, guest);
// if no department selected verify if there is at least one active and pick the first
if (!agent && !guest.department) {
if (!defaultAgent && !guest.department) {
const department = this.getRequiredDepartment();

if (department) {
Expand All @@ -127,7 +128,7 @@ export const Livechat = {
}

// delegate room creation to QueueManager
room = await QueueManager.requestRoom({ guest, message, roomInfo, agent, extraData });
room = await QueueManager.requestRoom({ guest, message, roomInfo, agent: defaultAgent, extraData });
newRoom = true;
}

Expand Down
2 changes: 1 addition & 1 deletion app/livechat/server/lib/RoutingManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const RoutingManager = {
LivechatInquiry.takeInquiry(_id);
const inq = this.assignAgent(inquiry, agent);

callbacks.run('livechat.afterTakeInquiry', inq);
callbacks.runAsync('livechat.afterTakeInquiry', inq, agent);

return LivechatRooms.findOneById(rid);
},
Expand Down
16 changes: 14 additions & 2 deletions app/models/server/models/LivechatInquiry.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class LivechatInquiry extends Base {
_id: inquiryId,
}, {
$set: { status: 'taken' },
$unset: { defaultAgent: 1 },
});
}

Expand All @@ -62,11 +63,14 @@ export class LivechatInquiry extends Base {
/*
* mark inquiry as queued
*/
queueInquiry(inquiryId) {
queueInquiry(inquiryId, defaultAgent) {
return this.update({
_id: inquiryId,
}, {
$set: { status: 'queued' },
$set: {
status: 'queued',
...defaultAgent && { defaultAgent },
},
});
}

Expand Down Expand Up @@ -180,6 +184,14 @@ export class LivechatInquiry extends Base {
return collectionObj.aggregate(aggregate).toArray();
}

removeDefaultAgentById(inquiryId) {
return this.update({
_id: inquiryId,
}, {
$unset: { defaultAgent: 1 },
});
}

/*
* remove the inquiry by roomId
*/
Expand Down
14 changes: 14 additions & 0 deletions app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export class LivechatRooms extends Base {
this.tryEnsureIndex({ 'metrics.serviceTimeDuration': 1 }, { sparse: true });
this.tryEnsureIndex({ 'metrics.visitorInactivity': 1 }, { sparse: true });
this.tryEnsureIndex({ 'omnichannel.predictedVisitorAbandonmentAt': 1 }, { sparse: true });
this.tryEnsureIndex({ closedAt: 1 }, { sparse: true });
this.tryEnsureIndex({ servedBy: 1 }, { sparse: true });
}

findLivechat(filter = {}, offset = 0, limit = 20) {
Expand Down Expand Up @@ -164,6 +166,18 @@ export class LivechatRooms extends Base {
return this.findOne(query, options);
}

findOneLastServedAndClosedByVisitorToken(visitorToken, options = {}) {
const query = {
t: 'l',
'v.token': visitorToken,
closedAt: { $exists: true },
servedBy: { $exists: true },
};

options.sort = { closedAt: -1 };
return this.findOne(query, options);
}

findOneByVisitorToken(visitorToken, fields) {
const options = {};

Expand Down
14 changes: 14 additions & 0 deletions app/models/server/models/LivechatVisitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ export class LivechatVisitors extends Base {
return this.update(query, update);
}

updateLastAgentByToken(token, lastAgent) {
const query = {
token,
};

const update = {
$set: {
lastAgent,
},
};

return this.update(query, update);
}

/**
* Find a visitor by their phone number
* @return {object} User from db
Expand Down
3 changes: 0 additions & 3 deletions app/models/server/models/Rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ export class Rooms extends Base {
// discussions
this.tryEnsureIndex({ prid: 1 }, { sparse: true });
this.tryEnsureIndex({ fname: 1 }, { sparse: true });
// Livechat - statistics
this.tryEnsureIndex({ closedAt: 1 }, { sparse: true });

// field used for DMs only
this.tryEnsureIndex({ uids: 1 }, { sparse: true });
}
Expand Down
4 changes: 2 additions & 2 deletions app/models/server/models/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ export class Users extends Base {
return this.findOne(query);
}

findOneOnlineAgentByUsername(username) {
findOneOnlineAgentByUsername(username, options) {
const query = queryStatusAgentOnline({ username });

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

findOneOnlineAgentById(_id) {
Expand Down
4 changes: 2 additions & 2 deletions ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { settings } from '../../../../../app/settings';
import { LivechatInquiry } from '../../../../../app/models/server';
import { dispatchInquiryPosition, checkWaitingQueue } from '../lib/Helper';

callbacks.add('livechat.beforeRouteChat', async (inquiry) => {
callbacks.add('livechat.beforeRouteChat', async (inquiry, agent) => {
if (!settings.get('Livechat_waiting_queue')) {
return inquiry;
}
Expand All @@ -18,7 +18,7 @@ callbacks.add('livechat.beforeRouteChat', async (inquiry) => {
return inquiry;
}

LivechatInquiry.queueInquiry(_id);
LivechatInquiry.queueInquiry(_id, agent);

const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({ _id });
if (inq) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ callbacks.add('livechat.checkAgentBeforeTakeInquiry', async (agent, inquiry) =>

const { queueInfo: { chats = 0 } = {} } = user;
if (maxNumberSimultaneousChat <= chats) {
callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry, agent);

if (!RoutingManager.getConfig().autoAssignAgent) {
throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed');
}

return null;
}
return agent;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { callbacks } from '../../../../../app/callbacks/server';
import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager';
import { settings } from '../../../../../app/settings/server';
import { LivechatRooms, LivechatInquiry, LivechatVisitors, Users } from '../../../../../app/models/server';
import { checkWaitingQueue } from '../lib/Helper';

const normalizeDefaultAgent = (agent) => {
if (!agent) {
return;
}

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

const checkDefaultAgentOnNewRoom = (defaultAgent, defaultGuest) => {
if (defaultAgent || !defaultGuest) {
return defaultAgent;
}

if (!RoutingManager.getConfig().autoAssignAgent) {
return defaultAgent;
}

const { _id: guestId } = defaultGuest;
const guest = LivechatVisitors.findOneById(guestId, { fields: { lastAgent: 1, token: 1 } });
const { lastAgent: { username: usernameByVisitor } = {}, token } = guest;

const lastGuestAgent = usernameByVisitor && normalizeDefaultAgent(Users.findOneOnlineAgentByUsername(usernameByVisitor, { fields: { _id: 1, username: 1 } }));
if (lastGuestAgent) {
return lastGuestAgent;
}

const room = LivechatRooms.findOneLastServedAndClosedByVisitorToken(token, { fields: { servedBy: 1 } });
if (!room || !room.servedBy) {
return defaultAgent;
}

const { servedBy: { username: usernameByRoom } } = room;
const lastRoomAgent = normalizeDefaultAgent(Users.findOneOnlineAgentByUsername(usernameByRoom, { fields: { _id: 1, username: 1 } }));
return lastRoomAgent || defaultAgent;
};

const onMaxNumberSimultaneousChatsReached = (inquiry, agent) => {
if (!inquiry || !inquiry.defaultAgent) {
return inquiry;
}

if (!RoutingManager.getConfig().autoAssignAgent) {
return inquiry;
}

const { _id, defaultAgent, department } = inquiry;

LivechatInquiry.removeDefaultAgentById(_id);

const { _id: defaultAgentId } = defaultAgent;
const { agentId } = agent;

if (defaultAgentId === agentId) {
checkWaitingQueue(department);
}

return LivechatInquiry.findOneById(_id);
};

const afterTakeInquiry = (inquiry, agent) => {
if (!inquiry || !agent) {
return inquiry;
}

if (!RoutingManager.getConfig().autoAssignAgent) {
return inquiry;
}

const { v: { token } = {} } = inquiry;
if (!token) {
return inquiry;
}

LivechatVisitors.updateLastAgentByToken(token, { ...agent, ts: new Date() });

return inquiry;
};
settings.get('Livechat_last_chatted_agent_routing', function(key, value) {
if (!value) {
callbacks.remove('livechat.checkDefaultAgentOnNewRoom', 'livechat-check-default-agent-new-room');
callbacks.remove('livechat.onMaxNumberSimultaneousChatsReached', 'livechat-on-max-number-simultaneous-chats-reached');
callbacks.remove('livechat.afterTakeInquiry', 'livechat-save-default-agent-after-take-inquiry');
return;
}

callbacks.add('livechat.checkDefaultAgentOnNewRoom', checkDefaultAgentOnNewRoom, callbacks.priority.MEDIUM, 'livechat-check-default-agent-new-room');
callbacks.add('livechat.onMaxNumberSimultaneousChatsReached', onMaxNumberSimultaneousChatsReached, callbacks.priority.MEDIUM, 'livechat-on-max-number-simultaneous-chats-reached');
callbacks.add('livechat.afterTakeInquiry', afterTakeInquiry, callbacks.priority.MEDIUM, 'livechat-save-default-agent-after-take-inquiry');
});
1 change: 1 addition & 0 deletions ee/app/livechat-enterprise/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import './hooks/beforeNewInquiry';
import './hooks/beforeNewRoom';
import './hooks/beforeRoutingChat';
import './hooks/checkAgentBeforeTakeInquiry';
import './hooks/handleLastChattedAgentPreferredEvents';
import './hooks/onCheckRoomParamsApi';
import './hooks/onLoadConfigApi';
import './hooks/onSetUserStatusLivechat';
Expand Down
3 changes: 2 additions & 1 deletion ee/app/livechat-enterprise/server/lib/Helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ const processWaitingQueue = async (department) => {
return;
}

const room = await RoutingManager.delegateInquiry(inquiry);
const { defaultAgent } = inquiry;
const room = await RoutingManager.delegateInquiry(inquiry, defaultAgent);

const propagateAgentDelegated = Meteor.bindEnvironment((rid, agentId) => {
dispatchAgentDelegated(rid, agentId);
Expand Down
7 changes: 7 additions & 0 deletions ee/app/livechat-enterprise/server/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,12 @@ export const createSettings = () => {
enableQuery: { _id: 'Livechat_auto_close_abandoned_rooms', value: true },
});

settings.add('Livechat_last_chatted_agent_routing', false, {
type: 'boolean',
group: 'Omnichannel',
section: 'Routing',
enableQuery: { _id: 'Livechat_Routing_Method', value: { $ne: 'Manual_Selection' } },
});

Settings.addOptionValueById('Livechat_Routing_Method', { key: 'Load_Balancing', i18nLabel: 'Load_Balancing' });
};
2 changes: 2 additions & 0 deletions ee/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"List_of_departments_for_forward": "List of departments allowed for forwarding (Optional)",
"List_of_departments_for_forward_description": "Allow to set a restricted list of departments that can receive chats from this department",
"Livechat_abandoned_rooms_closed_custom_message": "Custom message when room is automatically closed by visitor inactivity",
"Livechat_last_chatted_agent_routing": "Last-Chatted Agent Preferred",
"Livechat_last_chatted_agent_routing_Description": "The Last-Chatted Agent setting allocates chats to the agent who previously interacted with the same visitor if the agent is available when the chat starts.",
"Livechat_Monitors": "Monitors",
"Livechat_monitors": "Livechat monitors",
"Max_number_of_chats_per_agent": "Max. number of simultaneous chats",
Expand Down
2 changes: 2 additions & 0 deletions ee/i18n/pt-BR.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"List_of_departments_for_forward": "Lista de departamentos permitidos para o encaminhamento(Opcional).",
"List_of_departments_for_forward_description": "Permite definir uma lista restrita de departamentos que podem receber conversas desse departamento.",
"Livechat_abandoned_rooms_closed_custom_message": "Mensagem customizada para usar quando a sala for automaticamente fechada por abandono do visitante",
"Livechat_last_chatted_agent_routing": "Agente preferido pela última conversa",
"Livechat_last_chatted_agent_routing_Description": "Agente preferido pela última conversa aloca bate-papos para o agente que interagiu anteriormente com o mesmo visitante, caso o agente esteja disponível quando o bate-papo for iniciado.",
"Livechat_Monitors": "Monitores",
"Livechat_monitors": "Monitores de Livechat",
"Max_number_of_chats_per_agent": "Número máximo de atendimentos simultâneos",
Expand Down

0 comments on commit 9cde69f

Please sign in to comment.