From 9a38c8e13f925d88ece6955333773bce45ba7536 Mon Sep 17 00:00:00 2001
From: Matheus Barbosa Silva
 <36537004+matheusbsilva137@users.noreply.github.com>
Date: Wed, 18 Sep 2024 09:31:32 -0300
Subject: [PATCH] feat: Allow managing association to business units on
 departments' creation and update (#32682)

---
 .changeset/dirty-stingrays-beg.md             |   7 +
 .../imports/server/rest/departments.ts        |  15 +-
 .../app/livechat/server/lib/LivechatTyped.ts  |  25 +-
 .../livechat/server/methods/saveDepartment.ts |   5 +-
 .../livechat-enterprise/server/hooks/index.ts |   1 +
 .../server/hooks/manageDepartmentUnit.ts      |  53 ++
 .../ee/server/models/raw/LivechatUnit.ts      |  29 +-
 .../server/hooks/manageDepartmentUnit.spec.ts | 183 ++++++
 apps/meteor/lib/callbacks.ts                  |   2 +
 .../server/models/raw/LivechatDepartment.ts   |  12 +
 apps/meteor/tests/data/livechat/department.ts |  40 +-
 apps/meteor/tests/data/livechat/rooms.ts      |  47 +-
 apps/meteor/tests/data/livechat/units.ts      |  38 +-
 .../tests/end-to-end/api/livechat/14-units.ts | 556 +++++++++++++++++-
 .../core-typings/src/ILivechatDepartment.ts   |   1 +
 .../src/models/ILivechatDepartmentModel.ts    |   3 +
 .../src/models/ILivechatUnitModel.ts          |   2 +
 packages/rest-typings/src/v1/omnichannel.ts   |  12 +-
 18 files changed, 982 insertions(+), 49 deletions(-)
 create mode 100644 .changeset/dirty-stingrays-beg.md
 create mode 100644 apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts
 create mode 100644 apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts

diff --git a/.changeset/dirty-stingrays-beg.md b/.changeset/dirty-stingrays-beg.md
new file mode 100644
index 00000000000..cf5e3a4ca83
--- /dev/null
+++ b/.changeset/dirty-stingrays-beg.md
@@ -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
diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts
index 252a8385570..e56feeac2fa 100644
--- a/apps/meteor/app/livechat/imports/server/rest/departments.ts
+++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts
@@ -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),
diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
index 89d12503397..ade6726336e 100644
--- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
+++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
@@ -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;
 	}
 }
diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts
index b4833523ab3..659f85f4994 100644
--- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts
+++ b/apps/meteor/app/livechat/server/methods/saveDepartment.ts
@@ -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);
 	},
 });
diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts
index c5bf0a5aa39..a4b66087be2 100644
--- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts
+++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts
@@ -26,3 +26,4 @@ import './afterInquiryQueued';
 import './sendPdfTranscriptOnClose';
 import './applyRoomRestrictions';
 import './afterTagRemoved';
+import './manageDepartmentUnit';
diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts
new file mode 100644
index 00000000000..7de7ef0d6bf
--- /dev/null
+++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts
@@ -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');
diff --git a/apps/meteor/ee/server/models/raw/LivechatUnit.ts b/apps/meteor/ee/server/models/raw/LivechatUnit.ts
index fcabf12fa4f..c198ee04fbb 100644
--- a/apps/meteor/ee/server/models/raw/LivechatUnit.ts
+++ b/apps/meteor/ee/server/models/raw/LivechatUnit.ts
@@ -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);
diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts
new file mode 100644
index 00000000000..8fbf0dcf97a
--- /dev/null
+++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts
@@ -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;
+	});
+});
diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts
index 7eaa9ed7595..57b8527d500 100644
--- a/apps/meteor/lib/callbacks.ts
+++ b/apps/meteor/lib/callbacks.ts
@@ -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'
diff --git a/apps/meteor/server/models/raw/LivechatDepartment.ts b/apps/meteor/server/models/raw/LivechatDepartment.ts
index b8263af030a..9ecee34df5e 100644
--- a/apps/meteor/server/models/raw/LivechatDepartment.ts
+++ b/apps/meteor/server/models/raw/LivechatDepartment.ts
@@ -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,
diff --git a/apps/meteor/tests/data/livechat/department.ts b/apps/meteor/tests/data/livechat/department.ts
index 72ab0af9f26..d7f22fca970 100644
--- a/apps/meteor/tests/data/livechat/department.ts
+++ b/apps/meteor/tests/data/livechat/department.ts
@@ -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;
diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts
index b5d89762c61..46e5cbe489a 100644
--- a/apps/meteor/tests/data/livechat/rooms.ts
+++ b/apps/meteor/tests/data/livechat/rooms.ts
@@ -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) {
diff --git a/apps/meteor/tests/data/livechat/units.ts b/apps/meteor/tests/data/livechat/units.ts
index 03ea578e654..8a2d0f5a833 100644
--- a/apps/meteor/tests/data/livechat/units.ts
+++ b/apps/meteor/tests/data/livechat/units.ts
@@ -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);
+			});
+	});
+};
diff --git a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts
index 425c776fecd..e0c079ece24 100644
--- a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts
+++ b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts
@@ -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);
+		});
+	});
 });
diff --git a/packages/core-typings/src/ILivechatDepartment.ts b/packages/core-typings/src/ILivechatDepartment.ts
index a73cf55cb23..0138a88226f 100644
--- a/packages/core-typings/src/ILivechatDepartment.ts
+++ b/packages/core-typings/src/ILivechatDepartment.ts
@@ -16,6 +16,7 @@ export interface ILivechatDepartment {
 	archived?: boolean;
 	departmentsAllowedToForward?: string[];
 	maxNumberSimultaneousChat?: number;
+	parentId?: string;
 	ancestors?: string[];
 	allowReceiveForwardOffline?: boolean;
 	// extra optional fields
diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts
index 75fe0f54b2e..fe366256eff 100644
--- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts
+++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts
@@ -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>;
 }
diff --git a/packages/model-typings/src/models/ILivechatUnitModel.ts b/packages/model-typings/src/models/ILivechatUnitModel.ts
index 8858ee5b580..24a482eccd0 100644
--- a/packages/model-typings/src/models/ILivechatUnitModel.ts
+++ b/packages/model-typings/src/models/ILivechatUnitModel.ts
@@ -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[]>;
diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts
index a1c714c013b..b494e5d0e5a 100644
--- a/packages/rest-typings/src/v1/omnichannel.ts
+++ b/packages/rest-typings/src/v1/omnichannel.ts
@@ -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,
-- 
GitLab