diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 3a76c7ce06fa..cc32d2909f21 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -12,6 +12,7 @@ import { findDepartmentsBetweenIds, findDepartmentAgents, } from '../../../server/api/lib/departments'; +import { DepartmentHelper } from '../../../server/lib/Departments'; API.v1.addRoute( 'livechat/department', @@ -133,15 +134,14 @@ API.v1.addRoute( return API.v1.failure(); }, - delete() { + async delete() { check(this.urlParams, { _id: String, }); - if (Livechat.removeDepartment(this.urlParams._id)) { - return API.v1.success(); - } - return API.v1.failure(); + await DepartmentHelper.removeDepartment(this.urlParams._id); + + return API.v1.success(); }, }, ); diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts new file mode 100644 index 000000000000..ec9e66093470 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/Departments.ts @@ -0,0 +1,56 @@ +import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; +import { Logger } from '../../../logger/server'; + +class DepartmentHelperClass { + logger = new Logger('Omnichannel:DepartmentHelper'); + + async removeDepartment(departmentId: string) { + this.logger.debug(`Removing department: ${departmentId}`); + + const department = await LivechatDepartment.findOneById(departmentId); + if (!department) { + this.logger.debug(`Department not found: ${departmentId}`); + throw new Error('error-department-not-found'); + } + + const { _id } = department; + + const ret = await LivechatDepartment.removeById(_id); + if (ret.acknowledged !== true) { + this.logger.error(`Department record not removed: ${_id}. Result from db: ${ret}`); + throw new Error('error-failed-to-delete-department'); + } + this.logger.debug(`Department record removed: ${_id}`); + + const agentsIds: string[] = await LivechatDepartmentAgents.findAgentsByDepartmentId(department._id) + .cursor.map((agent) => agent.agentId) + .toArray(); + + this.logger.debug( + `Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`, + ); + + const promiseResponses = await Promise.allSettled([ + LivechatDepartmentAgents.removeByDepartmentId(_id), + LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id), + LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id), + ]); + promiseResponses.forEach((response, index) => { + if (response.status === 'rejected') { + this.logger.error(`Error while performing post-department-removal actions: ${_id}. Action No: ${index}. Error:`, response.reason); + } + }); + + this.logger.debug(`Post-department-removal actions completed: ${_id}. Notifying callbacks with department and agentsIds`); + + Meteor.defer(() => { + callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); + }); + + return ret; + } +} + +export const DepartmentHelper = new DepartmentHelperClass(); diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 47ae2de29551..6d32b0c2e834 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -1129,30 +1129,6 @@ export const Livechat = { return true; }, - removeDepartment(_id) { - check(_id, String); - - const department = LivechatDepartment.findOneById(_id, { fields: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - const ret = LivechatDepartment.removeById(_id); - const agentsIds = LivechatDepartmentAgents.findByDepartmentId(_id) - .fetch() - .map((agent) => agent.agentId); - LivechatDepartmentAgents.removeByDepartmentId(_id); - LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id); - if (ret) { - Meteor.defer(() => { - callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); - }); - } - return ret; - }, - showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; diff --git a/apps/meteor/app/livechat/server/methods/removeDepartment.js b/apps/meteor/app/livechat/server/methods/removeDepartment.js index 226fb1153376..c4d64ee25c12 100644 --- a/apps/meteor/app/livechat/server/methods/removeDepartment.js +++ b/apps/meteor/app/livechat/server/methods/removeDepartment.js @@ -1,19 +1,22 @@ import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { Livechat } from '../lib/Livechat'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { DepartmentHelper } from '../lib/Departments'; Meteor.methods({ 'livechat:removeDepartment'(_id) { methodDeprecationLogger.warn('livechat:removeDepartment will be deprecated in future versions of Rocket.Chat'); + check(_id, String); + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-departments')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeDepartment', }); } - return Livechat.removeDepartment(_id); + return DepartmentHelper.removeDepartment(_id); }, }); diff --git a/apps/meteor/app/models/server/models/LivechatDepartment.js b/apps/meteor/app/models/server/models/LivechatDepartment.js index 4f8a22fa04b0..4bed4d175dfd 100644 --- a/apps/meteor/app/models/server/models/LivechatDepartment.js +++ b/apps/meteor/app/models/server/models/LivechatDepartment.js @@ -152,18 +152,6 @@ export class LivechatDepartment extends Base { return this.find(query, options); } - - unsetFallbackDepartmentByDepartmentId(_id) { - return this.update( - { fallbackForwardDepartment: _id }, - { - $unset: { - fallbackForwardDepartment: 1, - }, - }, - { multi: true }, - ); - } } export default new LivechatDepartment(); diff --git a/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js b/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js index d3e9111b33f5..5ec1494a70fd 100644 --- a/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js +++ b/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js @@ -52,10 +52,6 @@ export class LivechatDepartmentAgents extends Base { this.remove({ departmentId, agentId }); } - removeByDepartmentId(departmentId) { - this.remove({ departmentId }); - } - getNextAgentForDepartment(departmentId, ignoreAgentId, extraQuery) { const agents = this.findByDepartmentId(departmentId).fetch(); diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.js b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.js deleted file mode 100644 index 7b1209364de8..000000000000 --- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.js +++ /dev/null @@ -1,26 +0,0 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import { AsyncStatePhase } from '../../../../../hooks/useAsyncState'; -import { useEndpointData } from '../../../../../hooks/useEndpointData'; -import Field from '../../../components/Field'; -import Info from '../../../components/Info'; -import Label from '../../../components/Label'; -import { FormSkeleton } from '../../Skeleton'; - -const DepartmentField = ({ departmentId }) => { - const t = useTranslation(); - const { value: data, phase: state } = useEndpointData('/v1/livechat/department/:_id', { keys: { _id: departmentId } }); - if (state === AsyncStatePhase.LOADING) { - return ; - } - const { department: { name } = {} } = data || { department: {} }; - return ( - - - {name || t('Department_not_found')} - - ); -}; - -export default DepartmentField; diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.tsx b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.tsx new file mode 100644 index 000000000000..a12bba236965 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.tsx @@ -0,0 +1,28 @@ +import { Box, Skeleton } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import Field from '../../../components/Field'; +import Info from '../../../components/Info'; +import Label from '../../../components/Label'; +import { useDepartmentInfo } from '../../hooks/useDepartmentInfo'; + +type DepartmentFieldProps = { + departmentId: string; +}; + +const DepartmentField = ({ departmentId }: DepartmentFieldProps) => { + const t = useTranslation(); + const { data, isLoading, isError } = useDepartmentInfo(departmentId); + + return ( + + + {isLoading && } + {isError && {t('Something_went_wrong')}} + {!isLoading && !isError && {data?.department?.name || t('Department_not_found')}} + + ); +}; + +export default DepartmentField; diff --git a/apps/meteor/client/views/omnichannel/directory/hooks/useDepartmentInfo.ts b/apps/meteor/client/views/omnichannel/directory/hooks/useDepartmentInfo.ts new file mode 100644 index 000000000000..076d794567b3 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/directory/hooks/useDepartmentInfo.ts @@ -0,0 +1,10 @@ +import type { OperationResult } from '@rocket.chat/rest-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; + +export const useDepartmentInfo = (departmentId: string): UseQueryResult> => { + const deptInfo = useEndpoint('GET', `/v1/livechat/department/:_id`, { _id: departmentId }); + + return useQuery(['livechat/department', departmentId], () => deptInfo({})); +}; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js deleted file mode 100644 index 8a17be5f27fd..000000000000 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js +++ /dev/null @@ -1,16 +0,0 @@ -import { callbacks } from '../../../../../lib/callbacks'; -import { LivechatDepartment } from '../../../../../app/models/server'; - -callbacks.add( - 'livechat.afterRemoveDepartment', - (options = {}) => { - const { department } = options; - if (!department) { - return options; - } - LivechatDepartment.removeDepartmentFromForwardListById(department._id); - return options; - }, - callbacks.priority.HIGH, - 'livechat-after-remove-department', -); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts new file mode 100644 index 000000000000..6f4adbf557d6 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts @@ -0,0 +1,30 @@ +import type { ILivechatAgent, ILivechatDepartmentRecord } from '@rocket.chat/core-typings'; +import { LivechatDepartment } from '@rocket.chat/models'; + +import { callbacks } from '../../../../../lib/callbacks'; +import { cbLogger } from '../lib/logger'; + +const afterRemoveDepartment = async (options: { department: ILivechatDepartmentRecord; agentsId: ILivechatAgent['_id'][] }) => { + cbLogger.debug(`Performing post-department-removal actions in EE: ${options?.department?._id}. Removing department from forward list`); + if (!options || !options.department) { + cbLogger.warn('No department found in options', options); + return options; + } + + const { department } = options; + + cbLogger.debug(`Removing department from forward list: ${department._id}`); + await LivechatDepartment.removeDepartmentFromForwardListById(department._id); + cbLogger.debug(`Removed department from forward list: ${department._id}`); + + cbLogger.debug(`Post-department-removal actions completed in EE: ${department._id}`); + + return options; +}; + +callbacks.add( + 'livechat.afterRemoveDepartment', + (options) => Promise.await(afterRemoveDepartment(options)), + callbacks.priority.HIGH, + 'livechat-after-remove-department', +); diff --git a/apps/meteor/ee/app/models/server/models/LivechatDepartment.js b/apps/meteor/ee/app/models/server/models/LivechatDepartment.js index cfa218c9db51..163ae5e72d1d 100644 --- a/apps/meteor/ee/app/models/server/models/LivechatDepartment.js +++ b/apps/meteor/ee/app/models/server/models/LivechatDepartment.js @@ -55,8 +55,4 @@ overwriteClassOnLicense('livechat-enterprise', LivechatDepartment, { }, }); -LivechatDepartment.prototype.removeDepartmentFromForwardListById = function (_id) { - return this.update({ departmentsAllowedToForward: _id }, { $pull: { departmentsAllowedToForward: _id } }, { multi: true }); -}; - export default LivechatDepartment; diff --git a/apps/meteor/ee/server/models/LivechatDepartment.ts b/apps/meteor/ee/server/models/LivechatDepartment.ts new file mode 100644 index 000000000000..620983c7711d --- /dev/null +++ b/apps/meteor/ee/server/models/LivechatDepartment.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../../../server/database/utils'; +import { LivechatDepartmentEE } from './raw/LivechatDepartment'; + +registerModel('ILivechatDepartmentModel', new LivechatDepartmentEE(db)); diff --git a/apps/meteor/ee/server/models/raw/LivechatDepartment.ts b/apps/meteor/ee/server/models/raw/LivechatDepartment.ts new file mode 100644 index 000000000000..589f47216ccd --- /dev/null +++ b/apps/meteor/ee/server/models/raw/LivechatDepartment.ts @@ -0,0 +1,15 @@ +import type { ILivechatDepartmentModel } from '@rocket.chat/model-typings'; + +import { LivechatDepartmentRaw } from '../../../../server/models/raw/LivechatDepartment'; + +declare module '@rocket.chat/model-typings' { + export interface ILivechatDepartmentModel { + removeDepartmentFromForwardListById(departmentId: string): Promise; + } +} + +export class LivechatDepartmentEE extends LivechatDepartmentRaw implements ILivechatDepartmentModel { + async removeDepartmentFromForwardListById(departmentId: string): Promise { + await this.updateMany({ departmentsAllowedToForward: departmentId }, { $pull: { departmentsAllowedToForward: departmentId } }); + } +} diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index 9645c9cd0501..7b803b4c9521 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -7,4 +7,5 @@ onLicense('livechat-enterprise', () => { import('./LivechatUnit'); import('./LivechatUnitMonitors'); import('./LivechatRooms'); + import('./LivechatDepartment'); }); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index da6c8b1d1cc5..7eed03bcbf29 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -107,7 +107,7 @@ type ChainedCallbackSignatures = { oldDepartmentId: ILivechatDepartmentRecord['_id']; }; 'livechat.afterInquiryQueued': (inquiry: ILivechatInquiryRecord) => ILivechatInquiryRecord; - 'livechat.afterRemoveDepartment': (params: { departmentId: ILivechatDepartmentRecord['_id']; agentsId: ILivechatAgent['_id'][] }) => { + 'livechat.afterRemoveDepartment': (params: { department: ILivechatDepartmentRecord; agentsId: ILivechatAgent['_id'][] }) => { departmentId: ILivechatDepartmentRecord['_id']; agentsId: ILivechatAgent['_id'][]; }; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 4fb24dac3515..8ecd2fe353d7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1880,6 +1880,7 @@ "error-email-domain-blacklisted": "The email domain is blacklisted", "error-email-send-failed": "Error trying to send email: __message__", "error-essential-app-disabled": "Error: a Rocket.Chat App that is essential for this is disabled. Please contact your administrator", + "error-failed-to-delete-department": "Failed to delete department", "error-field-unavailable": "__field__ is already in use :(", "error-file-too-large": "File is too large", "error-forwarding-chat": "Something went wrong while forwarding the chat, Please try again later.", @@ -5550,4 +5551,4 @@ "Theme_dark": "Dark", "Join_your_team": "Join your team", "Create_an_account": "Create an account" -} \ No newline at end of file +} diff --git a/apps/meteor/server/models/raw/LivechatDepartment.ts b/apps/meteor/server/models/raw/LivechatDepartment.ts index ee03b2ab31a8..6338e98b1222 100644 --- a/apps/meteor/server/models/raw/LivechatDepartment.ts +++ b/apps/meteor/server/models/raw/LivechatDepartment.ts @@ -110,4 +110,12 @@ export class LivechatDepartmentRaw extends BaseRaw im return this.updateMany(query, update); } + + unsetFallbackDepartmentByDepartmentId(departmentId: string): Promise { + return this.updateMany({ fallbackDepartment: departmentId }, { $unset: { fallbackDepartment: 1 } }); + } + + removeDepartmentFromForwardListById(_departmentId: string): Promise { + throw new Error('Method not implemented in Community Edition.'); + } } diff --git a/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts b/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts index a55dea5601e1..5e9a278b2db7 100644 --- a/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts +++ b/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts @@ -1,6 +1,6 @@ import type { ILivechatDepartmentAgents, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { FindPaginated, ILivechatDepartmentAgentsModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, Filter, FindOptions } from 'mongodb'; +import type { Collection, FindCursor, Db, Filter, FindOptions, DeleteResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -105,4 +105,8 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw { + return this.deleteOne({ departmentId }); + } } diff --git a/apps/meteor/server/models/raw/LivechatRooms.js b/apps/meteor/server/models/raw/LivechatRooms.js index 5e0f9f2af9f5..542f05ddbb02 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.js +++ b/apps/meteor/server/models/raw/LivechatRooms.js @@ -1288,4 +1288,8 @@ export class LivechatRoomsRaw extends BaseRaw { }, ]); } + + bulkRemoveDepartmentAndUnitsFromRooms(departmentId) { + return this.updateMany({ departmentId }, { $unset: { departmentId: 1, departmentAncestors: 1 } }); + } } diff --git a/apps/meteor/tests/data/livechat/units.ts b/apps/meteor/tests/data/livechat/units.ts index 07f11cfc3ca2..3b5a94c0132b 100644 --- a/apps/meteor/tests/data/livechat/units.ts +++ b/apps/meteor/tests/data/livechat/units.ts @@ -25,7 +25,7 @@ export const createMonitor = async (username: string): Promise<{ _id: string; us }); }; -export const createUnit = async (monitorId: string, username: string, departmentId: string): Promise => { +export const createUnit = async (monitorId: string, username: string, departmentIds: string[]): Promise => { return new Promise((resolve, reject) => { request .post(methodCall(`livechat:saveUnit`)) @@ -33,7 +33,7 @@ export const createUnit = async (monitorId: string, username: string, department .send({ message: JSON.stringify({ method: 'livechat:saveUnit', - params: [null, { name: faker.name.firstName(), visibility: faker.helpers.arrayElement(['public', 'private']) }, [{ monitorId, username }], [{ departmentId }]], + params: [null, { name: faker.name.firstName(), visibility: faker.helpers.arrayElement(['public', 'private']) }, [{ monitorId, username }], departmentIds.map((departmentId) => ({ departmentId }))], id: '101', msg: 'method', }), diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index df182eee81c7..88e5e6167a5d 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -5,7 +5,18 @@ import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -import { makeAgentAvailable, createAgent, createDepartment } from '../../../data/livechat/rooms'; +import { + makeAgentAvailable, + createAgent, + createDepartment, + createVisitor, + createLivechatRoom, + getLivechatRoomInfo, +} from '../../../data/livechat/rooms'; +import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department'; +import { IS_EE } from '../../../e2e/config/constants'; +import { createUser } from '../../../data/users.helper'; +import { createMonitor, createUnit } from '../../../data/livechat/units'; describe('LIVECHAT - Departments', function () { before((done) => getCredentials(done)); @@ -153,6 +164,129 @@ describe('LIVECHAT - Departments', function () { }); }); + describe('DELETE livechat/department/:_id', () => { + it('should return unauthorized error when the user does not have the necessary permission', async () => { + await updatePermission('manage-livechat-departments', []); + await updatePermission('remove-livechat-department', []); + + await request + .delete(api('livechat/department/testetetetstetete')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403); + }); + + it('should return an error when the department does not exist', async () => { + await updatePermission('manage-livechat-departments', ['admin']); + + const resp: Response = await request + .delete(api('livechat/department/testesteteste')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); + + expect(resp.body).to.have.property('success', false); + expect(resp.body).to.have.property('error', 'error-department-not-found'); + }); + + it('it should remove the department', async () => { + const department = await createDepartment(); + + const resp: Response = await request + .delete(api(`livechat/department/${department._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(resp.body).to.have.property('success', true); + }); + + it('it should remove the department and disassociate the rooms from it', async () => { + const { department } = await createDepartmentWithAnOnlineAgent(); + const newVisitor = await createVisitor(department._id); + const newRoom = await createLivechatRoom(newVisitor.token); + + const resp: Response = await request + .delete(api(`livechat/department/${department._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(resp.body).to.have.property('success', true); + + const latestRoom = await getLivechatRoomInfo(newRoom._id); + expect(latestRoom.departmentId).to.be.undefined; + }); + + (IS_EE ? it : it.skip)('it should remove the department and disassociate the rooms from it which have its units', async () => { + const { department } = await createDepartmentWithAnOnlineAgent(); + const newVisitor = await createVisitor(department._id); + const newRoom = await createLivechatRoom(newVisitor.token); + + const monitor = await createUser(); + await createMonitor(monitor.username); + const unit = await createUnit(monitor._id, monitor.username, [department._id]); + + // except the room to have the unit + let latestRoom = await getLivechatRoomInfo(newRoom._id); + expect(latestRoom.departmentId).to.be.equal(department._id); + expect(latestRoom.departmentAncestors).to.be.an('array').that.includes(unit._id); + + const resp: Response = await request + .delete(api(`livechat/department/${department._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(resp.body).to.have.property('success', true); + + latestRoom = await getLivechatRoomInfo(newRoom._id); + expect(latestRoom.departmentId).to.be.undefined; + expect(latestRoom.departmentAncestors).to.be.undefined; + }); + + (IS_EE ? it : it.skip)( + 'contd from above test case: if a unit has more than 1 dept, then it should not disassociate rooms from other dept when any one dept is removed', + async () => { + const { department: department1 } = await createDepartmentWithAnOnlineAgent(); + const newVisitor1 = await createVisitor(department1._id); + const newRoom1 = await createLivechatRoom(newVisitor1.token); + + const { department: department2 } = await createDepartmentWithAnOnlineAgent(); + const newVisitor2 = await createVisitor(department2._id); + const newRoom2 = await createLivechatRoom(newVisitor2.token); + + const monitor = await createUser(); + await createMonitor(monitor.username); + const unit = await createUnit(monitor._id, monitor.username, [department1._id, department2._id]); + + // except the room to have the unit + let latestRoom1 = await getLivechatRoomInfo(newRoom1._id); + let latestRoom2 = await getLivechatRoomInfo(newRoom2._id); + expect(latestRoom1.departmentId).to.be.equal(department1._id); + expect(latestRoom1.departmentAncestors).to.be.an('array').that.includes(unit._id); + expect(latestRoom2.departmentId).to.be.equal(department2._id); + expect(latestRoom2.departmentAncestors).to.be.an('array').that.includes(unit._id); + + const resp: Response = await request + .delete(api(`livechat/department/${department1._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(resp.body).to.have.property('success', true); + + latestRoom1 = await getLivechatRoomInfo(newRoom1._id); + expect(latestRoom1.departmentId).to.be.undefined; + expect(latestRoom1.departmentAncestors).to.be.undefined; + + latestRoom2 = await getLivechatRoomInfo(newRoom2._id); + expect(latestRoom2.departmentId).to.be.equal(department2._id); + expect(latestRoom2.departmentAncestors).to.be.an('array').that.includes(unit._id); + }, + ); + }); + describe('GET livechat/department.autocomplete', () => { it('should return an error when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-departments', []) 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 76bbe33001c2..ef624febce29 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 @@ -40,7 +40,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const user = await createUser(); await createMonitor(user.username); const department = await createDepartment(); - const unit = await createUnit(user._id, user.username, department._id); + const unit = await createUnit(user._id, user.username, [department._id]); const { body } = await request.get(api('livechat/units')).set(credentials).expect(200); expect(body.units).to.be.an('array').with.lengthOf.greaterThan(0); @@ -104,7 +104,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const user = await createUser(); await createMonitor(user.username); const department = await createDepartment(); - const unit = await createUnit(user._id, user.username, department._id); + const unit = await createUnit(user._id, user.username, [department._id]); const { body } = await request .get(api(`livechat/units/${unit._id}`)) @@ -128,7 +128,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const user = await createUser(); await createMonitor(user.username); const department = await createDepartment(); - const unit = await createUnit(user._id, user.username, department._id); + const unit = await createUnit(user._id, user.username, [department._id]); const { body } = await request .post(api(`livechat/units/${unit._id}`)) @@ -159,7 +159,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const user = await createUser(); await createMonitor(user.username); const department = await createDepartment(); - const unit = await createUnit(user._id, user.username, department._id); + const unit = await createUnit(user._id, user.username, [department._id]); const { body } = await request .delete(api(`livechat/units/${unit._id}`)) @@ -180,7 +180,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const user = await createUser(); await createMonitor(user.username); const department = await createDepartment(); - const unit = await createUnit(user._id, user.username, department._id); + const unit = await createUnit(user._id, user.username, [department._id]); const { body } = await request .get(api(`livechat/units/${unit._id}/departments`)) @@ -204,7 +204,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const user = await createUser(); await createMonitor(user.username); const department = await createDepartment(); - const unit = await createUnit(user._id, user.username, department._id); + const unit = await createUnit(user._id, user.username, [department._id]); const { body } = await request .get(api(`livechat/units/${unit._id}/departments/available`)) @@ -229,7 +229,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const user = await createUser(); await createMonitor(user.username); const department = await createDepartment(); - const unit = await createUnit(user._id, user.username, department._id); + const unit = await createUnit(user._id, user.username, [department._id]); const { body } = await request .get(api(`livechat/units/${unit._id}/monitors`)) diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index b44fd8b8b5be..3250254a0946 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -188,6 +188,8 @@ export interface IOmnichannelGenericRoom extends Omit): FindCursor; findAgentsByAgentIdAndBusinessHourId(_agentId: string, _businessHourId: string): []; + removeByDepartmentId(departmentId: string): Promise; } diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index b75f93007982..41c97bd2ce7e 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -30,4 +30,6 @@ export interface ILivechatDepartmentModel extends IBaseModel; removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId: string): Promise; + + unsetFallbackDepartmentByDepartmentId(departmentId: string): Promise; } diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index b3607e29b930..4e7f36b30044 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -109,4 +109,6 @@ export interface ILivechatRoomsModel extends IBaseModel { setAutoTransferredAtById(roomId: string): Promise; findAvailableSources(): AggregationCursor; + + bulkRemoveDepartmentAndUnitsFromRooms(departmentId: string): Promise; }