fix: Newly added agent not following business hours (#29529)

Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com>
pull/29567/head^2
Murtaza Patrawala 3 years ago committed by GitHub
parent 2bdddc5615
commit c31f93ed96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .changeset/quick-coats-protect.md
  2. 7
      apps/meteor/app/lib/server/functions/saveUser.js
  3. 34
      apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts
  4. 32
      apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts
  5. 3
      apps/meteor/app/livechat/server/business-hour/Helper.ts
  6. 2
      apps/meteor/app/livechat/server/business-hour/Single.ts
  7. 50
      apps/meteor/app/livechat/server/hooks/afterUserActions.ts
  8. 2
      apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts
  9. 9
      apps/meteor/app/livechat/server/lib/Livechat.js
  10. 1
      apps/meteor/app/livechat/server/lib/logger.ts
  11. 2
      apps/meteor/app/livechat/server/sendMessageBySMS.ts
  12. 8
      apps/meteor/app/livechat/server/startup.ts
  13. 32
      apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts
  14. 112
      apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts
  15. 2
      apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts
  16. 2
      apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts
  17. 4
      apps/meteor/ee/app/livechat-enterprise/server/startup.ts
  18. 2
      apps/meteor/lib/callbacks.ts
  19. 39
      apps/meteor/server/models/raw/Users.js
  20. 20
      apps/meteor/tests/data/livechat/businessHours.ts
  21. 6
      apps/meteor/tests/data/livechat/users.ts
  22. 86
      apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts
  23. 2
      packages/core-typings/src/ILivechatAgent.ts
  24. 10
      packages/model-typings/src/models/IUsersModel.ts

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
---
fix: newly added agent not following business hours

@ -419,11 +419,14 @@ export const saveUser = async function (userId, userData) {
await Users.updateOne({ _id: userData._id }, updateUser);
await callbacks.run('afterSaveUser', userData);
// App IPostUserUpdated event hook
const userUpdated = await Users.findOneById(userId);
await callbacks.run('afterSaveUser', {
user: userUpdated,
oldUser: oldUserData,
});
await Apps.triggerEvent(AppEvents.IPostUserUpdated, {
user: userUpdated,
previousUser: oldUserData,

@ -1,23 +1,28 @@
import moment from 'moment-timezone';
import { ILivechatAgentStatus } from '@rocket.chat/core-typings';
import type { ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings';
import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings';
import { LivechatBusinessHours, Users } from '@rocket.chat/models';
import type { UpdateFilter } from 'mongodb';
import type { IWorkHoursCronJobsWrapper } from '../../../../server/models/raw/LivechatBusinessHours';
import { businessHourLogger } from '../lib/logger';
import { filterBusinessHoursThatMustBeOpened } from './Helper';
export interface IBusinessHourBehavior {
findHoursToCreateJobs(): Promise<IWorkHoursCronJobsWrapper[]>;
openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void>;
closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void>;
onDisableBusinessHours(): Promise<void>;
onAddAgentToDepartment(options?: Record<string, any>): Promise<any>;
onAddAgentToDepartment(options?: { departmentId: string; agentsId: string[] }): Promise<any>;
onRemoveAgentFromDepartment(options?: Record<string, any>): Promise<any>;
onRemoveDepartment(department?: ILivechatDepartment): Promise<any>;
onStartBusinessHours(): Promise<void>;
afterSaveBusinessHours(businessHourData: ILivechatBusinessHour): Promise<void>;
allowAgentChangeServiceStatus(agentId: string): Promise<boolean>;
changeAgentActiveStatus(agentId: string, status: string): Promise<any>;
// If a new agent is created, this callback will be called
onNewAgentCreated(agentId: string): Promise<void>;
}
export interface IBusinessHourType {
@ -44,9 +49,7 @@ export abstract class AbstractBusinessHourBehavior {
return this.UsersRepository.isAgentWithinBusinessHours(agentId);
}
// After logout, users are turned not-available by default
// This will turn them available unless they put themselves offline (manual status change)
async changeAgentActiveStatus(agentId: string, status: string): Promise<any> {
async changeAgentActiveStatus(agentId: string, status: ILivechatAgentStatus): Promise<any> {
return this.UsersRepository.setLivechatStatusIf(
agentId,
status,
@ -56,6 +59,29 @@ export abstract class AbstractBusinessHourBehavior {
{ livechatStatusSystemModified: true },
);
}
async onNewAgentCreated(agentId: string): Promise<void> {
businessHourLogger.debug(`Executing onNewAgentCreated for agentId: ${agentId}`);
const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour();
if (!defaultBusinessHour) {
businessHourLogger.debug(`No default business hour found for agentId: ${agentId}`);
return;
}
const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]);
if (!businessHourToOpen.length) {
businessHourLogger.debug(
`No business hour to open found for agentId: ${agentId}. Default business hour is closed. Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.NOT_AVAILABLE}`,
);
await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE);
return;
}
await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id);
businessHourLogger.debug(`Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.AVAILABLE}`);
}
}
export abstract class AbstractBusinessHourType {

@ -7,16 +7,7 @@ import { Users } from '@rocket.chat/models';
import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour';
import { settings } from '../../../settings/server';
import { callbacks } from '../../../../lib/callbacks';
const cronJobDayDict: Record<string, number> = {
Sunday: 0,
Monday: 1,
Tuesday: 2,
Wednesday: 3,
Thursday: 4,
Friday: 5,
Saturday: 6,
};
import { businessHourLogger } from '../lib/logger';
export class BusinessHourManager {
private types: Map<string, IBusinessHourType> = new Map();
@ -35,6 +26,7 @@ export class BusinessHourManager {
async startManager(): Promise<void> {
await this.createCronJobsForWorkHours();
businessHourLogger.debug('Cron jobs created, setting up callbacks');
this.setupCallbacks();
await this.behavior.onStartBusinessHours();
}
@ -115,12 +107,19 @@ export class BusinessHourManager {
callbacks.priority.HIGH,
'business-hour-livechat-on-save-agent-department',
);
callbacks.add(
'livechat.onNewAgentCreated',
this.behavior.onNewAgentCreated.bind(this),
callbacks.priority.HIGH,
'business-hour-livechat-on-agent-created',
);
}
private removeCallbacks(): void {
callbacks.remove('livechat.removeAgentDepartment', 'business-hour-livechat-on-remove-agent-department');
callbacks.remove('livechat.afterRemoveDepartment', 'business-hour-livechat-after-remove-department');
callbacks.remove('livechat.saveAgentDepartment', 'business-hour-livechat-on-save-agent-department');
callbacks.remove('livechat.onNewAgentCreated', 'business-hour-livechat-on-agent-created');
}
private async createCronJobsForWorkHours(): Promise<void> {
@ -137,12 +136,17 @@ export class BusinessHourManager {
await Promise.all(finish.map(({ day, times }) => this.scheduleCronJob(times, day, 'close', this.closeWorkHoursCallback)));
}
private async scheduleCronJob(items: string[], day: string, type: string, job: (day: string, hour: string) => void): Promise<void> {
private async scheduleCronJob(
items: string[],
day: string,
type: 'open' | 'close',
job: (day: string, hour: string) => void,
): Promise<void> {
await Promise.all(
items.map((hour) => {
const jobName = `${day}/${hour}/${type}`;
const time = moment(hour, 'HH:mm');
const scheduleAt = `${time.minutes()} ${time.hours()} * * ${cronJobDayDict[day]}`;
const time = moment(hour, 'HH:mm').day(day);
const jobName = `${time.format('dddd')}/${time.format('HH:mm')}/${type}`;
const scheduleAt = `${time.minutes()} ${time.hours()} * * ${time.day()}`;
this.addToCache(jobName);
return this.cronJobs.add(jobName, scheduleAt, () => job(day, hour));
}),

@ -4,6 +4,7 @@ import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import { LivechatBusinessHours, Users } from '@rocket.chat/models';
import { createDefaultBusinessHourRow } from './LivechatBusinessHours';
import { businessHourLogger } from '../lib/logger';
export const filterBusinessHoursThatMustBeOpened = async (
businessHours: ILivechatBusinessHour[],
@ -52,8 +53,10 @@ export const openBusinessHourDefault = async (): Promise<void> => {
},
});
const businessHoursToOpenIds = (await filterBusinessHoursThatMustBeOpened(activeBusinessHours)).map((businessHour) => businessHour._id);
businessHourLogger.debug({ msg: 'Opening default business hours', businessHoursToOpenIds });
await Users.openAgentsBusinessHoursByBusinessHourId(businessHoursToOpenIds);
await Users.updateLivechatStatusBasedOnBusinessHours();
businessHourLogger.debug('Done opening default business hours');
};
export const createDefaultBusinessHourIfNotExists = async (): Promise<void> => {

@ -3,6 +3,7 @@ import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { IBusinessHourBehavior } from './AbstractBusinessHour';
import { AbstractBusinessHourBehavior } from './AbstractBusinessHour';
import { openBusinessHourDefault } from './Helper';
import { businessHourLogger } from '../lib/logger';
export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior {
async openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
@ -25,6 +26,7 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp
}
async onStartBusinessHours(): Promise<void> {
businessHourLogger.debug('Starting Single Business Hours');
return openBusinessHourDefault();
}

@ -1,23 +1,51 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import type { UsersUpdateParamsPOST } from '@rocket.chat/rest-typings';
import { callbacks } from '../../../../lib/callbacks';
import { Livechat } from '../lib/Livechat';
import { callbackLogger } from '../lib/logger';
type UserData = UsersUpdateParamsPOST['data'] & { _id: string };
type IAfterSaveUserProps = {
user: IUser;
oldUser: IUser | null;
};
const wasAgent = (user: Pick<IUser, 'roles'> | null) => user?.roles?.includes('livechat-agent');
const isAgent = (user: Pick<IUser, 'roles'> | null) => user?.roles?.includes('livechat-agent');
const handleAgentUpdated = async (userData: IAfterSaveUserProps) => {
const {
user: { _id: userId, username },
user: newUser,
oldUser,
} = userData;
if (wasAgent(oldUser) && !isAgent(newUser)) {
callbackLogger.debug('Removing agent', userId);
await Livechat.removeAgent(username);
}
const handleAgentUpdated = async (userData: UserData) => {
if (!userData?.roles?.includes('livechat-agent')) {
await Users.unsetExtension(userData._id);
if (!wasAgent(oldUser) && isAgent(newUser)) {
callbackLogger.debug('Adding agent', userId);
await Livechat.addAgent(username);
}
};
const handleDeactivateUser = async (userData: IUser) => {
if (userData?.roles?.includes('livechat-agent')) {
await Users.unsetExtension(userData._id);
const handleDeactivateUser = async (user: IUser) => {
if (wasAgent(user)) {
callbackLogger.debug('Removing agent', user._id);
await Livechat.removeAgent(user.username);
}
};
callbacks.add('afterSaveUser', handleAgentUpdated, callbacks.priority.LOW, 'livechat-after-save-user-remove-extension');
const handleActivateUser = async (user: IUser) => {
if (isAgent(user)) {
callbackLogger.debug('Adding agent', user._id);
await Livechat.addAgent(user.username);
}
};
callbacks.add('afterSaveUser', handleAgentUpdated, callbacks.priority.LOW, 'livechat-after-save-user-update-agent');
callbacks.add('afterDeactivateUser', handleDeactivateUser, callbacks.priority.LOW, 'livechat-after-deactivate-user-remove-agent');
callbacks.add('afterDeactivateUser', handleDeactivateUser, callbacks.priority.LOW, 'livechat-after-deactivate-user-remove-extension');
callbacks.add('afterActivateUser', handleActivateUser, callbacks.priority.LOW, 'livechat-after-activate-user-add-agent');

@ -3,7 +3,7 @@ import { LivechatRooms } from '@rocket.chat/models';
import { callbacks } from '../../../../lib/callbacks';
import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload';
import { callbackLogger } from '../lib/callbackLogger';
import { callbackLogger } from '../lib/logger';
callbacks.add(
'afterSaveMessage',

@ -533,6 +533,9 @@ export const Livechat = {
if (await addUserRolesAsync(user._id, ['livechat-agent'])) {
await Users.setOperator(user._id, true);
await this.setUserStatusLivechat(user._id, user.status !== 'offline' ? 'available' : 'not-available');
callbacks.runAsync('livechat.onNewAgentCreated', user._id);
return user;
}
@ -571,14 +574,10 @@ export const Livechat = {
const { _id } = user;
if (await removeUserFromRolesAsync(_id, ['livechat-agent'])) {
await Users.setOperator(_id, false);
await Users.removeLivechatData(_id);
await this.setUserStatusLivechat(_id, 'not-available');
await Promise.all([
Users.removeAgent(_id),
LivechatDepartmentAgents.removeByAgentId(_id),
LivechatVisitors.removeContactManagerByUsername(username),
Users.unsetExtension(_id),
]);
return true;
}

@ -1,3 +1,4 @@
import { Logger } from '../../../../server/lib/logger/Logger';
export const callbackLogger = new Logger('[Omnichannel] Callback');
export const businessHourLogger = new Logger('Business Hour');

@ -5,7 +5,7 @@ import { OmnichannelIntegration } from '@rocket.chat/core-services';
import { callbacks } from '../../../lib/callbacks';
import { settings } from '../../settings/server';
import { normalizeMessageFileUpload } from '../../utils/server/functions/normalizeMessageFileUpload';
import { callbackLogger } from './lib/callbackLogger';
import { callbackLogger } from './lib/logger';
callbacks.add(
'afterSaveMessage',

@ -62,10 +62,14 @@ Meteor.startup(async () => {
await createDefaultBusinessHourIfNotExists();
settings.watch<boolean>('Livechat_enable_business_hours', async (value) => {
Livechat.logger.debug(`Changing business hour type to ${value}`);
if (value) {
return businessHourManager.startManager();
await businessHourManager.startManager();
Livechat.logger.debug(`Business hour manager started`);
return;
}
return businessHourManager.stopManager();
await businessHourManager.stopManager();
Livechat.logger.debug(`Business hour manager stopped`);
});
settings.watch<string>('Livechat_Routing_Method', function (value) {

@ -4,11 +4,13 @@ import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import { LivechatBusinessHours, LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models';
import { isEnterprise } from '../../../license/server/license';
import { businessHourLogger } from '../../../../../app/livechat/server/lib/logger';
const getAllAgentIdsWithoutDepartment = async (): Promise<string[]> => {
const agentIdsWithDepartment = (
await LivechatDepartmentAgents.find({ departmentEnabled: true }, { projection: { agentId: 1 } }).toArray()
).map((dept: any) => dept.agentId);
).map((dept) => dept.agentId);
const agentIdsWithoutDepartment = (
await Users.findUsersInRolesWithQuery(
'livechat-agent',
@ -17,7 +19,8 @@ const getAllAgentIdsWithoutDepartment = async (): Promise<string[]> => {
},
{ projection: { _id: 1 } },
).toArray()
).map((user: any) => user._id);
).map((user) => user._id);
return agentIdsWithoutDepartment;
};
@ -43,7 +46,7 @@ const getAllAgentIdsForDefaultBusinessHour = async (): Promise<string[]> => {
return [...new Set([...withoutDepartment, ...withDepartmentNotConnectedToBusinessHour])];
};
const getAgentIdsToHandle = async (businessHour: Record<string, any>): Promise<string[]> => {
const getAgentIdsToHandle = async (businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<string[]> => {
if (businessHour.type === LivechatBusinessHourTypes.DEFAULT) {
return getAllAgentIdsForDefaultBusinessHour();
}
@ -51,22 +54,35 @@ const getAgentIdsToHandle = async (businessHour: Record<string, any>): Promise<s
await LivechatDepartment.findEnabledByBusinessHourId(businessHour._id, {
projection: { _id: 1 },
}).toArray()
).map((dept: any) => dept._id);
).map((dept) => dept._id);
return (
await LivechatDepartmentAgents.findByDepartmentIds(departmentIds, {
projection: { agentId: 1 },
}).toArray()
).map((dept: any) => dept.agentId);
).map((dept) => dept.agentId);
};
export const openBusinessHour = async (businessHour: Record<string, any>): Promise<void> => {
const agentIds: string[] = await getAgentIdsToHandle(businessHour);
export const openBusinessHour = async (businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<void> => {
const agentIds = await getAgentIdsToHandle(businessHour);
businessHourLogger.debug({
msg: 'Opening business hour',
businessHour: businessHour._id,
totalAgents: agentIds.length,
top10AgentIds: agentIds.slice(0, 10),
});
await Users.addBusinessHourByAgentIds(agentIds, businessHour._id);
await Users.updateLivechatStatusBasedOnBusinessHours();
};
export const closeBusinessHour = async (businessHour: Record<string, any>): Promise<void> => {
export const closeBusinessHour = async (businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<void> => {
const agentIds: string[] = await getAgentIdsToHandle(businessHour);
businessHourLogger.debug({
msg: 'Closing business hour',
businessHour: businessHour._id,
totalAgents: agentIds.length,
top10AgentIds: agentIds.slice(0, 10),
});
await Users.removeBusinessHourByAgentIds(agentIds, businessHour._id);
await Users.updateLivechatStatusBasedOnBusinessHours();
};

@ -1,16 +1,12 @@
import moment from 'moment';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { ILivechatDepartment, ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models';
import type { IBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour';
import { AbstractBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour';
import {
filterBusinessHoursThatMustBeOpened,
filterBusinessHoursThatMustBeOpenedByDay,
} from '../../../../../app/livechat/server/business-hour/Helper';
import { filterBusinessHoursThatMustBeOpened } from '../../../../../app/livechat/server/business-hour/Helper';
import { closeBusinessHour, openBusinessHour, removeBusinessHourByAgentIds } from './Helper';
import { bhLogger } from '../lib/logger';
import { businessHourLogger } from '../../../../../app/livechat/server/lib/logger';
interface IBusinessHoursExtraProperties extends ILivechatBusinessHour {
timezoneName: string;
@ -23,6 +19,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
this.onAddAgentToDepartment = this.onAddAgentToDepartment.bind(this);
this.onRemoveAgentFromDepartment = this.onRemoveAgentFromDepartment.bind(this);
this.onRemoveDepartment = this.onRemoveDepartment.bind(this);
this.onNewAgentCreated = this.onNewAgentCreated.bind(this);
}
async onStartBusinessHours(): Promise<void> {
@ -39,6 +36,11 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
},
});
const businessHoursToOpen = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);
businessHourLogger.debug({
msg: 'Starting Multiple Business Hours',
totalBusinessHoursToOpen: businessHoursToOpen.length,
top10BusinessHoursToOpen: businessHoursToOpen.slice(0, 10),
});
for (const businessHour of businessHoursToOpen) {
void this.openBusinessHour(businessHour);
}
@ -84,7 +86,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
return openBusinessHour(businessHour);
}
async onAddAgentToDepartment(options: Record<string, any> = {}): Promise<any> {
async onAddAgentToDepartment(options: { departmentId: string; agentsId: string[] }): Promise<any> {
const { departmentId, agentsId } = options;
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(departmentId, {
projection: { businessHourId: 1 },
@ -132,96 +134,8 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
return this.handleRemoveAgentsFromDepartments(deletedDepartment, agentsIds, options);
}
async allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
const isWithinBushinessHours = await this.UsersRepository.isAgentWithinBusinessHours(agentId);
if (isWithinBushinessHours) {
return true;
}
bhLogger.debug(`No active business hour found for agent with id: ${agentId} based on user's cache. Attempting to recheck the status`);
// double check to see if user is actually within business hours
// this is required since the cache of businessHour Ids we maintain within user's collection might be stale
// in many scenario's like, if the agent is created when a business is active,
// or if a normal user is converted to agent when a business hour is active
const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
const day = currentTime.format('dddd');
const allActiveBusinessHoursForEntireWeek = await this.BusinessHourRepository.findActiveBusinessHours({
projection: {
workHours: 1,
timezone: 1,
type: 1,
active: 1,
},
});
const openedBusinessHours = await filterBusinessHoursThatMustBeOpenedByDay(allActiveBusinessHoursForEntireWeek, day);
if (!openedBusinessHours.length) {
bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. No opened business hour found`);
return false;
}
const agentDepartments = await LivechatDepartmentAgents.find(
{ departmentEnabled: true, agentId },
{ projection: { agentId: 1, departmentId: 1 } },
).toArray();
if (agentDepartments.length) {
// check if any one these departments have a opened business hour linked to it
const departments = await LivechatDepartment.findInIds(
agentDepartments.map(({ departmentId }) => departmentId),
{ projection: { _id: 1, businessHourId: 1 } },
).toArray();
const departmentsWithActiveBH = departments.filter(
({ businessHourId }) => businessHourId && openedBusinessHours.findIndex(({ _id }) => _id === businessHourId) !== -1,
);
if (!departmentsWithActiveBH.length) {
bhLogger.debug(
`No opened business hour found for any of the departments connected to the agent with id: ${agentId}. Now, checking if the default business hour can be used`,
);
// check if this agent has any departments that is connected to any non-default business hour
// if no such departments found then check default BH and if it is active, then allow the agent to change service status
const hasAtLeastOneDepartmentWithNonDefaultBH = departments.some(({ businessHourId }) => {
// check if business hour is active
return businessHourId && allActiveBusinessHoursForEntireWeek.findIndex(({ _id }) => _id === businessHourId) !== -1;
});
if (!hasAtLeastOneDepartmentWithNonDefaultBH) {
const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT);
if (isDefaultBHActive?._id) {
await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId);
bhLogger.debug(`Business hour status recheck passed for agentId: ${agentId}. Found default business hour to be active`);
return true;
}
bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. Found default business hour to be inactive`);
}
return false;
}
const activeBusinessHoursForAgent = departmentsWithActiveBH.map(({ businessHourId }) => businessHourId);
await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId(activeBusinessHoursForAgent, agentId);
bhLogger.debug(
`Business hour status recheck passed for agentId: ${agentId}. Found following business hours to be active:`,
activeBusinessHoursForAgent,
);
return true;
}
// check if default businessHour is active
const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT);
if (isDefaultBHActive?._id) {
await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId);
bhLogger.debug(`Business hour status recheck passed for agentId: ${agentId}. Found default business hour to be active`);
return true;
}
bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. No opened business hour found`);
return false;
allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
return this.UsersRepository.isAgentWithinBusinessHours(agentId);
}
private async handleRemoveAgentsFromDepartments(department: Record<string, any>, agentsIds: string[], options: any): Promise<any> {
@ -255,7 +169,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
return options;
}
private async openBusinessHour(businessHour: Record<string, any>): Promise<void> {
private async openBusinessHour(businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<void> {
return openBusinessHour(businessHour);
}
@ -270,7 +184,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
await removeBusinessHourByAgentIds(agentIds, businessHourId);
}
private async closeBusinessHour(businessHour: Record<string, any>): Promise<void> {
private async closeBusinessHour(businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<void> {
await closeBusinessHour(businessHour);
}
}

@ -4,8 +4,8 @@ import { LivechatRooms, Subscriptions } from '@rocket.chat/models';
import { callbacks } from '../../../../../lib/callbacks';
import { settings } from '../../../../../app/settings/server';
import { debouncedDispatchWaitingQueueStatus } from '../lib/Helper';
import { callbackLogger } from '../../../../../app/livechat/server/lib/callbackLogger';
import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler';
import { callbackLogger } from '../../../../../app/livechat/server/lib/logger';
type LivechatCloseCallbackParams = {
room: IOmnichannelRoom;

@ -4,8 +4,8 @@ import { LivechatRooms, LivechatVisitors, Users } from '@rocket.chat/models';
import { OmnichannelEEService } from '@rocket.chat/core-services';
import { callbacks } from '../../../../../lib/callbacks';
import { callbackLogger } from '../../../../../app/livechat/server/lib/callbackLogger';
import { i18n } from '../../../../../server/lib/i18n';
import { callbackLogger } from '../../../../../app/livechat/server/lib/logger';
const resumeOnHoldCommentAndUser = async (room: IOmnichannelRoom): Promise<{ comment: string; resumedBy: IUser }> => {
const {

@ -8,6 +8,7 @@ import { MultipleBusinessHoursBehavior } from './business-hour/Multiple';
import { SingleBusinessHourBehavior } from '../../../../app/livechat/server/business-hour/Single';
import { businessHourManager } from '../../../../app/livechat/server/business-hour';
import { resetDefaultBusinessHourIfNeeded } from './business-hour/Helper';
import { logger } from './lib/logger';
const visitorActivityMonitor = new VisitorInactivityMonitor();
const businessHours = {
@ -31,12 +32,15 @@ Meteor.startup(async function () {
await updatePredictedVisitorAbandonment();
});
settings.change<string>('Livechat_business_hour_type', async (value) => {
logger.debug(`Changing business hour type to ${value}`);
if (!Object.keys(businessHours).includes(value)) {
logger.error(`Invalid business hour type ${value}`);
return;
}
businessHourManager.registerBusinessHourBehavior(businessHours[value as keyof typeof businessHours]);
if (settings.get('Livechat_enable_business_hours')) {
await businessHourManager.startManager();
logger.debug(`Business hour manager started`);
}
});

@ -61,6 +61,7 @@ interface EventLikeCallbackSignatures {
'livechat:afterReturnRoomAsInquiry': (params: { room: IRoom }) => void;
'livechat.setUserStatusLivechat': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void;
'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void;
'livechat.onNewAgentCreated': (agentId: string) => void;
'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void;
'afterAddedToRoom': (params: { user: IUser; inviter?: IUser }, room: IRoom) => void;
'beforeAddedToRoom': (params: { user: IUser; inviter: IUser }) => void;
@ -91,6 +92,7 @@ interface EventLikeCallbackSignatures {
'afterValidateLogin': (login: { user: IUser }) => void;
'afterJoinRoom': (user: IUser, room: IRoom) => void;
'beforeCreateRoom': (data: { type: IRoom['t']; extraData: { encrypted: boolean } }) => void;
'afterSaveUser': ({ user, oldUser }: { user: IUser; oldUser: IUser | null }) => void;
}
/**

@ -842,9 +842,6 @@ export class UsersRaw extends BaseRaw {
};
const update = {
$set: {
statusLivechat: 'available',
},
$addToSet: {
openBusinessHours: { $each: businessHourIds },
},
@ -878,9 +875,6 @@ export class UsersRaw extends BaseRaw {
};
const update = {
$set: {
statusLivechat: 'available',
},
$addToSet: {
openBusinessHours: businessHourId,
},
@ -985,15 +979,14 @@ export class UsersRaw extends BaseRaw {
}
async isAgentWithinBusinessHours(agentId) {
return (
(await this.find({
_id: agentId,
openBusinessHours: {
$exists: true,
$not: { $size: 0 },
},
}).count()) > 0
);
const query = {
_id: agentId,
openBusinessHours: {
$exists: true,
$not: { $size: 0 },
},
};
return (await this.col.countDocuments(query)) > 0;
}
removeBusinessHoursFromAllUsers() {
@ -2887,4 +2880,20 @@ export class UsersRaw extends BaseRaw {
countRoomMembers(roomId) {
return this.col.countDocuments({ __rooms: roomId, active: true });
}
removeAgent(_id) {
const update = {
$set: {
operator: false,
},
$unset: {
livechat: 1,
statusLivechat: 1,
extension: 1,
openBusinessHours: 1,
},
};
return this.updateOne({ _id }, update);
}
}

@ -1,7 +1,9 @@
import { ILivechatBusinessHour } from "@rocket.chat/core-typings";
import { ILivechatBusinessHour, LivechatBusinessHourTypes } from "@rocket.chat/core-typings";
import { api, credentials, methodCall, request } from "../api-data";
import { updateEESetting, updateSetting } from "../permissions.helper"
import { saveBusinessHour } from "./business-hours";
import moment from "moment";
type ISaveBhApiWorkHour = Omit<ILivechatBusinessHour, '_id' | 'ts' | 'timezone'> & { workHours: { day: string, start: string, finish: string, open: boolean }[] } & { departmentsToApplyBusinessHour?: string } & { timezoneName: string };
export const makeDefaultBusinessHourActiveAndClosed = async () => {
@ -76,6 +78,22 @@ export const disableDefaultBusinessHour = async () => {
});
}
export const getDefaultBusinessHour = async (): Promise<ILivechatBusinessHour> => {
const response = await request.get(api('livechat/business-hour')).set(credentials).query({ type: LivechatBusinessHourTypes.DEFAULT }).expect(200);
return response.body.businessHour;
};
export const openOrCloseBusinessHour = async (businessHour: ILivechatBusinessHour, open: boolean) => {
const enabledBusinessHour = {
...businessHour,
timezoneName: businessHour.timezone.name,
workHours: getWorkHours(open),
departmentsToApplyBusinessHour: businessHour.departments?.map((department) => department._id).join(',') || '',
}
await saveBusinessHour(enabledBusinessHour as any);
}
export const getWorkHours = (open = true): ISaveBhApiWorkHour['workHours'] => {
const workHours: ISaveBhApiWorkHour['workHours'] = [];

@ -3,6 +3,7 @@ import type { IUser } from "@rocket.chat/core-typings";
import { password } from "../user";
import { createUser, login } from "../users.helper";
import { createAgent, makeAgentAvailable } from "./rooms";
import { api, credentials, request } from "../api-data";
export const createBotAgent = async (): Promise<{
credentials: { 'X-Auth-Token': string; 'X-User-Id': string; };
@ -23,3 +24,8 @@ export const createBotAgent = async (): Promise<{
export const getRandomVisitorToken = (): string => faker.string.alphanumeric(17);
export const removeAgent = async (userId: string): Promise<void> => {
await request.delete(api(`livechat/users/agent/${userId}`))
.set(credentials)
.expect(200);
}

@ -1,14 +1,20 @@
/* eslint-env mocha */
import { LivechatBusinessHourTypes, LivechatBusinessHourBehaviors } from '@rocket.chat/core-typings';
import type { ILivechatAgent, ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { ILivechatAgentStatus, LivechatBusinessHourBehaviors, LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { saveBusinessHour } from '../../../data/livechat/business-hours';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
import { updateEESetting, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { IS_EE } from '../../../e2e/config/constants';
import { createUser, deleteUser, getMe, login } from '../../../data/users.helper';
import { createAgent, makeAgentAvailable } from '../../../data/livechat/rooms';
import { getWorkHours } from '../../../data/livechat/businessHours';
import { sleep } from '../../../../lib/utils/sleep';
import { getDefaultBusinessHour, openOrCloseBusinessHour, getWorkHours } from '../../../data/livechat/businessHours';
import type { IUserCredentialsHeader } from '../../../data/user';
import { password } from '../../../data/user';
import { removeAgent } from '../../../data/livechat/users';
describe('[CE] LIVECHAT - business hours', function () {
this.retries(0);
@ -226,4 +232,78 @@ describe('[CE] LIVECHAT - business hours', function () {
expect(result).to.have.property('error');
});
});
describe('BH behavior upon new agent creation/deletion', () => {
let defaultBH: ILivechatBusinessHour;
let agent: ILivechatAgent;
let agentCredentials: IUserCredentialsHeader;
before(async () => {
await updateSetting('Livechat_enable_business_hours', true);
await updateEESetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.SINGLE);
// wait for callbacks to run
await sleep(2000);
defaultBH = await getDefaultBusinessHour();
await openOrCloseBusinessHour(defaultBH, true);
agent = await createUser();
agentCredentials = await login(agent.username, password);
});
it('should create a new agent and verify if it is assigned to the default business hour which is open', async () => {
agent = await createAgent(agent.username);
const latestAgent: ILivechatAgent = await getMe(agentCredentials as any);
expect(latestAgent).to.be.an('object');
expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1);
expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBH._id);
});
it('should create a new agent and verify if it is assigned to the default business hour which is closed', async () => {
await openOrCloseBusinessHour(defaultBH, false);
const newUser: ILivechatAgent = await createUser();
const newUserCredentials = await login(newUser.username, password);
await createAgent(newUser.username);
const latestAgent: ILivechatAgent = await getMe(newUserCredentials);
expect(latestAgent).to.be.an('object');
expect(latestAgent.openBusinessHours).to.be.undefined;
expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE);
});
it('should verify if agent is assigned to BH when it is opened', async () => {
// first verify if agent is not assigned to any BH
let latestAgent: ILivechatAgent = await getMe(agentCredentials as any);
expect(latestAgent).to.be.an('object');
expect(latestAgent.openBusinessHours).to.be.an('array').of.length(0);
expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE);
// now open BH
await openOrCloseBusinessHour(defaultBH, true);
// verify if agent is assigned to BH
latestAgent = await getMe(agentCredentials as any);
expect(latestAgent).to.be.an('object');
expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1);
expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBH._id);
// verify if agent is able to make themselves available
await makeAgentAvailable(agentCredentials as any);
});
it('should verify if BH related props are cleared when an agent is deleted', async () => {
await removeAgent(agent._id);
const latestAgent: ILivechatAgent = await getMe(agentCredentials as any);
expect(latestAgent).to.be.an('object');
expect(latestAgent.openBusinessHours).to.be.undefined;
expect(latestAgent.statusLivechat).to.be.undefined;
});
after(async () => {
await deleteUser(agent._id);
});
});
});

@ -13,4 +13,6 @@ export interface ILivechatAgent extends IUser {
livechatCount: number;
lastRoutingTime: Date;
livechatStatusSystemModified?: boolean;
openBusinessHours?: string[];
}

@ -8,6 +8,7 @@ import type {
ILoginToken,
IPersonalAccessToken,
AtLeast,
ILivechatAgentStatus,
} from '@rocket.chat/core-typings';
import type { FindPaginated, IBaseModel } from './IBaseModel';
@ -92,7 +93,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
setLastRoutingTime(userId: any): Promise<number>;
setLivechatStatusIf(userId: any, status: any, conditions?: any, extraFields?: any): Promise<UpdateResult>;
setLivechatStatusIf(userId: string, status: ILivechatAgentStatus, conditions?: any, extraFields?: any): Promise<UpdateResult>;
getAgentAndAmountOngoingChats(
userId: any,
): Promise<{ agentId: string; username: string; lastAssignTime: Date; lastRoutingTime: Date; queueInfo: { chats: number } }>;
@ -128,7 +129,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds: any, agentId: any): any;
addBusinessHourByAgentIds(agentIds: any, businessHourId: any): any;
addBusinessHourByAgentIds(agentIds: string[], businessHourId: string): any;
removeBusinessHourByAgentIds(agentIds: any, businessHourId: any): any;
@ -142,7 +143,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
setLivechatStatusActiveBasedOnBusinessHours(userId: any): any;
isAgentWithinBusinessHours(agentId: any): Promise<any>;
isAgentWithinBusinessHours(agentId: string): Promise<boolean>;
removeBusinessHoursFromAllUsers(): any;
@ -253,7 +254,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
countAgents(): Promise<number>;
getNextAgent(ignoreAgentId?: string, extraQuery?: Filter<IUser>): Promise<{ agentId: string; username: string } | null>;
getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username: string } | null>;
setLivechatStatus(userId: string, status: UserStatus): Promise<UpdateResult>;
setLivechatStatus(userId: string, status: ILivechatAgentStatus): Promise<UpdateResult>;
setLivechatData(userId: string, data?: Record<string, any>): Promise<UpdateResult>;
closeOffice(): Promise<void>;
openOffice(): Promise<void>;
@ -374,4 +375,5 @@ export interface IUsersModel extends IBaseModel<IUser> {
countRoomMembers(roomId: string): Promise<number>;
countRemote(options?: FindOptions<IUser>): Promise<number>;
findOneByImportId(importId: string, options?: FindOptions<IUser>): Promise<IUser | null>;
removeAgent(_id: string): Promise<UpdateResult>;
}

Loading…
Cancel
Save