feat: Allow managing association to business units on departments' creation and update (#32682)

pull/33296/head^2
Matheus Barbosa Silva 1 year ago committed by GitHub
parent 4202d6570c
commit 9a38c8e13f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/dirty-stingrays-beg.md
  2. 15
      apps/meteor/app/livechat/imports/server/rest/departments.ts
  3. 25
      apps/meteor/app/livechat/server/lib/LivechatTyped.ts
  4. 5
      apps/meteor/app/livechat/server/methods/saveDepartment.ts
  5. 1
      apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts
  6. 53
      apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts
  7. 29
      apps/meteor/ee/server/models/raw/LivechatUnit.ts
  8. 183
      apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts
  9. 2
      apps/meteor/lib/callbacks.ts
  10. 12
      apps/meteor/server/models/raw/LivechatDepartment.ts
  11. 40
      apps/meteor/tests/data/livechat/department.ts
  12. 47
      apps/meteor/tests/data/livechat/rooms.ts
  13. 38
      apps/meteor/tests/data/livechat/units.ts
  14. 556
      apps/meteor/tests/end-to-end/api/livechat/14-units.ts
  15. 1
      packages/core-typings/src/ILivechatDepartment.ts
  16. 3
      packages/model-typings/src/models/ILivechatDepartmentModel.ts
  17. 2
      packages/model-typings/src/models/ILivechatUnitModel.ts
  18. 12
      packages/rest-typings/src/v1/omnichannel.ts

@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/rest-typings": minor
---
Added support for specifying a unit on departments' creation and update

@ -57,10 +57,18 @@ API.v1.addRoute(
check(this.bodyParams, {
department: Object,
agents: Match.Maybe(Array),
departmentUnit: Match.Maybe({ _id: Match.Optional(String) }),
});
const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {};
const department = await LivechatTs.saveDepartment(null, this.bodyParams.department as ILivechatDepartment, agents);
const { departmentUnit } = this.bodyParams;
const department = await LivechatTs.saveDepartment(
this.userId,
null,
this.bodyParams.department as ILivechatDepartment,
agents,
departmentUnit || {},
);
if (department) {
return API.v1.success({
@ -112,17 +120,18 @@ API.v1.addRoute(
check(this.bodyParams, {
department: Object,
agents: Match.Maybe(Array),
departmentUnit: Match.Maybe({ _id: Match.Optional(String) }),
});
const { _id } = this.urlParams;
const { department, agents } = this.bodyParams;
const { department, agents, departmentUnit } = this.bodyParams;
if (!permissionToSave) {
throw new Error('error-not-allowed');
}
const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {};
await LivechatTs.saveDepartment(_id, department, agentParam);
await LivechatTs.saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {});
return API.v1.success({
department: await LivechatDepartment.findOneById(_id),

@ -1789,18 +1789,37 @@ class LivechatClass {
* @param {string|null} _id - The department id
* @param {Partial<import('@rocket.chat/core-typings').ILivechatDepartment>} departmentData
* @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents
* @param {{_id?: string}} [departmentUnit] - The department's unit id
*/
async saveDepartment(
userId: string,
_id: string | null,
departmentData: LivechatDepartmentDTO,
departmentAgents?: {
upsert?: { agentId: string; count?: number; order?: number }[];
remove?: { agentId: string; count?: number; order?: number };
},
departmentUnit?: { _id?: string },
) {
check(_id, Match.Maybe(String));
if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') {
throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', {
method: 'livechat:saveDepartment',
});
}
const department = _id ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null;
const department = _id
? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } })
: null;
if (departmentUnit && !departmentUnit._id && department && department.parentId) {
const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1;
if (isLastDepartmentInUnit) {
throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", {
method: 'livechat:saveDepartment',
});
}
}
if (!department && !(await isDepartmentCreationAvailable())) {
throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', {
@ -1887,6 +1906,10 @@ class LivechatClass {
await callbacks.run('livechat.afterDepartmentDisabled', departmentDB);
}
if (departmentUnit) {
await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id });
}
return departmentDB;
}
}

@ -30,12 +30,13 @@ declare module '@rocket.chat/ddp-client' {
order?: number | undefined;
}[]
| undefined,
departmentUnit?: { _id?: string },
) => ILivechatDepartment;
}
}
Meteor.methods<ServerMethods>({
async 'livechat:saveDepartment'(_id, departmentData, departmentAgents) {
async 'livechat:saveDepartment'(_id, departmentData, departmentAgents, departmentUnit) {
const uid = Meteor.userId();
if (!uid || !(await hasPermissionAsync(uid, 'manage-livechat-departments'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
@ -43,6 +44,6 @@ Meteor.methods<ServerMethods>({
});
}
return Livechat.saveDepartment(_id, departmentData, { upsert: departmentAgents });
return Livechat.saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit);
},
});

@ -26,3 +26,4 @@ import './afterInquiryQueued';
import './sendPdfTranscriptOnClose';
import './applyRoomRestrictions';
import './afterTagRemoved';
import './manageDepartmentUnit';

@ -0,0 +1,53 @@
import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatUnit } from '@rocket.chat/models';
import { hasAnyRoleAsync } from '../../../../../app/authorization/server/functions/hasRole';
import { callbacks } from '../../../../../lib/callbacks';
import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles';
export const manageDepartmentUnit = async ({ userId, departmentId, unitId }: { userId: string; departmentId: string; unitId: string }) => {
const accessibleUnits = await getUnitsFromUser(userId);
const isLivechatManager = await hasAnyRoleAsync(userId, ['admin', 'livechat-manager']);
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'ancestors' | 'parentId'>>(departmentId, {
projection: { ancestors: 1, parentId: 1 },
});
const isDepartmentAlreadyInUnit = unitId && department?.ancestors?.includes(unitId);
if (!department || isDepartmentAlreadyInUnit) {
return;
}
const currentDepartmentUnitId = department.parentId;
const canManageNewUnit = !unitId || isLivechatManager || (Array.isArray(accessibleUnits) && accessibleUnits.includes(unitId));
const canManageCurrentUnit =
!currentDepartmentUnitId || isLivechatManager || (Array.isArray(accessibleUnits) && accessibleUnits.includes(currentDepartmentUnitId));
if (!canManageNewUnit || !canManageCurrentUnit) {
return;
}
if (unitId) {
const unit = await LivechatUnit.findOneById<Pick<IOmnichannelBusinessUnit, '_id' | 'ancestors'>>(unitId, {
projection: { ancestors: 1 },
});
if (!unit) {
return;
}
if (currentDepartmentUnitId) {
await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId);
}
await LivechatDepartment.addDepartmentToUnit(departmentId, unitId, [unitId, ...(unit.ancestors || [])]);
await LivechatUnit.incrementDepartmentsCount(unitId);
return;
}
if (currentDepartmentUnitId) {
await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId);
}
await LivechatDepartment.removeDepartmentFromUnit(departmentId);
};
callbacks.add('livechat.manageDepartmentUnit', manageDepartmentUnit, callbacks.priority.HIGH, 'livechat-manage-department-unit');

@ -11,7 +11,6 @@ const addQueryRestrictions = async (originalQuery: Filter<IOmnichannelBusinessUn
const units = await getUnitsFromUser();
if (Array.isArray(units)) {
query.ancestors = { $in: units };
const expressions = query.$and || [];
const condition = { $or: [{ ancestors: { $in: units } }, { _id: { $in: units } }] };
query.$and = [condition, ...expressions];
@ -109,28 +108,12 @@ export class LivechatUnitRaw extends BaseRaw<IOmnichannelBusinessUnit> implement
// remove other departments
for await (const departmentId of savedDepartments) {
if (!departmentsToSave.includes(departmentId)) {
await LivechatDepartment.updateOne(
{ _id: departmentId },
{
$set: {
parentId: null,
ancestors: null,
},
},
);
await LivechatDepartment.removeDepartmentFromUnit(departmentId);
}
}
for await (const departmentId of departmentsToSave) {
await LivechatDepartment.updateOne(
{ _id: departmentId },
{
$set: {
parentId: _id,
ancestors,
},
},
);
await LivechatDepartment.addDepartmentToUnit(departmentId, _id, ancestors);
}
await LivechatRooms.associateRoomsWithDepartmentToUnit(departmentsToSave, _id);
@ -154,6 +137,14 @@ export class LivechatUnitRaw extends BaseRaw<IOmnichannelBusinessUnit> implement
return this.updateMany(query, update);
}
incrementDepartmentsCount(_id: string): Promise<UpdateResult | Document> {
return this.updateOne({ _id }, { $inc: { numDepartments: 1 } });
}
decrementDepartmentsCount(_id: string): Promise<UpdateResult | Document> {
return this.updateOne({ _id }, { $inc: { numDepartments: -1 } });
}
async removeById(_id: string): Promise<DeleteResult> {
await LivechatUnitMonitors.removeByUnitId(_id);
await this.removeParentAndAncestorById(_id);

@ -0,0 +1,183 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const livechatDepartmentStub = {
findOneById: sinon.stub(),
addDepartmentToUnit: sinon.stub(),
removeDepartmentFromUnit: sinon.stub(),
};
const livechatUnitStub = {
findOneById: sinon.stub(),
decrementDepartmentsCount: sinon.stub(),
incrementDepartmentsCount: sinon.stub(),
};
const hasAnyRoleStub = sinon.stub();
const getUnitsFromUserStub = sinon.stub();
const { manageDepartmentUnit } = proxyquire
.noCallThru()
.load('../../../../../../app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts', {
'../methods/getUnitsFromUserRoles': {
getUnitsFromUser: getUnitsFromUserStub,
},
'../../../../../app/authorization/server/functions/hasRole': {
hasAnyRoleAsync: hasAnyRoleStub,
},
'@rocket.chat/models': {
LivechatDepartment: livechatDepartmentStub,
LivechatUnit: livechatUnitStub,
},
});
describe('hooks/manageDepartmentUnit', () => {
beforeEach(() => {
livechatDepartmentStub.findOneById.reset();
livechatDepartmentStub.addDepartmentToUnit.reset();
livechatDepartmentStub.removeDepartmentFromUnit.reset();
livechatUnitStub.findOneById.reset();
livechatUnitStub.decrementDepartmentsCount.reset();
livechatUnitStub.incrementDepartmentsCount.reset();
hasAnyRoleStub.reset();
});
it('should not perform any operation when an invalid department is provided', async () => {
livechatDepartmentStub.findOneById.resolves(null);
hasAnyRoleStub.resolves(true);
getUnitsFromUserStub.resolves(['unit-id']);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true;
});
it('should not perform any operation if the provided department is already in unit', async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' });
hasAnyRoleStub.resolves(true);
getUnitsFromUserStub.resolves(['unit-id']);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true;
});
it("should not perform any operation if user is a monitor and can't manage new unit", async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' });
hasAnyRoleStub.resolves(false);
getUnitsFromUserStub.resolves(['unit-id']);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true;
});
it("should not perform any operation if user is a monitor and can't manage current unit", async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' });
hasAnyRoleStub.resolves(false);
getUnitsFromUserStub.resolves(['new-unit-id']);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true;
});
it('should not perform any operation if user is an admin/manager but an invalid unit is provided', async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' });
livechatUnitStub.findOneById.resolves(undefined);
hasAnyRoleStub.resolves(true);
getUnitsFromUserStub.resolves(undefined);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true;
});
it('should remove department from its current unit if user is an admin/manager', async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' });
hasAnyRoleStub.resolves(true);
getUnitsFromUserStub.resolves(undefined);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: undefined });
expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.calledOnceWith('department-id')).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true;
});
it('should add department to a unit if user is an admin/manager', async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id' });
livechatUnitStub.findOneById.resolves({ _id: 'unit-id' });
hasAnyRoleStub.resolves(true);
getUnitsFromUserStub.resolves(undefined);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'unit-id', ['unit-id'])).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true;
});
it('should move department to a new unit if user is an admin/manager', async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' });
livechatUnitStub.findOneById.resolves({ _id: 'new-unit-id' });
hasAnyRoleStub.resolves(true);
getUnitsFromUserStub.resolves(undefined);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'new-unit-id', ['new-unit-id'])).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('new-unit-id')).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true;
});
it('should remove department from its current unit if user is a monitor that supervises the current unit', async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' });
hasAnyRoleStub.resolves(false);
getUnitsFromUserStub.resolves(['unit-id']);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: undefined });
expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.calledOnceWith('department-id')).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true;
});
it('should add department to a unit if user is a monitor that supervises the new unit', async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id' });
livechatUnitStub.findOneById.resolves({ _id: 'unit-id' });
hasAnyRoleStub.resolves(false);
getUnitsFromUserStub.resolves(['unit-id']);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'unit-id', ['unit-id'])).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true;
});
it('should move department to a new unit if user is a monitor that supervises the current and new units', async () => {
livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' });
livechatUnitStub.findOneById.resolves({ _id: 'unit-id' });
hasAnyRoleStub.resolves(false);
getUnitsFromUserStub.resolves(['unit-id', 'new-unit-id']);
await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' });
expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'new-unit-id', ['new-unit-id'])).to.be.true;
expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('new-unit-id')).to.be.true;
expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true;
expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true;
});
});

@ -225,6 +225,7 @@ type ChainedCallbackSignatures = {
'unarchiveRoom': (room: IRoom) => void;
'roomAvatarChanged': (room: IRoom) => void;
'beforeGetMentions': (mentionIds: string[], teamMentions: MessageMention[]) => Promise<string[]>;
'livechat.manageDepartmentUnit': (params: { userId: string; departmentId: string; unitId?: string }) => void;
};
export type Hook =
@ -247,6 +248,7 @@ export type Hook =
| 'livechat.offlineMessage'
| 'livechat.onCheckRoomApiParams'
| 'livechat.onLoadConfigApi'
| 'livechat.manageDepartmentUnit'
| 'loginPageStateChange'
| 'mapLDAPUserData'
| 'onCreateUser'

@ -222,6 +222,14 @@ export class LivechatDepartmentRaw extends BaseRaw<ILivechatDepartment> implemen
return this.updateOne({ _id }, { $set: { archived: true, enabled: false } });
}
addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise<Document | UpdateResult> {
return this.updateOne({ _id }, { $set: { parentId: unitId, ancestors } });
}
removeDepartmentFromUnit(_id: string): Promise<Document | UpdateResult> {
return this.updateOne({ _id }, { $set: { parentId: null, ancestors: null } });
}
async createOrUpdateDepartment(
_id: string | null,
data: {
@ -328,6 +336,10 @@ export class LivechatDepartmentRaw extends BaseRaw<ILivechatDepartment> implemen
return this.find(query, options);
}
countDepartmentsInUnit(unitId: string): Promise<number> {
return this.countDocuments({ parentId: unitId });
}
findActiveByUnitIds(unitIds: string[], options: FindOptions<ILivechatDepartment> = {}): FindCursor<ILivechatDepartment> {
const query = {
enabled: true,

@ -42,36 +42,44 @@ const updateDepartment = async (departmentId: string, departmentData: Partial<Li
return response.body.department;
};
const createDepartmentWithMethod = (
initialAgents: { agentId: string; username: string }[] = [],
{
allowReceiveForwardOffline = false,
fallbackForwardDepartment,
}: {
allowReceiveForwardOffline?: boolean;
fallbackForwardDepartment?: string;
} = {},
) =>
export const createDepartmentWithMethod = ({
initialAgents = [],
allowReceiveForwardOffline = false,
fallbackForwardDepartment,
name,
departmentUnit,
userCredentials = credentials,
departmentId = '',
}: {
initialAgents?: { agentId: string; username: string }[];
allowReceiveForwardOffline?: boolean;
fallbackForwardDepartment?: string;
name?: string;
departmentUnit?: { _id?: string };
userCredentials?: Credentials;
departmentId?: string;
} = {}): Promise<ILivechatDepartment> =>
new Promise((resolve, reject) => {
void request
.post(methodCall('livechat:saveDepartment'))
.set(credentials)
.set(userCredentials)
.send({
message: JSON.stringify({
method: 'livechat:saveDepartment',
params: [
'',
departmentId,
{
enabled: true,
email: faker.internet.email(),
showOnRegistration: true,
showOnOfflineForm: true,
name: `new department ${Date.now()}`,
name: name || `new department ${Date.now()}`,
description: 'created from api',
allowReceiveForwardOffline,
fallbackForwardDepartment,
},
initialAgents,
departmentUnit,
],
id: 'id',
msg: 'method',
@ -93,7 +101,7 @@ type OnlineAgent = {
export const createDepartmentWithAnOnlineAgent = async (): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => {
const { user, credentials } = await createAnOnlineAgent();
const department = (await createDepartmentWithMethod()) as ILivechatDepartment;
const department = await createDepartmentWithMethod();
await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true);
@ -108,7 +116,7 @@ export const createDepartmentWithAnOnlineAgent = async (): Promise<{ department:
export const createDepartmentWithAgent = async (agent: OnlineAgent): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => {
const { user, credentials } = agent;
const department = (await createDepartmentWithMethod()) as ILivechatDepartment;
const department = await createDepartmentWithMethod();
await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true);
@ -153,7 +161,7 @@ export const createDepartmentWithAnOfflineAgent = async ({
}> => {
const { user, credentials } = await createAnOfflineAgent();
const department = (await createDepartmentWithMethod(undefined, {
const department = (await createDepartmentWithMethod({
allowReceiveForwardOffline,
fallbackForwardDepartment,
})) as ILivechatDepartment;

@ -98,11 +98,55 @@ export const createDepartment = (
name?: string,
enabled = true,
opts: Record<string, any> = {},
departmentUnit?: { _id?: string },
userCredentials: Credentials = credentials,
): Promise<ILivechatDepartment> => {
return new Promise((resolve, reject) => {
void request
.post(api('livechat/department'))
.set(credentials)
.set(userCredentials)
.send({
department: {
name: name || `Department ${Date.now()}`,
enabled,
showOnOfflineForm: true,
showOnRegistration: true,
email: 'a@b.com',
...opts,
},
agents,
departmentUnit,
})
.end((err: Error, res: DummyResponse<ILivechatDepartment>) => {
if (err) {
return reject(err);
}
resolve(res.body.department);
});
});
};
export const updateDepartment = ({
departmentId,
userCredentials,
agents,
name,
enabled = true,
opts = {},
departmentUnit,
}: {
departmentId: string;
userCredentials: Credentials;
agents?: { agentId: string }[];
name?: string;
enabled?: boolean;
opts?: Record<string, any>;
departmentUnit?: { _id?: string };
}): Promise<ILivechatDepartment> => {
return new Promise((resolve, reject) => {
void request
.put(api(`livechat/department/${departmentId}`))
.set(userCredentials)
.send({
department: {
name: name || `Department ${Date.now()}`,
@ -113,6 +157,7 @@ export const createDepartment = (
...opts,
},
agents,
departmentUnit,
})
.end((err: Error, res: DummyResponse<ILivechatDepartment>) => {
if (err) {

@ -1,7 +1,7 @@
import { faker } from '@faker-js/faker';
import type { IOmnichannelBusinessUnit } from '@rocket.chat/core-typings';
import { methodCall, credentials, request } from '../api-data';
import { methodCall, credentials, request, api } from '../api-data';
import type { DummyResponse } from './utils';
export const createMonitor = async (username: string): Promise<{ _id: string; username: string }> => {
@ -57,3 +57,39 @@ export const createUnit = async (
});
});
};
export const deleteUnit = async (unit: IOmnichannelBusinessUnit): Promise<IOmnichannelBusinessUnit> => {
return new Promise((resolve, reject) => {
void request
.post(methodCall(`livechat:removeUnit`))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:removeUnit',
params: [unit._id],
id: '101',
msg: 'method',
}),
})
.end((err: Error, res: DummyResponse<string, 'wrapped'>) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(res.body.message).result);
});
});
};
export const getUnit = (unitId: string): Promise<IOmnichannelBusinessUnit> => {
return new Promise((resolve, reject) => {
void request
.get(api(`livechat/units/${unitId}`))
.set(credentials)
.end((err: Error, res: DummyResponse<IOmnichannelBusinessUnit, 'not-wrapped'>) => {
if (err) {
return reject(err);
}
resolve(res.body);
});
});
};

@ -1,12 +1,14 @@
import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import { before, after, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createDepartment } from '../../../data/livechat/rooms';
import { createMonitor, createUnit } from '../../../data/livechat/units';
import { getCredentials, api, request, credentials, methodCall } from '../../../data/api-data';
import { deleteDepartment, getDepartmentById, createDepartmentWithMethod } from '../../../data/livechat/department';
import { createDepartment, updateDepartment } from '../../../data/livechat/rooms';
import { createMonitor, createUnit, deleteUnit, getUnit } from '../../../data/livechat/units';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
import { createUser, deleteUser } from '../../../data/users.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, login } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Units', () => {
@ -14,6 +16,7 @@ import { IS_EE } from '../../../e2e/config/constants';
before(async () => {
await updateSetting('Livechat_enabled', true);
await updatePermission('manage-livechat-departments', ['livechat-manager', 'livechat-monitor', 'admin']);
});
describe('[GET] livechat/units', () => {
@ -409,4 +412,547 @@ import { IS_EE } from '../../../e2e/config/constants';
await deleteUser(user);
});
});
describe('[POST] livechat/department', () => {
let monitor1: Awaited<ReturnType<typeof createUser>>;
let monitor1Credentials: Awaited<ReturnType<typeof login>>;
let monitor2: Awaited<ReturnType<typeof createUser>>;
let monitor2Credentials: Awaited<ReturnType<typeof login>>;
let unit: IOmnichannelBusinessUnit;
before(async () => {
monitor1 = await createUser();
monitor2 = await createUser();
await createMonitor(monitor1.username);
monitor1Credentials = await login(monitor1.username, password);
await createMonitor(monitor2.username);
monitor2Credentials = await login(monitor2.username, password);
unit = await createUnit(monitor1._id, monitor1.username, []);
});
after(async () => Promise.all([deleteUser(monitor1), deleteUser(monitor2), deleteUnit(unit)]));
it('should fail creating department when providing an invalid property in the department unit object', () => {
return request
.post(api('livechat/department'))
.set(credentials)
.send({
department: { name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
departmentUnit: { invalidProperty: true },
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
});
it('should fail creating a department into an existing unit that a monitor does not supervise', async () => {
const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor2Credentials);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 0);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.not.have.property('parentId');
expect(fullDepartment).to.not.have.property('ancestors');
await deleteDepartment(department._id);
});
it('should succesfully create a department into an existing unit as an admin', async () => {
const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id });
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
await deleteDepartment(department._id);
});
it('should succesfully create a department into an existing unit that a monitor supervises', async () => {
const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor1Credentials);
// Deleting a department currently does not decrease its unit's counter. We must adjust this check when this is fixed
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
await deleteDepartment(department._id);
});
});
describe('[PUT] livechat/department', () => {
let monitor1: Awaited<ReturnType<typeof createUser>>;
let monitor1Credentials: Awaited<ReturnType<typeof login>>;
let monitor2: Awaited<ReturnType<typeof createUser>>;
let monitor2Credentials: Awaited<ReturnType<typeof login>>;
let unit: IOmnichannelBusinessUnit;
let department: ILivechatDepartment;
let baseDepartment: ILivechatDepartment;
before(async () => {
monitor1 = await createUser();
monitor2 = await createUser();
await createMonitor(monitor1.username);
monitor1Credentials = await login(monitor1.username, password);
await createMonitor(monitor2.username);
monitor2Credentials = await login(monitor2.username, password);
department = await createDepartment();
baseDepartment = await createDepartment();
unit = await createUnit(monitor1._id, monitor1.username, [baseDepartment._id]);
});
after(async () =>
Promise.all([
deleteUser(monitor1),
deleteUser(monitor2),
deleteUnit(unit),
deleteDepartment(department._id),
deleteDepartment(baseDepartment._id),
]),
);
it("should fail updating a department's unit when providing an invalid property in the department unit object", () => {
const updatedName = 'updated-department-name';
return request
.put(api(`livechat/department/${department._id}`))
.set(credentials)
.send({
department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
departmentUnit: { invalidProperty: true },
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Match error: Unknown key in field departmentUnit.invalidProperty');
});
});
it("should fail updating a department's unit when providing an invalid _id type in the department unit object", () => {
const updatedName = 'updated-department-name';
return request
.put(api(`livechat/department/${department._id}`))
.set(credentials)
.send({
department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
departmentUnit: { _id: true },
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Match error: Expected string, got boolean in field departmentUnit._id');
});
});
it('should fail removing the last department from a unit', () => {
const updatedName = 'updated-department-name';
return request
.put(api(`livechat/department/${baseDepartment._id}`))
.set(credentials)
.send({
department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
departmentUnit: {},
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-unit-cant-be-empty');
});
});
it('should succesfully add an existing department to a unit as an admin', async () => {
const updatedName = 'updated-department-name';
const updatedDepartment = await updateDepartment({
userCredentials: credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: { _id: unit._id },
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should succesfully remove an existing department from a unit as an admin', async () => {
const updatedName = 'updated-department-name';
const updatedDepartment = await updateDepartment({
userCredentials: credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: {},
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('parentId').that.is.null;
expect(fullDepartment).to.have.property('ancestors').that.is.null;
});
it('should fail adding a department into an existing unit that a monitor does not supervise', async () => {
const updatedName = 'updated-department-name2';
const updatedDepartment = await updateDepartment({
userCredentials: monitor2Credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: { _id: unit._id },
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('parentId').that.is.null;
expect(fullDepartment).to.have.property('ancestors').that.is.null;
});
it('should succesfully add a department into an existing unit that a monitor supervises', async () => {
const updatedName = 'updated-department-name3';
const updatedDepartment = await updateDepartment({
userCredentials: monitor1Credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: { _id: unit._id },
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('name', updatedName);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should fail removing a department from a unit that a monitor does not supervise', async () => {
const updatedName = 'updated-department-name4';
const updatedDepartment = await updateDepartment({
userCredentials: monitor2Credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: {},
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('name', updatedName);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should succesfully remove a department from a unit that a monitor supervises', async () => {
const updatedName = 'updated-department-name5';
const updatedDepartment = await updateDepartment({
userCredentials: monitor1Credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: {},
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('name', updatedName);
expect(fullDepartment).to.have.property('parentId').that.is.null;
expect(fullDepartment).to.have.property('ancestors').that.is.null;
});
});
describe('[POST] livechat:saveDepartment', () => {
let monitor1: Awaited<ReturnType<typeof createUser>>;
let monitor1Credentials: Awaited<ReturnType<typeof login>>;
let monitor2: Awaited<ReturnType<typeof createUser>>;
let monitor2Credentials: Awaited<ReturnType<typeof login>>;
let unit: IOmnichannelBusinessUnit;
const departmentName = 'Test-Department-Livechat-Method';
let testDepartmentId = '';
let baseDepartment: ILivechatDepartment;
before(async () => {
monitor1 = await createUser();
monitor2 = await createUser();
await createMonitor(monitor1.username);
monitor1Credentials = await login(monitor1.username, password);
await createMonitor(monitor2.username);
monitor2Credentials = await login(monitor2.username, password);
baseDepartment = await createDepartment();
unit = await createUnit(monitor1._id, monitor1.username, [baseDepartment._id]);
});
after(async () =>
Promise.all([
deleteUser(monitor1),
deleteUser(monitor2),
deleteUnit(unit),
deleteDepartment(testDepartmentId),
deleteDepartment(baseDepartment._id),
]),
);
it('should fail creating department when providing an invalid _id type in the department unit object', () => {
return request
.post(methodCall('livechat:saveDepartment'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveDepartment',
params: [
'',
{ name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
[],
{ _id: true },
],
id: 'id',
msg: 'method',
}),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message').that.is.a('string');
const data = JSON.parse(res.body.message);
expect(data).to.have.property('error').that.is.an('object');
expect(data.error).to.have.property('errorType', 'Meteor.Error');
expect(data.error).to.have.property('error', 'error-invalid-department-unit');
});
});
it('should fail removing last department from unit', () => {
return request
.post(methodCall('livechat:saveDepartment'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveDepartment',
params: [
baseDepartment._id,
{ name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
[],
{},
],
id: 'id',
msg: 'method',
}),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message').that.is.a('string');
const data = JSON.parse(res.body.message);
expect(data).to.have.property('error').that.is.an('object');
expect(data.error).to.have.property('errorType', 'Meteor.Error');
expect(data.error).to.have.property('error', 'error-unit-cant-be-empty');
});
});
it('should fail creating a department into an existing unit that a monitor does not supervise', async () => {
const departmentName = 'Fail-Test';
const department = await createDepartmentWithMethod({
userCredentials: monitor2Credentials,
name: departmentName,
departmentUnit: { _id: unit._id },
});
testDepartmentId = department._id;
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(testDepartmentId);
expect(fullDepartment).to.not.have.property('parentId');
expect(fullDepartment).to.not.have.property('ancestors');
await deleteDepartment(testDepartmentId);
});
it('should succesfully create a department into an existing unit as an admin', async () => {
const testDepartment = await createDepartmentWithMethod({ name: departmentName, departmentUnit: { _id: unit._id } });
testDepartmentId = testDepartment._id;
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(testDepartmentId);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should succesfully remove an existing department from a unit as an admin', async () => {
await createDepartmentWithMethod({ name: departmentName, departmentUnit: {}, departmentId: testDepartmentId });
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(testDepartmentId);
expect(fullDepartment).to.have.property('parentId').that.is.null;
expect(fullDepartment).to.have.property('ancestors').that.is.null;
});
it('should succesfully add an existing department to a unit as an admin', async () => {
await createDepartmentWithMethod({ name: departmentName, departmentUnit: { _id: unit._id }, departmentId: testDepartmentId });
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(testDepartmentId);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should succesfully remove a department from a unit that a monitor supervises', async () => {
await createDepartmentWithMethod({
name: departmentName,
departmentUnit: {},
departmentId: testDepartmentId,
userCredentials: monitor1Credentials,
});
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(testDepartmentId);
expect(fullDepartment).to.have.property('parentId').that.is.null;
expect(fullDepartment).to.have.property('ancestors').that.is.null;
});
it('should succesfully add an existing department to a unit that a monitor supervises', async () => {
await createDepartmentWithMethod({
name: departmentName,
departmentUnit: { _id: unit._id },
departmentId: testDepartmentId,
userCredentials: monitor1Credentials,
});
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(testDepartmentId);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should fail removing a department from a unit that a monitor does not supervise', async () => {
await createDepartmentWithMethod({
name: departmentName,
departmentUnit: {},
departmentId: testDepartmentId,
userCredentials: monitor2Credentials,
});
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(testDepartmentId);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
await deleteDepartment(testDepartmentId);
});
it('should succesfully create a department in a unit that a monitor supervises', async () => {
const testDepartment = await createDepartmentWithMethod({
name: departmentName,
departmentUnit: { _id: unit._id },
userCredentials: monitor1Credentials,
});
testDepartmentId = testDepartment._id;
// Deleting a department currently does not decrease its unit's counter. We must adjust this check when this is fixed
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 3);
const fullDepartment = await getDepartmentById(testDepartmentId);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
});
});

@ -16,6 +16,7 @@ export interface ILivechatDepartment {
archived?: boolean;
departmentsAllowedToForward?: string[];
maxNumberSimultaneousChat?: number;
parentId?: string;
ancestors?: string[];
allowReceiveForwardOffline?: boolean;
// extra optional fields

@ -59,6 +59,7 @@ export interface ILivechatDepartmentModel extends IBaseModel<ILivechatDepartment
): Promise<FindCursor<ILivechatDepartment>>;
findOneByIdOrName(_idOrName: string, options?: FindOptions<ILivechatDepartment>): Promise<ILivechatDepartment | null>;
findByUnitIds(unitIds: string[], options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
countDepartmentsInUnit(unitId: string): Promise<number>;
findActiveByUnitIds(unitIds: string[], options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
findNotArchived(options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
getBusinessHoursWithDepartmentStatuses(): Promise<
@ -73,4 +74,6 @@ export interface ILivechatDepartmentModel extends IBaseModel<ILivechatDepartment
findEnabledInIds(departmentsIds: string[], options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
archiveDepartment(_id: string): Promise<Document | UpdateResult>;
unarchiveDepartment(_id: string): Promise<Document | UpdateResult>;
addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise<Document | UpdateResult>;
removeDepartmentFromUnit(_id: string): Promise<Document | UpdateResult>;
}

@ -32,6 +32,8 @@ export interface ILivechatUnitModel extends IBaseModel<IOmnichannelBusinessUnit>
departments: { departmentId: string }[],
): Promise<Omit<IOmnichannelBusinessUnit, '_updatedAt'>>;
removeParentAndAncestorById(parentId: string): Promise<UpdateResult | Document>;
incrementDepartmentsCount(_id: string): Promise<UpdateResult | Document>;
decrementDepartmentsCount(_id: string): Promise<UpdateResult | Document>;
removeById(_id: string): Promise<DeleteResult>;
findOneByIdOrName(_idOrName: string, options: FindOptions<IOmnichannelBusinessUnit>): Promise<IOmnichannelBusinessUnit | null>;
findByMonitorId(monitorId: string): Promise<string[]>;

@ -576,7 +576,8 @@ type POSTLivechatDepartmentProps = {
chatClosingTags?: string[];
fallbackForwardDepartment?: string;
};
agents: { agentId: string; count?: number; order?: number }[];
agents?: { agentId: string; count?: number; order?: number }[];
departmentUnit?: { _id?: string };
};
const POSTLivechatDepartmentSchema = {
@ -645,6 +646,15 @@ const POSTLivechatDepartmentSchema = {
},
nullable: true,
},
departmentUnit: {
type: 'object',
properties: {
_id: {
type: 'string',
},
},
additionalProperties: false,
},
},
required: ['department'],
additionalProperties: false,

Loading…
Cancel
Save