fix: Remove the association between BH and disabled/archived departments (#29543)

Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>
Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com>
pull/29638/head^2
Kevin Aleman 3 years ago committed by GitHub
parent 4da8801830
commit b837cb9f2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .changeset/funny-coins-trade.md
  2. 6
      .changeset/rotten-spoons-teach.md
  3. 6
      apps/meteor/app/livechat/imports/server/rest/departments.ts
  4. 4
      apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts
  5. 60
      apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts
  6. 8
      apps/meteor/app/livechat/server/business-hour/Single.ts
  7. 8
      apps/meteor/app/livechat/server/lib/Livechat.js
  8. 6
      apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx
  9. 18
      apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts
  10. 104
      apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts
  11. 19
      apps/meteor/ee/app/livechat-enterprise/server/business-hour/lib/business-hour.ts
  12. 8
      apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts
  13. 2
      apps/meteor/ee/app/livechat-enterprise/server/startup.ts
  14. 2
      apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.js
  15. 2
      apps/meteor/lib/callbacks.ts
  16. 4
      apps/meteor/server/models/raw/LivechatBusinessHours.ts
  17. 78
      apps/meteor/server/models/raw/LivechatDepartment.ts
  18. 10
      apps/meteor/server/models/raw/LivechatDepartmentAgents.ts
  19. 12
      apps/meteor/tests/data/livechat/business-hours.ts
  20. 76
      apps/meteor/tests/data/livechat/businessHours.ts
  21. 68
      apps/meteor/tests/data/livechat/department.ts
  22. 16
      apps/meteor/tests/data/livechat/rooms.ts
  23. 3
      apps/meteor/tests/end-to-end/api/livechat/10-departments.ts
  24. 466
      apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts
  25. 2
      packages/model-typings/src/models/ILivechatBusinessHoursModel.ts
  26. 2
      packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts
  27. 9
      packages/model-typings/src/models/ILivechatDepartmentModel.ts

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
---
Fixed a problem where disabled department agent's where still being activated when applicable business hours met.

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
---
Fixed logic around Default Business Hours where agents from disabled/archived departments where being omitted from processing at closing time

@ -192,11 +192,9 @@ API.v1.addRoute(
},
{
async post() {
if (await Livechat.archiveDepartment(this.urlParams._id)) {
return API.v1.success();
}
await Livechat.archiveDepartment(this.urlParams._id);
return API.v1.failure();
return API.v1.success();
},
},
);

@ -16,7 +16,9 @@ export interface IBusinessHourBehavior {
onDisableBusinessHours(): Promise<void>;
onAddAgentToDepartment(options?: { departmentId: string; agentsId: string[] }): Promise<any>;
onRemoveAgentFromDepartment(options?: Record<string, any>): Promise<any>;
onRemoveDepartment(department?: ILivechatDepartment): Promise<any>;
onRemoveDepartment(options: { department: ILivechatDepartment; agentsIds: string[] }): Promise<any>;
onDepartmentDisabled(department?: ILivechatDepartment): Promise<any>;
onDepartmentArchived(department: Pick<ILivechatDepartment, '_id'>): Promise<void>;
onStartBusinessHours(): Promise<void>;
afterSaveBusinessHours(businessHourData: ILivechatBusinessHour): Promise<void>;
allowAgentChangeServiceStatus(agentId: string): Promise<boolean>;

@ -2,11 +2,12 @@ import moment from 'moment';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import type { AgendaCronJobs } from '@rocket.chat/cron';
import { Users } from '@rocket.chat/models';
import { LivechatDepartment, Users } from '@rocket.chat/models';
import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour';
import { settings } from '../../../settings/server';
import { callbacks } from '../../../../lib/callbacks';
import { closeBusinessHour } from '../../../../ee/app/livechat-enterprise/server/business-hour/Helper';
import { businessHourLogger } from '../lib/logger';
export class BusinessHourManager {
@ -28,6 +29,7 @@ export class BusinessHourManager {
await this.createCronJobsForWorkHours();
businessHourLogger.debug('Cron jobs created, setting up callbacks');
this.setupCallbacks();
await this.cleanupDisabledDepartmentReferences();
await this.behavior.onStartBusinessHours();
}
@ -38,6 +40,40 @@ export class BusinessHourManager {
await this.behavior.onDisableBusinessHours();
}
async restartManager(): Promise<void> {
await this.stopManager();
await this.startManager();
}
async cleanupDisabledDepartmentReferences(): Promise<void> {
// Get business hours with departments enabled and disabled
const bhWithDepartments = await LivechatDepartment.getBusinessHoursWithDepartmentStatuses();
if (!bhWithDepartments.length) {
// If there are no bh, skip
return;
}
for await (const { _id: businessHourId, validDepartments, invalidDepartments } of bhWithDepartments) {
if (!invalidDepartments.length) {
continue;
}
// If there are no enabled departments, close the business hour
const allDepsAreDisabled = validDepartments.length === 0 && invalidDepartments.length > 0;
if (allDepsAreDisabled) {
const businessHour = await this.getBusinessHour(businessHourId, LivechatBusinessHourTypes.CUSTOM);
if (!businessHour) {
continue;
}
await closeBusinessHour(businessHour);
}
// Remove business hour from disabled departments
await LivechatDepartment.removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(invalidDepartments, businessHourId);
}
}
async allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
if (!settings.get('Livechat_enable_business_hours')) {
return true;
@ -88,6 +124,14 @@ export class BusinessHourManager {
return Users.setLivechatStatusActiveBasedOnBusinessHours(agentId);
}
async restartCronJobsIfNecessary(): Promise<void> {
if (!settings.get('Livechat_enable_business_hours')) {
return;
}
await this.createCronJobsForWorkHours();
}
private setupCallbacks(): void {
callbacks.add(
'livechat.removeAgentDepartment',
@ -107,6 +151,18 @@ export class BusinessHourManager {
callbacks.priority.HIGH,
'business-hour-livechat-on-save-agent-department',
);
callbacks.add(
'livechat.afterDepartmentDisabled',
this.behavior.onDepartmentDisabled.bind(this),
callbacks.priority.HIGH,
'business-hour-livechat-on-department-disabled',
);
callbacks.add(
'livechat.afterDepartmentArchived',
this.behavior.onDepartmentArchived.bind(this),
callbacks.priority.HIGH,
'business-hour-livechat-on-department-archived',
);
callbacks.add(
'livechat.onNewAgentCreated',
this.behavior.onNewAgentCreated.bind(this),
@ -119,6 +175,8 @@ export class BusinessHourManager {
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.afterDepartmentDisabled', 'business-hour-livechat-on-department-disabled');
callbacks.remove('livechat.afterDepartmentArchived', 'business-hour-livechat-on-department-archived');
callbacks.remove('livechat.onNewAgentCreated', 'business-hour-livechat-on-agent-created');
}

@ -49,4 +49,12 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp
onRemoveDepartment(): Promise<void> {
return Promise.resolve();
}
onDepartmentDisabled(): Promise<void> {
return Promise.resolve();
}
onDepartmentArchived(): Promise<void> {
return Promise.resolve();
}
}

@ -740,6 +740,8 @@ export const Livechat = {
});
}
// TODO: these kind of actions should be on events instead of here
await LivechatDepartmentAgents.enableAgentsByDepartmentId(_id);
return LivechatDepartmentRaw.unarchiveDepartment(_id);
},
@ -754,7 +756,11 @@ export const Livechat = {
});
}
return LivechatDepartmentRaw.archiveDepartment(_id);
await LivechatDepartmentAgents.disableAgentsByDepartmentId(_id);
await LivechatDepartmentRaw.archiveDepartment(_id);
this.logger.debug({ msg: 'Running livechat.afterDepartmentArchived callback for department:', departmentId: _id });
await callbacks.run('livechat.afterDepartmentArchived', department);
},
showConnecting() {

@ -13,12 +13,14 @@ type AutoCompleteDepartmentMultipleProps = {
onChange: (value: PaginatedMultiSelectOption[]) => void;
onlyMyDepartments?: boolean;
showArchived?: boolean;
enabled?: boolean;
};
const AutoCompleteDepartmentMultiple = ({
value,
onlyMyDepartments = false,
showArchived = false,
enabled = false,
onChange = () => undefined,
}: AutoCompleteDepartmentMultipleProps) => {
const t = useTranslation();
@ -28,8 +30,8 @@ const AutoCompleteDepartmentMultiple = ({
const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList(
useMemo(
() => ({ filter: debouncedDepartmentsFilter, onlyMyDepartments, ...(showArchived && { showArchived: true }) }),
[debouncedDepartmentsFilter, onlyMyDepartments, showArchived],
() => ({ filter: debouncedDepartmentsFilter, onlyMyDepartments, ...(showArchived && { showArchived: true }), enabled }),
[debouncedDepartmentsFilter, enabled, onlyMyDepartments, showArchived],
),
);

@ -7,9 +7,12 @@ 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) => dept.agentId);
// Fetch departments with agents excluding archived ones (disabled ones still can be tied to business hours)
// Then find the agents that are not in any of those departments
const departmentIds = (await LivechatDepartment.findNotArchived({ projection: { _id: 1 } }).toArray()).map(({ _id }) => _id);
const agentIdsWithDepartment = await LivechatDepartmentAgents.findAllAgentsConnectedToListOfDepartments(departmentIds);
const agentIdsWithoutDepartment = (
await Users.findUsersInRolesWithQuery(
@ -62,7 +65,10 @@ const getAgentIdsToHandle = async (businessHour: Pick<ILivechatBusinessHour, '_i
).map((dept) => dept.agentId);
};
export const openBusinessHour = async (businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<void> => {
export const openBusinessHour = async (
businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>,
updateLivechatStatus = true,
): Promise<void> => {
const agentIds = await getAgentIdsToHandle(businessHour);
businessHourLogger.debug({
msg: 'Opening business hour',
@ -72,7 +78,9 @@ export const openBusinessHour = async (businessHour: Pick<ILivechatBusinessHour,
});
await Users.addBusinessHourByAgentIds(agentIds, businessHour._id);
await Users.updateLivechatStatusBasedOnBusinessHours();
if (updateLivechatStatus) {
await Users.updateLivechatStatusBasedOnBusinessHours();
}
};
export const closeBusinessHour = async (businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<void> => {

@ -1,12 +1,14 @@
import moment from 'moment';
import type { ILivechatDepartment, ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models';
import { LivechatDepartment, LivechatDepartmentAgents, Users } 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 } from '../../../../../app/livechat/server/business-hour/Helper';
import { closeBusinessHour, openBusinessHour, removeBusinessHourByAgentIds } from './Helper';
import { businessHourLogger } from '../../../../../app/livechat/server/lib/logger';
import { bhLogger } from '../lib/logger';
import { settings } from '../../../../../app/settings/server';
import { businessHourManager } from '../../../../../app/livechat/server/business-hour';
interface IBusinessHoursExtraProperties extends ILivechatBusinessHour {
timezoneName: string;
@ -19,6 +21,8 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
this.onAddAgentToDepartment = this.onAddAgentToDepartment.bind(this);
this.onRemoveAgentFromDepartment = this.onRemoveAgentFromDepartment.bind(this);
this.onRemoveDepartment = this.onRemoveDepartment.bind(this);
this.onDepartmentArchived = this.onDepartmentArchived.bind(this);
this.onDepartmentDisabled = this.onDepartmentDisabled.bind(this);
this.onNewAgentCreated = this.onNewAgentCreated.bind(this);
}
@ -36,7 +40,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
},
});
const businessHoursToOpen = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);
businessHourLogger.debug({
bhLogger.debug({
msg: 'Starting Multiple Business Hours',
totalBusinessHoursToOpen: businessHoursToOpen.length,
top10BusinessHoursToOpen: businessHoursToOpen.slice(0, 10),
@ -125,13 +129,100 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
return this.handleRemoveAgentsFromDepartments(department, agentsId, options);
}
async onRemoveDepartment(options: Record<string, any> = {}): Promise<any> {
async onRemoveDepartment(options: { department: ILivechatDepartment; agentsIds: string[] }): Promise<any> {
bhLogger.debug(`onRemoveDepartment: department ${options.department._id} removed`);
const { department, agentsIds } = options;
if (!department || !agentsIds?.length) {
return options;
}
const deletedDepartment = LivechatDepartment.trashFindOneById(department._id);
return this.handleRemoveAgentsFromDepartments(deletedDepartment, agentsIds, options);
return this.onDepartmentDisabled(department);
}
async onDepartmentDisabled(department: ILivechatDepartment): Promise<void> {
if (!department.businessHourId) {
bhLogger.debug({
msg: 'onDepartmentDisabled: department has no business hour',
departmentId: department._id,
});
return;
}
// Get business hour
let businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId);
if (!businessHour) {
bhLogger.error({
msg: 'onDepartmentDisabled: business hour not found',
businessHourId: department.businessHourId,
});
return;
}
// Unlink business hour from department
await LivechatDepartment.removeBusinessHourFromDepartmentsByIdsAndBusinessHourId([department._id], businessHour._id);
// cleanup user's cache for default business hour and this business hour
const defaultBH = await this.BusinessHourRepository.findOneDefaultBusinessHour();
if (!defaultBH) {
bhLogger.error('onDepartmentDisabled: default business hour not found');
throw new Error('Default business hour not found');
}
await this.UsersRepository.closeAgentsBusinessHoursByBusinessHourIds([businessHour._id, defaultBH._id]);
// If i'm the only one, disable the business hour
const imTheOnlyOne = !(await LivechatDepartment.countByBusinessHourIdExcludingDepartmentId(businessHour._id, department._id));
if (imTheOnlyOne) {
bhLogger.warn({
msg: 'onDepartmentDisabled: department is the only one on business hour, disabling it',
departmentId: department._id,
businessHourId: businessHour._id,
});
await this.BusinessHourRepository.disableBusinessHour(businessHour._id);
businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId);
if (!businessHour) {
bhLogger.error({
msg: 'onDepartmentDisabled: business hour not found',
businessHourId: department.businessHourId,
});
throw new Error(`Business hour ${department.businessHourId} not found`);
}
}
// start default business hour and this BH if needed
if (!settings.get('Livechat_enable_business_hours')) {
bhLogger.debug(`onDepartmentDisabled: business hours are disabled. skipping`);
return;
}
const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([businessHour, defaultBH]);
for await (const bh of businessHourToOpen) {
bhLogger.debug({
msg: 'onDepartmentDisabled: opening business hour',
businessHourId: bh._id,
});
await openBusinessHour(bh, false);
}
await Users.updateLivechatStatusBasedOnBusinessHours();
await businessHourManager.restartCronJobsIfNecessary();
bhLogger.debug({
msg: 'onDepartmentDisabled: successfully processed department disabled event',
departmentId: department._id,
});
}
async onDepartmentArchived(department: Pick<ILivechatDepartment, '_id'>): Promise<void> {
bhLogger.debug('Processing department archived event on multiple business hours', department);
const dbDepartment = await LivechatDepartment.findOneById(department._id, { projection: { businessHourId: 1, _id: 1 } });
if (!dbDepartment) {
bhLogger.error(`No department found with id: ${department._id} when archiving it`);
return;
}
return this.onDepartmentDisabled(dbDepartment);
}
allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
@ -147,7 +238,6 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
}
// TODO: We're doing a full fledged aggregation with lookups and getting the whole array just for getting the length? :(
if (!(await LivechatDepartmentAgents.findAgentsByAgentIdAndBusinessHourId(agentId, department.businessHourId)).length) {
// eslint-disable-line no-await-in-loop
agentIdsToRemoveCurrentBusinessHour.push(agentId);
}
}

@ -1,6 +1,6 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatBusinessHours } from '@rocket.chat/models';
import { LivechatBusinessHours, LivechatDepartment } from '@rocket.chat/models';
import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission';
import type { IPaginatedResponse, IPagination } from '../../api/lib/definition';
@ -26,8 +26,23 @@ export async function findBusinessHours(userId: string, { offset, count, sort }:
const [businessHours, total] = await Promise.all([cursor.toArray(), totalCount]);
// add departments to businessHours
const businessHoursWithDepartments = await Promise.all(
businessHours.map(async (businessHour) => {
const currentDepartments = await LivechatDepartment.findByBusinessHourId(businessHour._id, {
projection: { _id: 1 },
}).toArray();
if (currentDepartments.length) {
businessHour.departments = currentDepartments;
}
return businessHour;
}),
);
return {
businessHours,
businessHours: businessHoursWithDepartments,
count: businessHours.length,
offset,
total,

@ -26,6 +26,7 @@ import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingMa
import { settings } from '../../../../../app/settings/server';
import { queueLogger } from './logger';
import { getInquirySortMechanismSetting } from '../../../../../app/livechat/server/lib/settings';
import { callbacks } from '../../../../../lib/callbacks';
export const LivechatEnterprise = {
async addMonitor(username: string) {
@ -201,7 +202,7 @@ export const LivechatEnterprise = {
) {
check(_id, Match.Maybe(String));
const department = _id ? await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1, archived: 1 } }) : null;
const department = _id ? await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null;
if (!hasLicense('livechat-enterprise')) {
const totalDepartments = await LivechatDepartmentRaw.countTotal();
@ -278,6 +279,11 @@ export const LivechatEnterprise = {
await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled);
}
// Disable event
if (department?.enabled && !departmentDB?.enabled) {
void callbacks.run('livechat.afterDepartmentDisabled', departmentDB);
}
return departmentDB;
},

@ -39,7 +39,7 @@ Meteor.startup(async function () {
}
businessHourManager.registerBusinessHourBehavior(businessHours[value as keyof typeof businessHours]);
if (settings.get('Livechat_enable_business_hours')) {
await businessHourManager.startManager();
await businessHourManager.restartManager();
logger.debug(`Business hour manager started`);
}
});

@ -30,7 +30,7 @@ const BusinessHoursMultiple = ({ values = {}, handlers = {}, className }) => {
<Field className={className}>
<Field.Label>{t('Departments')}</Field.Label>
<Field.Row>
<AutoCompleteDepartmentMultiple value={departments} onChange={handleDepartments} />
<AutoCompleteDepartmentMultiple value={departments} onChange={handleDepartments} enabled={true} />
</Field.Row>
</Field>
</>

@ -92,6 +92,8 @@ interface EventLikeCallbackSignatures {
'afterValidateLogin': (login: { user: IUser }) => void;
'afterJoinRoom': (user: IUser, room: IRoom) => void;
'beforeCreateRoom': (data: { type: IRoom['t']; extraData: { encrypted: boolean } }) => void;
'livechat.afterDepartmentDisabled': (department: ILivechatDepartmentRecord) => void;
'livechat.afterDepartmentArchived': (department: Pick<ILivechatDepartmentRecord, '_id'>) => void;
'afterSaveUser': ({ user, oldUser }: { user: IUser; oldUser: IUser | null }) => void;
}

@ -171,4 +171,8 @@ export class LivechatBusinessHoursRaw extends BaseRaw<ILivechatBusinessHour> imp
}
return this.col.find(query, options).toArray();
}
disableBusinessHour(businessHourId: string): Promise<any> {
return this.updateOne({ _id: businessHourId }, { $set: { active: false } });
}
}

@ -83,6 +83,12 @@ export class LivechatDepartmentRaw extends BaseRaw<ILivechatDepartment> implemen
},
sparse: true,
},
{
key: {
archived: 1,
},
sparse: true,
},
];
}
@ -123,6 +129,11 @@ export class LivechatDepartmentRaw extends BaseRaw<ILivechatDepartment> implemen
return this.find(query, options);
}
countByBusinessHourIdExcludingDepartmentId(businessHourId: string, departmentId: string): Promise<number> {
const query = { businessHourId, _id: { $ne: departmentId } };
return this.col.countDocuments(query);
}
findEnabledByBusinessHourId(businessHourId: string, options: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment> {
const query = { businessHourId, enabled: true };
return this.find(query, options);
@ -236,7 +247,12 @@ export class LivechatDepartmentRaw extends BaseRaw<ILivechatDepartment> implemen
await LivechatDepartmentAgents.setDepartmentEnabledByDepartmentId(_id, data.enabled);
}
return Object.assign(record, { _id });
const latestDept = await this.findOneById(_id);
if (!latestDept) {
throw new Error(`Department ${_id} not found`);
}
return latestDept;
}
unsetFallbackDepartmentByDepartmentId(departmentId: string): Promise<Document | UpdateResult> {
@ -354,6 +370,66 @@ export class LivechatDepartmentRaw extends BaseRaw<ILivechatDepartment> implemen
return this.find(query, options);
}
findNotArchived(options: FindOptions<ILivechatDepartment> = {}): FindCursor<ILivechatDepartment> {
const query = { archived: { $ne: false } };
return this.find(query, options);
}
getBusinessHoursWithDepartmentStatuses(): Promise<
{
_id: string;
validDepartments: string[];
invalidDepartments: string[];
}[]
> {
return this.col
.aggregate<{ _id: string; validDepartments: string[]; invalidDepartments: string[] }>([
{
$match: {
businessHourId: {
$exists: true,
},
},
},
{
$group: {
_id: '$businessHourId',
validDepartments: {
$push: {
$cond: {
if: {
$or: [
{
$eq: ['$enabled', true],
},
{
$ne: ['$archived', true],
},
],
},
then: '$_id',
else: '$$REMOVE',
},
},
},
invalidDepartments: {
$push: {
$cond: {
if: {
$or: [{ $eq: ['$enabled', false] }, { $eq: ['$archived', true] }],
},
then: '$_id',
else: '$$REMOVE',
},
},
},
},
},
])
.toArray();
}
checkIfMonitorIsMonitoringDepartmentById(monitorId: string, departmentId: string): Promise<boolean> {
const aggregation = [
{

@ -357,8 +357,16 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw<ILivechatDepartmentAgen
return this.col.countDocuments({ departmentId });
}
disableAgentsByDepartmentId(departmentId: string): Promise<UpdateResult | Document> {
return this.updateMany({ departmentId }, { $set: { departmentEnabled: false } });
}
enableAgentsByDepartmentId(departmentId: string): Promise<UpdateResult | Document> {
return this.updateMany({ departmentId }, { $set: { departmentEnabled: true } });
}
findAllAgentsConnectedToListOfDepartments(departmentIds: string[]): Promise<string[]> {
return this.col.distinct('agentId', { departmentId: { $in: departmentIds } });
return this.col.distinct('agentId', { departmentId: { $in: departmentIds }, departmentEnabled: true });
}
}

@ -1,12 +0,0 @@
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { credentials, methodCall, request } from '../api-data';
export const saveBusinessHour = async (businessHour: ILivechatBusinessHour) => {
const { body } = await request
.post(methodCall('livechat:saveBusinessHour'))
.set(credentials)
.send({ message: JSON.stringify({ params: [businessHour], msg: 'method', method: 'livechat:saveBusinessHour', id: '101' }) })
.expect(200);
return JSON.parse(body.message);
};

@ -1,11 +1,49 @@
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 };
// TODO: Migrate to an API call and return the business hour updated/created
export const saveBusinessHour = async (businessHour: ISaveBhApiWorkHour) => {
const { body } = await request
.post(methodCall('livechat:saveBusinessHour'))
.set(credentials)
.send({ message: JSON.stringify({ params: [businessHour], msg: 'method', method: 'livechat:saveBusinessHour', id: '101' }) })
.expect(200);
return JSON.parse(body.message);
};
export const createCustomBusinessHour = async (departments: string[]): Promise<ILivechatBusinessHour> => {
const name = `business-hour-${Date.now()}`;
const businessHour: ISaveBhApiWorkHour = {
name,
active: true,
type: LivechatBusinessHourTypes.CUSTOM,
workHours: getWorkHours(),
timezoneName: 'Asia/Calcutta',
departmentsToApplyBusinessHour: '',
};
if (departments.length) {
businessHour.departmentsToApplyBusinessHour = departments.join(',');
}
await saveBusinessHour(businessHour);
const existingBusinessHours: ILivechatBusinessHour[] = await getAllCustomBusinessHours();
const createdBusinessHour = existingBusinessHours.find((bh) => bh.name === name);
if (!createdBusinessHour) {
throw new Error('Could not create business hour');
}
return createdBusinessHour;
};
export const makeDefaultBusinessHourActiveAndClosed = async () => {
// enable settings
await updateSetting('Livechat_enable_business_hours', true);
@ -17,6 +55,7 @@ export const makeDefaultBusinessHourActiveAndClosed = async () => {
.set(credentials)
.send();
// TODO: Refactor this to use openOrCloseBusinessHour() instead
const workHours = businessHour.workHours as { start: string; finish: string; day: string, open: boolean }[];
const allEnabledWorkHours = workHours.map((workHour) => {
workHour.open = true;
@ -53,6 +92,7 @@ export const disableDefaultBusinessHour = async () => {
.set(credentials)
.send();
// TODO: Refactor this to use openOrCloseBusinessHour() instead
const workHours = businessHour.workHours as { start: string; finish: string; day: string, open: boolean }[];
const allDisabledWorkHours = workHours.map((workHour) => {
workHour.open = false;
@ -78,16 +118,47 @@ export const disableDefaultBusinessHour = async () => {
});
}
export const removeCustomBusinessHour = async (businessHourId: string) => {
await request
.post(methodCall('livechat:removeBusinessHour'))
.set(credentials)
.send({ message: JSON.stringify({ params: [businessHourId, LivechatBusinessHourTypes.CUSTOM], msg: 'method', method: 'livechat:removeBusinessHour', id: '101' }) })
.expect(200);
};
const getAllCustomBusinessHours = async (): Promise<ILivechatBusinessHour[]> => {
const response = await request.get(api('livechat/business-hours')).set(credentials).expect(200);
return (response.body.businessHours || []).filter((businessHour: ILivechatBusinessHour) => businessHour.type === LivechatBusinessHourTypes.CUSTOM);
};
export const removeAllCustomBusinessHours = async () => {
const existingBusinessHours: ILivechatBusinessHour[] = await getAllCustomBusinessHours();
const promises = existingBusinessHours.map((businessHour) => removeCustomBusinessHour(businessHour._id));
await Promise.all(promises);
};
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 getCustomBusinessHourById = async (businessHourId: string): Promise<ILivechatBusinessHour> => {
const response = await request.get(api('livechat/business-hour')).set(credentials).query({ type: LivechatBusinessHourTypes.CUSTOM, _id: businessHourId }).expect(200);
return response.body.businessHour;
};
export const openOrCloseBusinessHour = async (businessHour: ILivechatBusinessHour, open: boolean) => {
const enabledBusinessHour = {
...businessHour,
timezoneName: businessHour.timezone.name,
workHours: getWorkHours(open),
workHours: getWorkHours().map((workHour) => {
return {
...workHour,
open,
}
}),
departmentsToApplyBusinessHour: businessHour.departments?.map((department) => department._id).join(',') || '',
}
@ -102,6 +173,7 @@ export const getWorkHours = (open = true): ISaveBhApiWorkHour['workHours'] => {
day: moment().day(i).format('dddd'),
start: '00:00',
finish: '23:59',
open,
});
}

@ -1,33 +1,33 @@
import { faker } from '@faker-js/faker';
import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import type { ILivechatDepartment, IUser, LivechatDepartmentDTO } from '@rocket.chat/core-typings';
import { api, credentials, methodCall, request } from '../api-data';
import { IUserCredentialsHeader, password } from '../user';
import { createUser, login } from '../users.helper';
import { createAgent, makeAgentAvailable } from './rooms';
import type { DummyResponse } from './utils';
export const createDepartment = (): Promise<ILivechatDepartment> =>
new Promise((resolve, reject) => {
request
.post(api('livechat/department'))
.send({
department: {
enabled: false,
email: 'email@email.com',
showOnRegistration: true,
showOnOfflineForm: true,
name: `new department ${Date.now()}`,
description: 'created from api',
},
})
.set(credentials)
.end((err: Error, res: DummyResponse<ILivechatDepartment>) => {
if (err) {
return reject(err);
}
resolve(res.body.department);
});
});
export const NewDepartmentData = ((): Partial<ILivechatDepartment> => ({
enabled: true,
name: `new department ${Date.now()}`,
description: 'created from api',
showOnRegistration: true,
email: faker.internet.email(),
showOnOfflineForm: true,
}))();
export const createDepartment = async (departmentData: Partial<ILivechatDepartment> = NewDepartmentData): Promise<ILivechatDepartment> => {
const response = await request.post(api('livechat/department')).set(credentials).send({
department: departmentData,
}).expect(200);
return response.body.department;
};
export const updateDepartment = async (departmentId: string, departmentData: Partial<LivechatDepartmentDTO>): Promise<ILivechatDepartment> => {
const response = await request.put(api(`livechat/department/${ departmentId }`)).set(credentials).send({
department: departmentData,
}).expect(200);
return response.body.department;
};
export const createDepartmentWithMethod = (initialAgents: { agentId: string, username: string }[] = []) =>
new Promise((resolve, reject) => {
@ -88,3 +88,23 @@ export const addOrRemoveAgentFromDepartment = async (departmentId: string, agent
throw new Error('Failed to add or remove agent from department. Status code: ' + response.status + '\n' + response.body);
}
}
export const archiveDepartment = async (departmentId: string): Promise<void> => {
await request.post(api(`livechat/department/${ departmentId }/archive`)).set(credentials).expect(200);
}
export const disableDepartment = async (department: ILivechatDepartment): Promise<void> => {
department.enabled = false;
delete department._updatedAt;
const updatedDepartment = await updateDepartment(department._id, department);
expect(updatedDepartment.enabled).to.be.false;
}
export const deleteDepartment = async (departmentId: string): Promise<void> => {
await request.delete(api(`livechat/department/${ departmentId }`)).set(credentials).expect(200);
}
export const getDepartmentById = async (departmentId: string): Promise<ILivechatDepartment> => {
const response = await request.get(api(`livechat/department/${ departmentId }`)).set(credentials).expect(200);
return response.body.department;
};

@ -106,22 +106,6 @@ export const createDepartment = (agents?: { agentId: string }[], name?: string):
});
};
export const deleteDepartment = (departmentId: string): Promise<unknown> => {
return new Promise((resolve, reject) => {
request
.delete(api(`livechat/department/${departmentId}`))
.set(credentials)
.send()
.expect(200)
.end((err: Error, res: DummyResponse<ILivechatAgent>) => {
if (err) {
return reject(err);
}
resolve(res.body);
});
});
};
export const createAgent = (overrideUsername?: string): Promise<ILivechatAgent> =>
new Promise((resolve, reject) => {
request

@ -12,9 +12,8 @@ import {
createVisitor,
createLivechatRoom,
getLivechatRoomInfo,
deleteDepartment,
} from '../../../data/livechat/rooms';
import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department';
import { createDepartmentWithAnOnlineAgent, deleteDepartment } from '../../../data/livechat/department';
import { IS_EE } from '../../../e2e/config/constants';
import { createUser } from '../../../data/users.helper';
import { createMonitor, createUnit } from '../../../data/livechat/units';

@ -1,22 +1,37 @@
/* eslint-env mocha */
import type { ILivechatAgent, ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { ILivechatAgentStatus, LivechatBusinessHourBehaviors, LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { ILivechatAgent, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatBusinessHourBehaviors, LivechatBusinessHourTypes, ILivechatAgentStatus } 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 { updateEESetting, updatePermission, updateSetting } from '../../../data/permissions.helper';
import {
getDefaultBusinessHour,
removeAllCustomBusinessHours,
saveBusinessHour,
openOrCloseBusinessHour,
createCustomBusinessHour,
getCustomBusinessHourById,
getWorkHours,
} from '../../../data/livechat/businessHours';
import { removePermissionFromAllRoles, restorePermissionToRoles, updateSetting, updateEESetting } from '../../../data/permissions.helper';
import { IS_EE } from '../../../e2e/config/constants';
import {
addOrRemoveAgentFromDepartment,
archiveDepartment,
createDepartmentWithAnOnlineAgent,
disableDepartment,
getDepartmentById,
deleteDepartment,
} from '../../../data/livechat/department';
import { sleep } from '../../../../lib/utils/sleep';
import { createUser, deleteUser, getMe, login } from '../../../data/users.helper';
import { createAgent, makeAgentAvailable } from '../../../data/livechat/rooms';
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 () {
describe('LIVECHAT - business hours', function () {
this.retries(0);
before((done) => getCredentials(done));
@ -28,14 +43,15 @@ describe('[CE] LIVECHAT - business hours', function () {
});
let defaultBhId: any;
describe('livechat/business-hour', () => {
describe('[CE] livechat/business-hour', () => {
it('should fail when user doesnt have view-livechat-business-hours permission', async () => {
await updatePermission('view-livechat-business-hours', []);
await removePermissionFromAllRoles('view-livechat-business-hours');
const response = await request.get(api('livechat/business-hour')).set(credentials).expect(403);
expect(response.body.success).to.be.false;
await restorePermissionToRoles('view-livechat-business-hours');
});
it('should fail when business hour type is not a valid BH type', async () => {
await updatePermission('view-livechat-business-hours', ['admin', 'livechat-manager']);
const response = await request.get(api('livechat/business-hour')).set(credentials).query({ type: 'invalid' }).expect(200);
expect(response.body.success).to.be.true;
expect(response.body.businessHour).to.be.null;
@ -87,14 +103,15 @@ describe('[CE] LIVECHAT - business hours', function () {
});
});
(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - business hours', () => {
(IS_EE ? describe : describe.skip)('[EE] livechat/business-hour', () => {
it('should fail if user doesnt have view-livechat-business-hours permission', async () => {
await updatePermission('view-livechat-business-hours', []);
await removePermissionFromAllRoles('view-livechat-business-hours');
const response = await request.get(api('livechat/business-hours')).set(credentials).expect(403);
expect(response.body.success).to.be.false;
await restorePermissionToRoles('view-livechat-business-hours');
});
it('should return a list of business hours', async () => {
await updatePermission('view-livechat-business-hours', ['admin', 'livechat-manager']);
const response = await request.get(api('livechat/business-hours')).set(credentials).expect(200);
expect(response.body.success).to.be.true;
expect(response.body.businessHours).to.be.an('array').with.lengthOf.greaterThan(0);
@ -233,6 +250,429 @@ describe('[CE] LIVECHAT - business hours', function () {
});
});
// Scenario: Assume we have a BH linked to a department, and we archive the department
// Expected result:
// 1) If BH is open and only linked to that department, it should be closed
// 2) If BH is open and linked to other departments, it should remain open
// 3) Agents within the archived department should be assigned to default BH
// 3.1) We'll also need to handle the case where if an agent is assigned to "dep1"
// and "dep2" and both these depts are connected to same BH, then in this case after
// archiving "dep1", we'd still need to BH within this user's cache since he's part of
// "dep2" which is linked to BH
(IS_EE ? describe : describe.skip)('[EE] BH operations post department archiving', () => {
let defaultBusinessHour: ILivechatBusinessHour;
let customBusinessHour: ILivechatBusinessHour;
let deptLinkedToCustomBH: ILivechatDepartment;
let agentLinkedToDept: Awaited<ReturnType<typeof createDepartmentWithAnOnlineAgent>>['agent'];
before(async () => {
await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE);
// wait for the callbacks to be registered
await sleep(1000);
});
beforeEach(async () => {
// cleanup any existing business hours
await removeAllCustomBusinessHours();
// get default business hour
defaultBusinessHour = await getDefaultBusinessHour();
// close default business hour
await openOrCloseBusinessHour(defaultBusinessHour, false);
// create custom business hour and link it to a department
const { department, agent } = await createDepartmentWithAnOnlineAgent();
customBusinessHour = await createCustomBusinessHour([department._id]);
agentLinkedToDept = agent;
deptLinkedToCustomBH = department;
// open custom business hour
await openOrCloseBusinessHour(customBusinessHour, true);
});
it('upon archiving a department, if BH is open and only linked to that department, it should be closed', async () => {
// archive department
await archiveDepartment(deptLinkedToCustomBH._id);
// verify if department is archived and BH link is removed
const department = await getDepartmentById(deptLinkedToCustomBH._id);
expect(department).to.be.an('object');
expect(department).to.have.property('archived', true);
expect(department.businessHourId).to.be.undefined;
// verify if BH is closed
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH).to.have.property('active', false);
expect(latestCustomBH.departments).to.be.an('array').that.is.empty;
});
it('upon archiving a department, if BH is open and linked to other departments, it should remain open', async () => {
// create another department and link it to the same BH
const { department, agent } = await createDepartmentWithAnOnlineAgent();
await removeAllCustomBusinessHours();
customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]);
// archive department
await archiveDepartment(deptLinkedToCustomBH._id);
// verify if department is archived and BH link is removed
const archivedDepartment = await getDepartmentById(deptLinkedToCustomBH._id);
expect(archivedDepartment).to.be.an('object');
expect(archivedDepartment).to.have.property('archived', true);
expect(archivedDepartment.businessHourId).to.be.undefined;
// verify if other department is not archived and BH link is not removed
const otherDepartment = await getDepartmentById(department._id);
expect(otherDepartment).to.be.an('object');
expect(otherDepartment.businessHourId).to.be.equal(customBusinessHour._id);
// verify if BH is still open
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH).to.have.property('active', true);
expect(latestCustomBH.departments).to.be.an('array').of.length(1);
expect(latestCustomBH?.departments?.[0]._id).to.be.equal(department._id);
// cleanup
await deleteDepartment(department._id);
await deleteUser(agent.user);
});
it('upon archiving a department, agents within the archived department should be assigned to default BH', async () => {
await openOrCloseBusinessHour(defaultBusinessHour, true);
// archive department
await archiveDepartment(deptLinkedToCustomBH._id);
const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials 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(defaultBusinessHour._id);
});
it('upon archiving a department, overlapping agents should still have BH within their cache', async () => {
// create another department and link it to the same BH
const { department, agent } = await createDepartmentWithAnOnlineAgent();
await removeAllCustomBusinessHours();
customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]);
// create overlapping agent by adding previous agent to newly created department
await addOrRemoveAgentFromDepartment(
department._id,
{
agentId: agentLinkedToDept.user._id,
username: agentLinkedToDept.user.username || '',
},
true,
);
// archive department
await archiveDepartment(deptLinkedToCustomBH._id);
// verify if department is archived and BH link is removed
const archivedDepartment = await getDepartmentById(deptLinkedToCustomBH._id);
expect(archivedDepartment).to.be.an('object');
expect(archivedDepartment).to.have.property('archived', true);
expect(archivedDepartment.businessHourId).to.be.undefined;
// verify if BH is still open
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH).to.have.property('active', true);
expect(latestCustomBH.departments).to.be.an('array').of.length(1);
expect(latestCustomBH?.departments?.[0]?._id).to.be.equal(department._id);
// verify if overlapping agent still has BH within his cache
const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials 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(customBusinessHour._id);
// verify if other agent still has BH within his cache
const otherAgent: ILivechatAgent = await getMe(agent.credentials as any);
expect(otherAgent).to.be.an('object');
expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1);
expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id);
// cleanup
await deleteDepartment(department._id);
await deleteUser(agent.user);
});
afterEach(async () => {
await deleteDepartment(deptLinkedToCustomBH._id);
await deleteUser(agentLinkedToDept.user);
});
});
(IS_EE ? describe : describe.skip)('[EE] BH operations post department disablement', () => {
let defaultBusinessHour: ILivechatBusinessHour;
let customBusinessHour: ILivechatBusinessHour;
let deptLinkedToCustomBH: ILivechatDepartment;
let agentLinkedToDept: Awaited<ReturnType<typeof createDepartmentWithAnOnlineAgent>>['agent'];
before(async () => {
await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE);
// wait for the callbacks to be registered
await sleep(1000);
});
beforeEach(async () => {
// cleanup any existing business hours
await removeAllCustomBusinessHours();
// get default business hour
defaultBusinessHour = await getDefaultBusinessHour();
// close default business hour
await openOrCloseBusinessHour(defaultBusinessHour, false);
// create custom business hour and link it to a department
const { department, agent } = await createDepartmentWithAnOnlineAgent();
customBusinessHour = await createCustomBusinessHour([department._id]);
agentLinkedToDept = agent;
deptLinkedToCustomBH = department;
// open custom business hour
await openOrCloseBusinessHour(customBusinessHour, true);
});
it('upon disabling a department, if BH is open and only linked to that department, it should be closed', async () => {
// disable department
await disableDepartment(deptLinkedToCustomBH);
// verify if BH link is removed
const department = await getDepartmentById(deptLinkedToCustomBH._id);
expect(department).to.be.an('object');
expect(department.businessHourId).to.be.undefined;
// verify if BH is closed
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH.active).to.be.false;
expect(latestCustomBH.departments).to.be.an('array').that.is.empty;
});
it('upon disabling a department, if BH is open and linked to other departments, it should remain open', async () => {
// create another department and link it to the same BH
const { department, agent } = await createDepartmentWithAnOnlineAgent();
await removeAllCustomBusinessHours();
customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]);
// disable department
await disableDepartment(deptLinkedToCustomBH);
// verify if BH link is removed
const disabledDepartment = await getDepartmentById(deptLinkedToCustomBH._id);
expect(disabledDepartment).to.be.an('object');
expect(disabledDepartment.businessHourId).to.be.undefined;
// verify if other department BH link is not removed
const otherDepartment = await getDepartmentById(department._id);
expect(otherDepartment).to.be.an('object');
expect(otherDepartment.businessHourId).to.be.equal(customBusinessHour._id);
// verify if BH is still open
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH).to.have.property('active', true);
expect(latestCustomBH.departments).to.be.an('array').of.length(1);
expect(latestCustomBH?.departments?.[0]._id).to.be.equal(department._id);
// cleanup
await deleteDepartment(department._id);
await deleteUser(agent.user);
});
it('upon disabling a department, agents within the disabled department should be assigned to default BH', async () => {
await openOrCloseBusinessHour(defaultBusinessHour, true);
// disable department
await disableDepartment(deptLinkedToCustomBH);
const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials 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(defaultBusinessHour._id);
});
it('upon disabling a department, overlapping agents should still have BH within their cache', async () => {
// create another department and link it to the same BH
const { department, agent } = await createDepartmentWithAnOnlineAgent();
await removeAllCustomBusinessHours();
customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]);
// create overlapping agent by adding previous agent to newly created department
await addOrRemoveAgentFromDepartment(
department._id,
{
agentId: agentLinkedToDept.user._id,
username: agentLinkedToDept.user.username || '',
},
true,
);
// disable department
await disableDepartment(deptLinkedToCustomBH);
// verify if BH link is removed
const disabledDepartment = await getDepartmentById(deptLinkedToCustomBH._id);
expect(disabledDepartment).to.be.an('object');
expect(disabledDepartment.businessHourId).to.be.undefined;
// verify if BH is still open
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH).to.have.property('active', true);
expect(latestCustomBH.departments).to.be.an('array').of.length(1);
expect(latestCustomBH?.departments?.[0]?._id).to.be.equal(department._id);
// verify if overlapping agent still has BH within his cache
const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials 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(customBusinessHour._id);
// verify if other agent still has BH within his cache
const otherAgent: ILivechatAgent = await getMe(agent.credentials as any);
expect(otherAgent).to.be.an('object');
expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1);
expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id);
// cleanup
await deleteDepartment(department._id);
await deleteUser(agent.user);
});
afterEach(async () => {
await deleteDepartment(deptLinkedToCustomBH._id);
await deleteUser(agentLinkedToDept.user);
});
});
(IS_EE ? describe : describe.skip)('[EE] BH operations post department removal', () => {
let defaultBusinessHour: ILivechatBusinessHour;
let customBusinessHour: ILivechatBusinessHour;
let deptLinkedToCustomBH: ILivechatDepartment;
let agentLinkedToDept: Awaited<ReturnType<typeof createDepartmentWithAnOnlineAgent>>['agent'];
before(async () => {
await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE);
// wait for the callbacks to be registered
await sleep(1000);
});
beforeEach(async () => {
// cleanup any existing business hours
await removeAllCustomBusinessHours();
// get default business hour
defaultBusinessHour = await getDefaultBusinessHour();
// close default business hour
await openOrCloseBusinessHour(defaultBusinessHour, false);
// create custom business hour and link it to a department
const { department, agent } = await createDepartmentWithAnOnlineAgent();
customBusinessHour = await createCustomBusinessHour([department._id]);
agentLinkedToDept = agent;
deptLinkedToCustomBH = department;
// open custom business hour
await openOrCloseBusinessHour(customBusinessHour, true);
});
it('upon deleting a department, if BH is open and only linked to that department, it should be closed', async () => {
await deleteDepartment(deptLinkedToCustomBH._id);
// verify if BH is closed
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH.active).to.be.false;
expect(latestCustomBH.departments).to.be.an('array').that.is.empty;
});
it('upon deleting a department, if BH is open and linked to other departments, it should remain open', async () => {
// create another department and link it to the same BH
const { department, agent } = await createDepartmentWithAnOnlineAgent();
await removeAllCustomBusinessHours();
customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]);
await deleteDepartment(deptLinkedToCustomBH._id);
// verify if other department BH link is not removed
const otherDepartment = await getDepartmentById(department._id);
expect(otherDepartment).to.be.an('object');
expect(otherDepartment.businessHourId).to.be.equal(customBusinessHour._id);
// verify if BH is still open
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH).to.have.property('active', true);
expect(latestCustomBH.departments).to.be.an('array').of.length(1);
expect(latestCustomBH?.departments?.[0]._id).to.be.equal(department._id);
// cleanup
await deleteDepartment(department._id);
await deleteUser(agent.user);
});
it('upon deleting a department, agents within the disabled department should be assigned to default BH', async () => {
await openOrCloseBusinessHour(defaultBusinessHour, true);
await deleteDepartment(deptLinkedToCustomBH._id);
const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials 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(defaultBusinessHour._id);
});
it('upon deleting a department, overlapping agents should still have BH within their cache', async () => {
// create another department and link it to the same BH
const { department, agent } = await createDepartmentWithAnOnlineAgent();
await removeAllCustomBusinessHours();
customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]);
// create overlapping agent by adding previous agent to newly created department
await addOrRemoveAgentFromDepartment(
department._id,
{
agentId: agentLinkedToDept.user._id,
username: agentLinkedToDept.user.username || '',
},
true,
);
await deleteDepartment(deptLinkedToCustomBH._id);
// verify if BH is still open
const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id);
expect(latestCustomBH).to.be.an('object');
expect(latestCustomBH).to.have.property('active', true);
expect(latestCustomBH.departments).to.be.an('array').of.length(1);
expect(latestCustomBH?.departments?.[0]?._id).to.be.equal(department._id);
// verify if overlapping agent still has BH within his cache
const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials 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(customBusinessHour._id);
// verify if other agent still has BH within his cache
const otherAgent: ILivechatAgent = await getMe(agent.credentials as any);
expect(otherAgent).to.be.an('object');
expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1);
expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id);
// cleanup
await deleteDepartment(department._id);
await deleteUser(agent.user);
});
afterEach(async () => {
await deleteUser(agentLinkedToDept.user);
});
});
describe('BH behavior upon new agent creation/deletion', () => {
let defaultBH: ILivechatBusinessHour;
let agent: ILivechatAgent;

@ -39,4 +39,6 @@ export interface ILivechatBusinessHoursModel extends IBaseModel<ILivechatBusines
type?: LivechatBusinessHourTypes,
options?: any,
): Promise<ILivechatBusinessHour[]>;
disableBusinessHour(businessHourId: string): Promise<any>;
}

@ -87,5 +87,7 @@ export interface ILivechatDepartmentAgentsModel extends IBaseModel<ILivechatDepa
getNextBotForDepartment(departmentId: string, ignoreAgentId?: string): Promise<{ agentId: string; username: string } | undefined>;
replaceUsernameOfAgentByUserId(userId: string, username: string): Promise<UpdateResult | Document>;
countByDepartmentId(departmentId: string): Promise<number>;
disableAgentsByDepartmentId(departmentId: string): Promise<UpdateResult | Document>;
enableAgentsByDepartmentId(departmentId: string): Promise<UpdateResult | Document>;
findAllAgentsConnectedToListOfDepartments(departmentIds: string[]): Promise<string[]>;
}

@ -14,6 +14,7 @@ export interface ILivechatDepartmentModel extends IBaseModel<ILivechatDepartment
): FindCursor<ILivechatDepartment>;
findByBusinessHourId(businessHourId: string, options: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
countByBusinessHourIdExcludingDepartmentId(businessHourId: string, departmentId: string): Promise<number>;
findEnabledByBusinessHourId(businessHourId: string, options: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
@ -59,5 +60,13 @@ export interface ILivechatDepartmentModel extends IBaseModel<ILivechatDepartment
findOneByIdOrName(_idOrName: string, options?: FindOptions<ILivechatDepartment>): Promise<ILivechatDepartment | null>;
findByUnitIds(unitIds: string[], options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
findActiveByUnitIds(unitIds: string[], options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
findNotArchived(options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
getBusinessHoursWithDepartmentStatuses(): Promise<
{
_id: string;
validDepartments: string[];
invalidDepartments: string[];
}[]
>;
checkIfMonitorIsMonitoringDepartmentById(monitorId: string, departmentId: string): Promise<boolean>;
}

Loading…
Cancel
Save