diff --git a/.changeset/spicy-kings-think.md b/.changeset/spicy-kings-think.md new file mode 100644 index 000000000000..9e8f3648b28c --- /dev/null +++ b/.changeset/spicy-kings-think.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes multiple problems with the `processRoomAbandonment` hook. This hook is in charge of calculating the time a room has been abandoned (this means, the time that elapsed since a room was last answered by an agent until it was closed). However, when business hours were enabled and the user didn't open on one day, if an abandoned room happened to be abandoned _over_ the day there was no business hour configuration, then the process will error out. +Additionally, the values the code was calculating were not right. When business hours are enabled, this code should only count the abandonment time _while a business hour was open_. When rooms were left abandoned for days or weeks, this will also throw an error or output an invalid count. diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 6f8250d2acd4..5af39b924057 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -29,6 +29,9 @@ inputs: required: false description: 'Setup node.js' default: 'true' + NPM_TOKEN: + required: false + description: 'NPM token' runs: using: composite @@ -65,6 +68,7 @@ runs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ inputs.NPM_TOKEN }} - run: yarn build if: inputs.setup == 'true' diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml index dfbc1cef4150..525595146700 100644 --- a/.github/actions/meteor-build/action.yml +++ b/.github/actions/meteor-build/action.yml @@ -13,6 +13,9 @@ inputs: required: true description: 'Node version' type: string + NPM_TOKEN: + required: false + description: 'NPM token' runs: using: composite @@ -29,6 +32,7 @@ runs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ inputs.NPM_TOKEN }} # - name: Free disk space # run: | diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 60d54ab896dd..1035e2835792 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -1,22 +1,27 @@ name: 'Setup Node' +description: 'Setup NodeJS' inputs: node-version: required: true - type: string + description: 'Node version' cache-modules: required: false - type: boolean + description: 'Cache node_modules' install: required: false - type: boolean + description: 'Install dependencies' deno-dir: required: false - type: string + description: 'Deno directory' default: ~/.deno-cache + NPM_TOKEN: + required: false + description: 'NPM token' outputs: node-version: + description: 'Node version' value: ${{ steps.node-version.outputs.node-version }} runs: @@ -49,6 +54,13 @@ runs: node-version: ${{ inputs.node-version }} cache: 'yarn' + - name: yarn login + shell: bash + if: inputs.NPM_TOKEN + run: | + echo "//registry.npmjs.org/:_authToken=${{ inputs.NPM_TOKEN }}" > ~/.npmrc + - name: yarn install + if: inputs.install shell: bash run: yarn diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index fd214bc39488..af50b3230ba7 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -35,6 +35,7 @@ jobs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # - name: Free disk space # run: | diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 31a8bc2ea2b6..e6c02b7b6417 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -130,6 +130,8 @@ jobs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - uses: rharkor/caching-for-turbo@v1.5 - run: yarn build @@ -145,6 +147,7 @@ jobs: # the same reason we need to rebuild the docker image at this point is the reason we dont want to publish it publish-image: false setup: false + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Start httpbin container and wait for it to be ready if: inputs.type == 'api' diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index a32c1e575b8f..840808ff5e31 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -39,6 +39,7 @@ jobs: node-version: ${{ inputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: rharkor/caching-for-turbo@v1.5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 246c34423bb1..514dd6d1c518 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,6 +152,7 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Cache vite uses: actions/cache@v3 @@ -253,6 +254,7 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} platform: ${{ matrix.platform }} build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} build-gh-docker: name: 🚢 Build Docker Images for Production @@ -280,6 +282,7 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} platform: ${{ matrix.platform }} build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Rename official Docker tag to GitHub Container Registry if: matrix.platform == 'official' @@ -560,6 +563,7 @@ jobs: release: preview username: ${{ secrets.CR_USER }} password: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} docker-image-publish: name: 🚀 Publish Docker Image (main) diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 5ef8027b1467..b2eae5d90b92 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -37,6 +37,7 @@ jobs: node-version: 14.21.3 cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: rharkor/caching-for-turbo@v1.5 diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index e792127eac9d..084f2a383480 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -24,6 +24,7 @@ jobs: node-version: 14.21.3 cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: rharkor/caching-for-turbo@v1.5 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ccc3408e194e..3f2067ac7ec3 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -27,6 +27,7 @@ jobs: node-version: 14.21.3 cache-modules: true install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: rharkor/caching-for-turbo@v1.5 diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index 8eb53fbb8fa7..a6031bd42efa 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -6,11 +6,12 @@ import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; +import type { CloseRoomParams } from '../lib/localTypes'; -const getSecondsWhenOfficeHoursIsDisabled = (room: IOmnichannelRoom, agentLastMessage: IMessage) => +export const getSecondsWhenOfficeHoursIsDisabled = (room: IOmnichannelRoom, agentLastMessage: IMessage) => moment(new Date(room.closedAt || new Date())).diff(moment(new Date(agentLastMessage.ts)), 'seconds'); -const parseDays = ( +export const parseDays = ( acc: Record, day: IBusinessHourWorkHour, ) => { @@ -22,7 +23,7 @@ const parseDays = ( return acc; }; -const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => { +export const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => { if (!settings.get('Livechat_enable_business_hours')) { return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); } @@ -49,65 +50,85 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas } let totalSeconds = 0; - const endOfConversation = moment(new Date(room.closedAt || new Date())); - const startOfInactivity = moment(new Date(agentLastMessage.ts)); + const endOfConversation = moment.utc(new Date(room.closedAt || new Date())); + const startOfInactivity = moment.utc(new Date(agentLastMessage.ts)); const daysOfInactivity = endOfConversation.clone().startOf('day').diff(startOfInactivity.clone().startOf('day'), 'days'); - const inactivityDay = moment(new Date(agentLastMessage.ts)); + const inactivityDay = moment.utc(new Date(agentLastMessage.ts)); + for (let index = 0; index <= daysOfInactivity; index++) { const today = inactivityDay.clone().format('dddd'); const officeDay = officeDays[today]; - // Config doesnt have data for this day, we skip day if (!officeDay) { inactivityDay.add(1, 'days'); continue; } - const startTodaysOfficeHour = moment(`${officeDay.start.day}:${officeDay.start.time}`, 'dddd:HH:mm').add(index, 'days'); - const endTodaysOfficeHour = moment(`${officeDay.finish.day}:${officeDay.finish.time}`, 'dddd:HH:mm').add(index, 'days'); - if (officeDays[today].open) { - const firstDayOfInactivity = startOfInactivity.clone().format('D') === inactivityDay.clone().format('D'); - const lastDayOfInactivity = endOfConversation.clone().format('D') === inactivityDay.clone().format('D'); - - if (!firstDayOfInactivity && !lastDayOfInactivity) { - totalSeconds += endTodaysOfficeHour.clone().diff(startTodaysOfficeHour, 'seconds'); - } else { - const end = endOfConversation.isBefore(endTodaysOfficeHour) ? endOfConversation : endTodaysOfficeHour; - const start = firstDayOfInactivity ? inactivityDay : startTodaysOfficeHour; - totalSeconds += end.clone().diff(start, 'seconds'); - } + if (!officeDay.open) { + inactivityDay.add(1, 'days'); + continue; + } + if (!officeDay?.start?.time || !officeDay?.finish?.time) { + inactivityDay.add(1, 'days'); + continue; } - inactivityDay.add(1, 'days'); - } - return totalSeconds; -}; -callbacks.add( - 'livechat.closeRoom', - async (params) => { - const { room } = params; + const [officeStartHour, officeStartMinute] = officeDay.start.time.split(':'); + const [officeCloseHour, officeCloseMinute] = officeDay.finish.time.split(':'); + // We should only take in consideration the time where the office is open and the conversation was inactive + const todayStartOfficeHours = inactivityDay + .clone() + .set({ hour: parseInt(officeStartHour, 10), minute: parseInt(officeStartMinute, 10) }); + const todayEndOfficeHours = inactivityDay.clone().set({ hour: parseInt(officeCloseHour, 10), minute: parseInt(officeCloseMinute, 10) }); - if (!isOmnichannelRoom(room)) { - return params; + // 1: Room was inactive the whole day, we add the whole time BH is inactive + if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) { + totalSeconds += todayEndOfficeHours.diff(todayStartOfficeHours, 'seconds'); } - const closedByAgent = room.closer !== 'visitor'; - const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token; - if (!closedByAgent || !wasTheLastMessageSentByAgent) { - return params; + // 2: Room was inactive before start but was closed before end of BH, we add the inactive time + if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) { + totalSeconds += endOfConversation.diff(todayStartOfficeHours, 'seconds'); } - if (!room.v?.lastMessageTs) { - return params; + // 3: Room was inactive after start and ended after end of BH, we add the inactive time + if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) { + totalSeconds += todayEndOfficeHours.diff(startOfInactivity, 'seconds'); } - const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs); - if (!agentLastMessage) { - return params; + // 4: Room was inactive after start and before end of BH, we add the inactive time + if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) { + totalSeconds += endOfConversation.diff(startOfInactivity, 'seconds'); } - const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage); - await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse); + inactivityDay.add(1, 'days'); + } + return totalSeconds; +}; + +export const onCloseRoom = async (params: { room: IOmnichannelRoom; options: CloseRoomParams['options'] }) => { + const { room } = params; + + if (!isOmnichannelRoom(room)) { + return params; + } + + const closedByAgent = room.closer !== 'visitor'; + const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token; + if (!closedByAgent || !wasTheLastMessageSentByAgent) { + return params; + } + + if (!room.v?.lastMessageTs) { return params; - }, - callbacks.priority.HIGH, - 'process-room-abandonment', -); + } + + const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs); + if (!agentLastMessage) { + return params; + } + const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse); + + return params; +}; + +callbacks.add('livechat.closeRoom', onCloseRoom, callbacks.priority.HIGH, 'process-room-abandonment'); diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index fef6ad0936f8..109f49f440b5 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -1,4 +1,4 @@ -import { isEditedMessage } from '@rocket.chat/core-typings'; +import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings'; import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; @@ -70,8 +70,12 @@ callbacks.add( message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } }; } - const analyticsData = getAnalyticsData(room, new Date()); - await LivechatRooms.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData, roomUpdater); + if (isMessageFromVisitor(message)) { + LivechatRooms.getAnalyticsUpdateQueryBySentByVisitor(room, message, roomUpdater); + } else { + const analyticsData = getAnalyticsData(room, new Date()); + LivechatRooms.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, roomUpdater); + } return message; }, diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index ebe10ec67fbc..731cbcebf593 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -9,7 +9,7 @@ import type { ReportResult, MACStats, } from '@rocket.chat/core-typings'; -import { isMessageFromVisitor, UserStatus } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import type { ILivechatRoomsModel } from '@rocket.chat/model-typings'; import type { Updater } from '@rocket.chat/models'; import { Settings } from '@rocket.chat/models'; @@ -2010,7 +2010,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return updater; } - private getAnalyticsUpdateQueryBySentByAgent( + getAnalyticsUpdateQueryBySentByAgent( room: IOmnichannelRoom, message: IMessage, analyticsData: Record | undefined, @@ -2027,10 +2027,9 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.getAnalyticsUpdateQuery(analyticsData, updater); } - private getAnalyticsUpdateQueryBySentByVisitor( + getAnalyticsUpdateQueryBySentByVisitor( room: IOmnichannelRoom, message: IMessage, - analyticsData: Record | undefined, updater: Updater = this.getUpdater(), ) { // livechat analytics : update last message timestamps @@ -2039,21 +2038,10 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive // update visitor timestamp, only if its new inquiry and not continuing message if (agentLastReply >= visitorLastQuery) { - return this.getAnalyticsUpdateQuery(analyticsData, updater).set('metrics.v.lq', message.ts); + return updater.set('metrics.v.lq', message.ts); } - return this.getAnalyticsUpdateQuery(analyticsData, updater); - } - - async getAnalyticsUpdateQueryByRoomId( - room: IOmnichannelRoom, - message: IMessage, - analyticsData: Record | undefined, - updater: Updater = this.getUpdater(), - ) { - return isMessageFromVisitor(message) - ? this.getAnalyticsUpdateQueryBySentByVisitor(room, message, analyticsData, updater) - : this.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, updater); + return updater; } getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, { departmentId }: { departmentId?: string } = {}) { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts index 2d96aef8e365..872eafdfb2a2 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts @@ -22,101 +22,102 @@ test.describe('OC - Manage Departments', () => { test.beforeAll(async ({ api }) => { // turn on department removal - const statusCode = (await api.post('/settings/Omnichannel_enable_department_removal', { value: true })).status(); - await expect(statusCode).toBe(200); + await api.post('/settings/Omnichannel_enable_department_removal', { value: true }); }); test.afterAll(async ({ api }) => { // turn off department removal - const statusCode = (await api.post('/settings/Omnichannel_enable_department_removal', { value: false })).status(); - await expect(statusCode).toBe(200); + await api.post('/settings/Omnichannel_enable_department_removal', { value: false }); }); - test.beforeEach(async ({ page }: { page: Page }) => { - poOmnichannelDepartments = new OmnichannelDepartments(page); + test.describe('Create first department', async () => { + test.beforeEach(async ({ page }: { page: Page }) => { + poOmnichannelDepartments = new OmnichannelDepartments(page); - await page.goto('/omnichannel'); - await poOmnichannelDepartments.sidenav.linkDepartments.click(); - }); - - test('Create department', async () => { - const departmentName = faker.string.uuid(); + await page.goto('/omnichannel'); + await poOmnichannelDepartments.sidenav.linkDepartments.click(); + }); - await poOmnichannelDepartments.headingButtonNew('Create department').click(); + test('Create department', async () => { + const departmentName = faker.string.uuid(); - await test.step('expect name and email to be required', async () => { - await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); - await poOmnichannelDepartments.inputName.fill('any_text'); - await poOmnichannelDepartments.inputName.fill(''); - await expect(poOmnichannelDepartments.invalidInputName).toBeVisible(); - await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredName)).toBeVisible(); - await poOmnichannelDepartments.inputName.fill('any_text'); - await expect(poOmnichannelDepartments.invalidInputName).not.toBeVisible(); + await poOmnichannelDepartments.headingButtonNew('Create department').click(); - await poOmnichannelDepartments.inputEmail.fill('any_text'); - await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); - await expect(poOmnichannelDepartments.errorMessage(ERROR.invalidEmail)).toBeVisible(); + await test.step('expect name and email to be required', async () => { + await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); + await poOmnichannelDepartments.inputName.fill('any_text'); + await poOmnichannelDepartments.inputName.fill(''); + await expect(poOmnichannelDepartments.invalidInputName).toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredName)).toBeVisible(); + await poOmnichannelDepartments.inputName.fill('any_text'); + await expect(poOmnichannelDepartments.invalidInputName).not.toBeVisible(); - await poOmnichannelDepartments.inputEmail.fill(''); - await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); - await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).toBeVisible(); + await poOmnichannelDepartments.inputEmail.fill('any_text'); + await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.invalidEmail)).toBeVisible(); - await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); - await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); - await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).not.toBeVisible(); - }); + await poOmnichannelDepartments.inputEmail.fill(''); + await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).toBeVisible(); - await test.step('expect create new department', async () => { - await poOmnichannelDepartments.btnEnabled.click(); - await poOmnichannelDepartments.inputName.fill(departmentName); - await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); - await poOmnichannelDepartments.btnSave.click(); - await poOmnichannelDepartments.btnCloseToastSuccess.click(); - - await poOmnichannelDepartments.search(departmentName); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); - }); + await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); + await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).not.toBeVisible(); + }); - await test.step('expect to delete department', async () => { - await poOmnichannelDepartments.search(departmentName); - await poOmnichannelDepartments.selectedDepartmentMenu(departmentName).click(); - await poOmnichannelDepartments.menuDeleteOption.click(); + await test.step('expect create new department', async () => { + await poOmnichannelDepartments.btnEnabled.click(); + await poOmnichannelDepartments.inputName.fill(departmentName); + await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); + await poOmnichannelDepartments.btnSave.click(); - await test.step('expect confirm delete department', async () => { - await expect(poOmnichannelDepartments.modalConfirmDelete).toBeVisible(); + await poOmnichannelDepartments.search(departmentName); + await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + }); - await test.step('expect delete to be disabled when name is incorrect', async () => { - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); - await poOmnichannelDepartments.inputModalConfirmDelete.fill('someramdomname'); - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + await test.step('expect to delete department', async () => { + await poOmnichannelDepartments.search(departmentName); + await poOmnichannelDepartments.selectedDepartmentMenu(departmentName).click(); + await poOmnichannelDepartments.menuDeleteOption.click(); + + await test.step('expect confirm delete department', async () => { + await test.step('expect delete to be disabled when name is incorrect', async () => { + await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + await poOmnichannelDepartments.inputModalConfirmDelete.fill('someramdomname'); + await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + }); + + await test.step('expect to successfuly delete if department name is correct', async () => { + await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + await poOmnichannelDepartments.inputModalConfirmDelete.fill(departmentName); + await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeEnabled(); + await poOmnichannelDepartments.btnModalConfirmDelete.click(); + }); }); - await test.step('expect to successfuly delete if department name is correct', async () => { - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); - await poOmnichannelDepartments.inputModalConfirmDelete.fill(departmentName); - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeEnabled(); - await poOmnichannelDepartments.btnModalConfirmDelete.click(); + await test.step('expect department to have been deleted', async () => { + await poOmnichannelDepartments.search(departmentName); + await expect(poOmnichannelDepartments.firstRowInTable).toHaveCount(0); }); }); - - await test.step('expect department to have been deleted', async () => { - await poOmnichannelDepartments.search(departmentName); - await expect(poOmnichannelDepartments.firstRowInTable).toHaveCount(0); - }); }); }); test.describe('After creation', async () => { let department: Awaited>['data']; - test.beforeEach(async ({ api }) => { + + test.beforeEach(async ({ api, page }) => { + poOmnichannelDepartments = new OmnichannelDepartments(page); + department = await createDepartment(api).then((res) => res.data); + await page.goto('/omnichannel/departments'); }); test.afterEach(async ({ api }) => { await deleteDepartment(api, { id: department._id }); }); - test('Edit department', async ({ api }) => { + test('Edit department', async () => { await test.step('expect create new department', async () => { await poOmnichannelDepartments.search(department.name); await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); @@ -132,19 +133,13 @@ test.describe('OC - Manage Departments', () => { await poOmnichannelDepartments.inputName.fill(`edited-${department.name}`); await poOmnichannelDepartments.btnSave.click(); - await poOmnichannelDepartments.btnCloseToastSuccess.click(); await poOmnichannelDepartments.search(`edited-${department.name}`); await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); }); - - await test.step('expect to delete department', async () => { - const deleteRes = await deleteDepartment(api, { id: department._id }); - await expect(deleteRes.status()).toBe(200); - }); }); - test('Archive department', async ({ api }) => { + test('Archive department', async () => { await test.step('expect create new department', async () => { await poOmnichannelDepartments.search(department.name); await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); @@ -172,11 +167,6 @@ test.describe('OC - Manage Departments', () => { await poOmnichannelDepartments.menuUnarchiveOption.click(); await expect(poOmnichannelDepartments.firstRowInTable).toHaveCount(0); }); - - await test.step('expect to delete department', async () => { - const deleteRes = await deleteDepartment(api, { id: department._id }); - await expect(deleteRes.status()).toBe(200); - }); }); test('Request tag(s) before closing conversation', async () => { @@ -269,7 +259,7 @@ test.describe('OC - Manage Departments', () => { await test.step('expect to disable department removal setting', async () => { const statusCode = (await api.post('/settings/Omnichannel_enable_department_removal', { value: false })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); }); await test.step('expect not to be able to delete department', async () => { @@ -280,12 +270,12 @@ test.describe('OC - Manage Departments', () => { await test.step('expect to enable department removal setting', async () => { const statusCode = (await api.post('/settings/Omnichannel_enable_department_removal', { value: true })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); }); await test.step('expect to delete department', async () => { const deleteRes = await deleteDepartment(api, { id: department._id }); - await expect(deleteRes.status()).toBe(200); + expect(deleteRes.status()).toBe(200); }); }); }); diff --git a/apps/meteor/tests/unit/app/livechat/server/hooks/processRoomAbandonment.spec.ts b/apps/meteor/tests/unit/app/livechat/server/hooks/processRoomAbandonment.spec.ts new file mode 100644 index 000000000000..91f88c36b022 --- /dev/null +++ b/apps/meteor/tests/unit/app/livechat/server/hooks/processRoomAbandonment.spec.ts @@ -0,0 +1,623 @@ +import { expect } from 'chai'; +import { it, describe } from 'mocha'; +import p from 'proxyquire'; +import sinon from 'sinon'; + +const settingsStub = sinon.stub(); +const models = { + LivechatDepartment: { + findOneById: sinon.stub(), + }, + LivechatBusinessHours: { + findOneById: sinon.stub(), + }, + Messages: { + findAgentLastMessageByVisitorLastMessageTs: sinon.stub(), + }, + LivechatRooms: { + setVisitorInactivityInSecondsById: sinon.stub(), + }, +}; + +const businessHourManagerMock = { + getBusinessHour: sinon.stub(), +}; + +const { getSecondsWhenOfficeHoursIsDisabled, parseDays, getSecondsSinceLastAgentResponse, onCloseRoom } = p + .noCallThru() + .load('../../../../../../app/livechat/server/hooks/processRoomAbandonment.ts', { + '@rocket.chat/models': models, + '../../../../lib/callbacks': { + callbacks: { add: sinon.stub(), priority: { HIGH: 'high' } }, + }, + '../../../settings/server': { + settings: { get: settingsStub }, + }, + '../business-hour': { businessHourManager: businessHourManagerMock }, + }); + +describe('processRoomAbandonment', () => { + describe('getSecondsWhenOfficeHoursIsDisabled', () => { + it('should return the seconds since the agents last message till room was closed', () => { + const room = { + closedAt: new Date('2024-01-01T12:00:10Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T12:00:00Z'), + }; + const result = getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + it('should return the seconds since agents last message till now when room.closedAt is undefined', () => { + const room = { + closedAt: undefined, + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + }); + describe('parseDays', () => { + it('should properly return the days in the expected format', () => { + const days = [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ]; + + const result = days.reduce(parseDays, {}); + expect(result).to.be.deep.equal({ + Monday: { + start: { day: 'Monday', time: '10:00' }, + finish: { day: 'Monday', time: '11:00' }, + open: true, + }, + Tuesday: { + start: { day: 'Tuesday', time: '10:00' }, + finish: { day: 'Tuesday', time: '11:00' }, + open: true, + }, + Wednesday: { + start: { day: 'Wednesday', time: '10:00' }, + finish: { day: 'Wednesday', time: '11:00' }, + open: true, + }, + }); + }); + it('should properly parse open/close days', () => { + const days = [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: false, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ]; + + const result = days.reduce(parseDays, {}); + expect(result).to.be.deep.equal({ + Monday: { + start: { day: 'Monday', time: '10:00' }, + finish: { day: 'Monday', time: '11:00' }, + open: true, + }, + Tuesday: { + start: { day: 'Tuesday', time: '10:00' }, + finish: { day: 'Tuesday', time: '11:00' }, + open: false, + }, + Wednesday: { + start: { day: 'Wednesday', time: '10:00' }, + finish: { day: 'Wednesday', time: '11:00' }, + open: true, + }, + }); + }); + }); + describe('getSecondsSinceLastAgentResponse', () => { + beforeEach(() => { + settingsStub.reset(); + models.LivechatDepartment.findOneById.reset(); + models.LivechatBusinessHours.findOneById.reset(); + businessHourManagerMock.getBusinessHour.reset(); + }); + it('should return the seconds since agent last message when Livechat_enable_business_hours is false', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(false); + const room = { + closedAt: undefined, + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + it('should return the seconds since last agent message when room has a department but department has an invalid business hour attached', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({ + businessHourId: 'businessHourId', + }); + models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves(null); + const room = { + closedAt: undefined, + departmentId: 'departmentId', + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(models.LivechatDepartment.findOneById.calledWith(room.departmentId)).to.be.true; + expect(result).to.be.equal(10); + }); + it('should return the seconds since last agent message when department has a valid business hour but business hour doest have work hours', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({ + businessHourId: 'businessHourId', + }); + models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves({ + workHours: [], + }); + businessHourManagerMock.getBusinessHour.withArgs('businessHourId').resolves(null); + const room = { + closedAt: undefined, + departmentId: 'departmentId', + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + it('should return the seconds since last agent message when department has a valid business hour but business hour workhours is empty', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({ + businessHourId: 'businessHourId', + }); + models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves({ + workHours: [], + }); + businessHourManagerMock.getBusinessHour.withArgs('businessHourId').resolves({ + workHours: [], + }); + const room = { + closedAt: undefined, + departmentId: 'departmentId', + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(10); + }); + it('should get the data from the default business hour when room has no department attached and return the seconds since last agent message when default bh has no workhours', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [], + }); + const room = { + closedAt: undefined, + }; + const agentLastMessage = { + ts: new Date(new Date().getTime() - 10000), + }; + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(models.LivechatDepartment.findOneById.called).to.be.false; + expect(models.LivechatBusinessHours.findOneById.called).to.be.false; + expect(businessHourManagerMock.getBusinessHour.called).to.be.true; + expect(businessHourManagerMock.getBusinessHour.getCall(0).args.length).to.be.equal(0); + expect(result).to.be.equal(10); + }); + it('should return the proper number of seconds the room was inactive considering business hours (inactive same day)', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-01T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ], + }); + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(3600); + }); + it('should return the proper number of seconds the room was inactive considering business hours (inactive same day)', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-01T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '23:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(7200); + }); + it('should return 0 if a room happened to be inactive on a day outside of business hours', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-03T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-03T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(0); + }); + it('should return the proper number of seconds when a room was inactive for more than 1 day', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-03T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(7200); + }); + it('should return the proper number of seconds when a room was inactive for more than 1 day, and one of those days was a closed day', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-03T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: false, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(7200); + }); + it('should return the proper number of seconds when a room was inactive for more than 1 day and one of those days is not in configuration', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-03T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(7200); + }); + it('should return the proper number of seconds when a room has been inactive for more than a week', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-10T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } }, + open: true, + }, + { + day: 'Thursday', + start: { utc: { dayOfWeek: 'Thursday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Thursday', time: '11:00' } }, + open: false, + }, + { + day: 'Saturday', + start: { utc: { dayOfWeek: 'Friday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Friday', time: '11:00' } }, + open: true, + }, + { + day: 'Sunday', + start: { utc: { dayOfWeek: 'Saturday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Saturday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(28800); + }); + it('should return 0 when room was inactive in the same day but the configuration for bh on that day is invalid', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-01T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T00:00:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: undefined } }, + finish: { utc: { dayOfWeek: 'Monday', time: undefined } }, + open: true, + }, + { + day: 'Wednesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: false, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(0); + }); + it('should return the proper number of seconds when a room has been inactive for more than a day but the inactivity started after BH started', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-02T12:00:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T10:15:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(6300); + }); + it('should return the proper number of seconds when a room was inactive between a BH start and end', async () => { + settingsStub.withArgs('Livechat_enable_business_hours').returns(true); + const room = { + closedAt: new Date('2024-01-01T10:50:00Z'), + }; + const agentLastMessage = { + ts: new Date('2024-01-01T10:15:00Z'), + }; + businessHourManagerMock.getBusinessHour.resolves({ + workHours: [ + { + day: 'Monday', + start: { utc: { dayOfWeek: 'Monday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } }, + open: true, + }, + { + day: 'Tuesday', + start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } }, + finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } }, + open: true, + }, + ], + }); + + const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage); + expect(result).to.be.equal(2100); + }); + }); + describe('onCloseRoom', () => { + beforeEach(() => { + models.Messages.findAgentLastMessageByVisitorLastMessageTs.reset(); + }); + it('should skip the hook if room is not an omnichannel room', async () => { + const param = { room: { t: 'd' } }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false; + expect(r).to.be.equal(param); + }); + it('should skip if room was not closed by agent', async () => { + const param = { room: { t: 'l' }, closer: 'visitor' }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false; + expect(r).to.be.equal(param); + }); + it('should skip if the last message on room was not from an agent', async () => { + const param = { room: { t: 'l' }, closer: 'user', lastMessage: { token: 'xxxx' } }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false; + expect(r).to.be.equal(param); + }); + it('should skip if the last message is not on db', async () => { + models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves(null); + const param = { room: { _id: 'xyz', t: 'l', v: { lastMessageTs: new Date() }, closer: 'user', lastMessage: { msg: 'test' } } }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.calledWith('xyz', param.room.v.lastMessageTs)).to.be.true; + expect(r).to.be.equal(param); + }); + it('should skip if the visitor has not send any messages', async () => { + models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves({ ts: undefined }); + const param = { room: { _id: 'xyz', t: 'l', v: { token: 'xfasfdsa' }, closer: 'user', lastMessage: { msg: 'test' } } }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false; + expect(r).to.be.equal(param); + }); + it('should set the visitor inactivity in seconds when all params are valid', async () => { + models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves({ ts: new Date('2024-01-01T10:15:00Z') }); + settingsStub.withArgs('Livechat_enable_business_hours').returns(false); + const param = { + room: { + _id: 'xyz', + t: 'l', + v: { lastMessageTs: new Date() }, + closedAt: new Date('2024-01-01T10:50:00Z'), + closer: 'user', + lastMessage: { msg: 'test' }, + }, + }; + const r = await onCloseRoom(param); + + expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.calledWith('xyz', param.room.v.lastMessageTs)).to.be.true; + expect(models.LivechatRooms.setVisitorInactivityInSecondsById.calledWith('xyz', 2100)).to.be.true; + expect(r).to.be.equal(param); + }); + }); +}); diff --git a/ee/packages/api-client/.eslintrc.json b/packages/api-client/.eslintrc.json similarity index 100% rename from ee/packages/api-client/.eslintrc.json rename to packages/api-client/.eslintrc.json diff --git a/ee/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md similarity index 100% rename from ee/packages/api-client/CHANGELOG.md rename to packages/api-client/CHANGELOG.md diff --git a/ee/packages/api-client/LICENSE b/packages/api-client/LICENSE similarity index 100% rename from ee/packages/api-client/LICENSE rename to packages/api-client/LICENSE diff --git a/ee/packages/api-client/__tests__/2fahandling.spec.ts b/packages/api-client/__tests__/2fahandling.spec.ts similarity index 100% rename from ee/packages/api-client/__tests__/2fahandling.spec.ts rename to packages/api-client/__tests__/2fahandling.spec.ts diff --git a/ee/packages/api-client/jest.config.ts b/packages/api-client/jest.config.ts similarity index 100% rename from ee/packages/api-client/jest.config.ts rename to packages/api-client/jest.config.ts diff --git a/ee/packages/api-client/package.json b/packages/api-client/package.json similarity index 100% rename from ee/packages/api-client/package.json rename to packages/api-client/package.json diff --git a/ee/packages/api-client/src/Credentials.ts b/packages/api-client/src/Credentials.ts similarity index 100% rename from ee/packages/api-client/src/Credentials.ts rename to packages/api-client/src/Credentials.ts diff --git a/ee/packages/api-client/src/RestClientInterface.ts b/packages/api-client/src/RestClientInterface.ts similarity index 100% rename from ee/packages/api-client/src/RestClientInterface.ts rename to packages/api-client/src/RestClientInterface.ts diff --git a/ee/packages/api-client/src/errors.ts b/packages/api-client/src/errors.ts similarity index 100% rename from ee/packages/api-client/src/errors.ts rename to packages/api-client/src/errors.ts diff --git a/ee/packages/api-client/src/index.ts b/packages/api-client/src/index.ts similarity index 100% rename from ee/packages/api-client/src/index.ts rename to packages/api-client/src/index.ts diff --git a/ee/packages/api-client/tsconfig.json b/packages/api-client/tsconfig.json similarity index 71% rename from ee/packages/api-client/tsconfig.json rename to packages/api-client/tsconfig.json index b397e2c4421f..9d8ef0c3a373 100644 --- a/ee/packages/api-client/tsconfig.json +++ b/packages/api-client/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.client.json", + "extends": "../../tsconfig.base.client.json", "compilerOptions": { "module": "commonjs", "rootDir": "./src", diff --git a/ee/packages/ddp-client/.eslintrc.json b/packages/ddp-client/.eslintrc.json similarity index 100% rename from ee/packages/ddp-client/.eslintrc.json rename to packages/ddp-client/.eslintrc.json diff --git a/ee/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md similarity index 100% rename from ee/packages/ddp-client/CHANGELOG.md rename to packages/ddp-client/CHANGELOG.md diff --git a/ee/packages/ddp-client/LICENSE b/packages/ddp-client/LICENSE similarity index 100% rename from ee/packages/ddp-client/LICENSE rename to packages/ddp-client/LICENSE diff --git a/ee/packages/ddp-client/README.md b/packages/ddp-client/README.md similarity index 100% rename from ee/packages/ddp-client/README.md rename to packages/ddp-client/README.md diff --git a/ee/packages/ddp-client/__examples__/simple.ts b/packages/ddp-client/__examples__/simple.ts similarity index 100% rename from ee/packages/ddp-client/__examples__/simple.ts rename to packages/ddp-client/__examples__/simple.ts diff --git a/ee/packages/ddp-client/__mocks__/ws.ts b/packages/ddp-client/__mocks__/ws.ts similarity index 100% rename from ee/packages/ddp-client/__mocks__/ws.ts rename to packages/ddp-client/__mocks__/ws.ts diff --git a/ee/packages/ddp-client/__tests__/Account.spec.ts b/packages/ddp-client/__tests__/Account.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/Account.spec.ts rename to packages/ddp-client/__tests__/Account.spec.ts diff --git a/ee/packages/ddp-client/__tests__/ClientStream.spec.ts b/packages/ddp-client/__tests__/ClientStream.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/ClientStream.spec.ts rename to packages/ddp-client/__tests__/ClientStream.spec.ts diff --git a/ee/packages/ddp-client/__tests__/Connection.spec.ts b/packages/ddp-client/__tests__/Connection.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/Connection.spec.ts rename to packages/ddp-client/__tests__/Connection.spec.ts diff --git a/ee/packages/ddp-client/__tests__/DDPDispatcher.spec.ts b/packages/ddp-client/__tests__/DDPDispatcher.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/DDPDispatcher.spec.ts rename to packages/ddp-client/__tests__/DDPDispatcher.spec.ts diff --git a/ee/packages/ddp-client/__tests__/DDPSDK.spec.ts b/packages/ddp-client/__tests__/DDPSDK.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/DDPSDK.spec.ts rename to packages/ddp-client/__tests__/DDPSDK.spec.ts diff --git a/ee/packages/ddp-client/__tests__/MinimalDDPClient.spec.ts b/packages/ddp-client/__tests__/MinimalDDPClient.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/MinimalDDPClient.spec.ts rename to packages/ddp-client/__tests__/MinimalDDPClient.spec.ts diff --git a/ee/packages/ddp-client/__tests__/Timeout.spec.ts b/packages/ddp-client/__tests__/Timeout.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/Timeout.spec.ts rename to packages/ddp-client/__tests__/Timeout.spec.ts diff --git a/ee/packages/ddp-client/__tests__/helpers/index.ts b/packages/ddp-client/__tests__/helpers/index.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/helpers/index.ts rename to packages/ddp-client/__tests__/helpers/index.ts diff --git a/ee/packages/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts b/packages/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts similarity index 100% rename from ee/packages/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts rename to packages/ddp-client/__tests__/wrapOnceEventIntoPromise.spec.ts diff --git a/ee/packages/ddp-client/jest.config.ts b/packages/ddp-client/jest.config.ts similarity index 100% rename from ee/packages/ddp-client/jest.config.ts rename to packages/ddp-client/jest.config.ts diff --git a/ee/packages/ddp-client/package.json b/packages/ddp-client/package.json similarity index 100% rename from ee/packages/ddp-client/package.json rename to packages/ddp-client/package.json diff --git a/ee/packages/ddp-client/src/ClientStream.ts b/packages/ddp-client/src/ClientStream.ts similarity index 100% rename from ee/packages/ddp-client/src/ClientStream.ts rename to packages/ddp-client/src/ClientStream.ts diff --git a/ee/packages/ddp-client/src/Connection.ts b/packages/ddp-client/src/Connection.ts similarity index 100% rename from ee/packages/ddp-client/src/Connection.ts rename to packages/ddp-client/src/Connection.ts diff --git a/ee/packages/ddp-client/src/DDPDispatcher.ts b/packages/ddp-client/src/DDPDispatcher.ts similarity index 100% rename from ee/packages/ddp-client/src/DDPDispatcher.ts rename to packages/ddp-client/src/DDPDispatcher.ts diff --git a/ee/packages/ddp-client/src/DDPSDK.ts b/packages/ddp-client/src/DDPSDK.ts similarity index 100% rename from ee/packages/ddp-client/src/DDPSDK.ts rename to packages/ddp-client/src/DDPSDK.ts diff --git a/ee/packages/ddp-client/src/MinimalDDPClient.ts b/packages/ddp-client/src/MinimalDDPClient.ts similarity index 100% rename from ee/packages/ddp-client/src/MinimalDDPClient.ts rename to packages/ddp-client/src/MinimalDDPClient.ts diff --git a/ee/packages/ddp-client/src/TimeoutControl.ts b/packages/ddp-client/src/TimeoutControl.ts similarity index 100% rename from ee/packages/ddp-client/src/TimeoutControl.ts rename to packages/ddp-client/src/TimeoutControl.ts diff --git a/ee/packages/ddp-client/src/index.ts b/packages/ddp-client/src/index.ts similarity index 100% rename from ee/packages/ddp-client/src/index.ts rename to packages/ddp-client/src/index.ts diff --git a/ee/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts b/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts similarity index 100% rename from ee/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts rename to packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts diff --git a/ee/packages/ddp-client/src/legacy/types/SDKLegacy.ts b/packages/ddp-client/src/legacy/types/SDKLegacy.ts similarity index 100% rename from ee/packages/ddp-client/src/legacy/types/SDKLegacy.ts rename to packages/ddp-client/src/legacy/types/SDKLegacy.ts diff --git a/ee/packages/ddp-client/src/livechat/LivechatClientImpl.ts b/packages/ddp-client/src/livechat/LivechatClientImpl.ts similarity index 100% rename from ee/packages/ddp-client/src/livechat/LivechatClientImpl.ts rename to packages/ddp-client/src/livechat/LivechatClientImpl.ts diff --git a/ee/packages/ddp-client/src/livechat/types/LivechatSDK.ts b/packages/ddp-client/src/livechat/types/LivechatSDK.ts similarity index 100% rename from ee/packages/ddp-client/src/livechat/types/LivechatSDK.ts rename to packages/ddp-client/src/livechat/types/LivechatSDK.ts diff --git a/ee/packages/ddp-client/src/types/Account.ts b/packages/ddp-client/src/types/Account.ts similarity index 100% rename from ee/packages/ddp-client/src/types/Account.ts rename to packages/ddp-client/src/types/Account.ts diff --git a/ee/packages/ddp-client/src/types/ClientStream.ts b/packages/ddp-client/src/types/ClientStream.ts similarity index 100% rename from ee/packages/ddp-client/src/types/ClientStream.ts rename to packages/ddp-client/src/types/ClientStream.ts diff --git a/ee/packages/ddp-client/src/types/DDPClient.ts b/packages/ddp-client/src/types/DDPClient.ts similarity index 100% rename from ee/packages/ddp-client/src/types/DDPClient.ts rename to packages/ddp-client/src/types/DDPClient.ts diff --git a/ee/packages/ddp-client/src/types/IncomingPayload.ts b/packages/ddp-client/src/types/IncomingPayload.ts similarity index 100% rename from ee/packages/ddp-client/src/types/IncomingPayload.ts rename to packages/ddp-client/src/types/IncomingPayload.ts diff --git a/ee/packages/ddp-client/src/types/OutgoingPayload.ts b/packages/ddp-client/src/types/OutgoingPayload.ts similarity index 100% rename from ee/packages/ddp-client/src/types/OutgoingPayload.ts rename to packages/ddp-client/src/types/OutgoingPayload.ts diff --git a/ee/packages/ddp-client/src/types/RemoveListener.ts b/packages/ddp-client/src/types/RemoveListener.ts similarity index 100% rename from ee/packages/ddp-client/src/types/RemoveListener.ts rename to packages/ddp-client/src/types/RemoveListener.ts diff --git a/ee/packages/ddp-client/src/types/SDK.ts b/packages/ddp-client/src/types/SDK.ts similarity index 100% rename from ee/packages/ddp-client/src/types/SDK.ts rename to packages/ddp-client/src/types/SDK.ts diff --git a/ee/packages/ddp-client/src/types/Subscription.ts b/packages/ddp-client/src/types/Subscription.ts similarity index 100% rename from ee/packages/ddp-client/src/types/Subscription.ts rename to packages/ddp-client/src/types/Subscription.ts diff --git a/ee/packages/ddp-client/src/types/connectionPayloads.ts b/packages/ddp-client/src/types/connectionPayloads.ts similarity index 100% rename from ee/packages/ddp-client/src/types/connectionPayloads.ts rename to packages/ddp-client/src/types/connectionPayloads.ts diff --git a/ee/packages/ddp-client/src/types/heartbeatsPayloads.ts b/packages/ddp-client/src/types/heartbeatsPayloads.ts similarity index 100% rename from ee/packages/ddp-client/src/types/heartbeatsPayloads.ts rename to packages/ddp-client/src/types/heartbeatsPayloads.ts diff --git a/ee/packages/ddp-client/src/types/methods.ts b/packages/ddp-client/src/types/methods.ts similarity index 100% rename from ee/packages/ddp-client/src/types/methods.ts rename to packages/ddp-client/src/types/methods.ts diff --git a/ee/packages/ddp-client/src/types/methodsPayloads.ts b/packages/ddp-client/src/types/methodsPayloads.ts similarity index 100% rename from ee/packages/ddp-client/src/types/methodsPayloads.ts rename to packages/ddp-client/src/types/methodsPayloads.ts diff --git a/ee/packages/ddp-client/src/types/publicationPayloads.ts b/packages/ddp-client/src/types/publicationPayloads.ts similarity index 100% rename from ee/packages/ddp-client/src/types/publicationPayloads.ts rename to packages/ddp-client/src/types/publicationPayloads.ts diff --git a/ee/packages/ddp-client/src/types/streams.ts b/packages/ddp-client/src/types/streams.ts similarity index 100% rename from ee/packages/ddp-client/src/types/streams.ts rename to packages/ddp-client/src/types/streams.ts diff --git a/ee/packages/ddp-client/src/wrapOnceEventIntoPromise.ts b/packages/ddp-client/src/wrapOnceEventIntoPromise.ts similarity index 100% rename from ee/packages/ddp-client/src/wrapOnceEventIntoPromise.ts rename to packages/ddp-client/src/wrapOnceEventIntoPromise.ts diff --git a/ee/packages/ddp-client/tsconfig.json b/packages/ddp-client/tsconfig.json similarity index 81% rename from ee/packages/ddp-client/tsconfig.json rename to packages/ddp-client/tsconfig.json index 29b8cb051fe3..b98ff74ba385 100644 --- a/ee/packages/ddp-client/tsconfig.json +++ b/packages/ddp-client/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.client.json", + "extends": "../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "./src", "outDir": "./dist", diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index babfa4ea2165..3a9eb98d57c4 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -214,12 +214,17 @@ export interface ILivechatRoomsModel extends IBaseModel { ): Updater; getNotResponseByRoomIdUpdateQuery(updater: Updater): Updater; getAgentLastMessageTsUpdateQuery(updater?: Updater): Updater; - getAnalyticsUpdateQueryByRoomId( + getAnalyticsUpdateQueryBySentByAgent( room: IOmnichannelRoom, message: IMessage, analyticsData: Record | undefined, updater?: Updater, - ): Promise>; + ): Updater; + getAnalyticsUpdateQueryBySentByVisitor( + room: IOmnichannelRoom, + message: IMessage, + updater?: Updater, + ): Updater; getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, data?: { departmentId: string }): Promise; getAnalyticsMetricsBetweenDate( t: 'l', diff --git a/yarn.lock b/yarn.lock index de477be8048a..6a6c2a8ee9d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8463,9 +8463,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/api-client@workspace:^, @rocket.chat/api-client@workspace:ee/packages/api-client": +"@rocket.chat/api-client@workspace:^, @rocket.chat/api-client@workspace:packages/api-client": version: 0.0.0-use.local - resolution: "@rocket.chat/api-client@workspace:ee/packages/api-client" + resolution: "@rocket.chat/api-client@workspace:packages/api-client" dependencies: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" @@ -8657,9 +8657,9 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/ddp-client@workspace:^, @rocket.chat/ddp-client@workspace:ee/packages/ddp-client, @rocket.chat/ddp-client@workspace:~": +"@rocket.chat/ddp-client@workspace:^, @rocket.chat/ddp-client@workspace:packages/ddp-client, @rocket.chat/ddp-client@workspace:~": version: 0.0.0-use.local - resolution: "@rocket.chat/ddp-client@workspace:ee/packages/ddp-client" + resolution: "@rocket.chat/ddp-client@workspace:packages/ddp-client" dependencies: "@rocket.chat/api-client": "workspace:^" "@rocket.chat/core-typings": "workspace:~"