Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/warm-ants-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/rest-typings": patch
---

Adds deprecation warning on `livechat:saveBusinessHour` with new endpoint replacing it; `livechat/business-houra.save`
40 changes: 39 additions & 1 deletion apps/meteor/app/livechat/imports/server/rest/businessHours.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { isGETBusinessHourParams } from '@rocket.chat/rest-typings';
import {
isGETBusinessHourParams,
isPOSTLivechatBusinessHoursSaveParams,
POSTLivechatBusinessHoursSaveSuccessResponse,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';

import { API } from '../../../../api/server';
import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass';
import { findLivechatBusinessHour } from '../../../server/api/lib/businessHours';
import { businessHourManager } from '../../../server/business-hour';

API.v1.addRoute(
'livechat/business-hour',
Expand All @@ -16,3 +24,33 @@ API.v1.addRoute(
},
},
);

const livechatBusinessHoursEndpoints = API.v1.post(
'livechat/business-hours.save',
{
response: {
200: POSTLivechatBusinessHoursSaveSuccessResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
authRequired: true,
body: isPOSTLivechatBusinessHoursSaveParams,
},
async function action() {
const params = this.bodyParams;

try {
await businessHourManager.saveBusinessHour(params);
return API.v1.success();
} catch (error: unknown) {
return API.v1.failure('error-saving-business-hour', error instanceof Error ? error.message : String(error));
}
},
);

type LivechatBusinessHoursEndpoints = ExtractRoutesFromAPI<typeof livechatBusinessHoursEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends LivechatBusinessHoursEndpoints {}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { AtLeast, ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings';
import type {
AtLeast,
ILivechatAgentStatus,
ILivechatBusinessHour,
ILivechatDepartment,
ISaveLivechatBusinessHour,
} from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings';
import { LivechatBusinessHours, Users } from '@rocket.chat/models';
Expand Down Expand Up @@ -29,7 +35,7 @@ export interface IBusinessHourBehavior {
export interface IBusinessHourType {
name: string;
getBusinessHour(id?: string): Promise<ILivechatBusinessHour | null>;
saveBusinessHour(businessHourData: ILivechatBusinessHour): Promise<ILivechatBusinessHour>;
saveBusinessHour(businessHourData: ISaveLivechatBusinessHour): Promise<ILivechatBusinessHour>;
removeBusinessHourById(id: string): Promise<void>;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ILivechatBusinessHour, IBusinessHourTimezone } from '@rocket.chat/core-typings';
import type { ILivechatBusinessHour, IBusinessHourTimezone, ISaveLivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { AgendaCronJobs } from '@rocket.chat/cron';
import { LivechatBusinessHours, LivechatDepartment, Users } from '@rocket.chat/models';
Expand Down Expand Up @@ -103,7 +103,7 @@ export class BusinessHourManager {
return businessHourType.getBusinessHour(id);
}

async saveBusinessHour(businessHourData: ILivechatBusinessHour): Promise<void> {
async saveBusinessHour(businessHourData: ISaveLivechatBusinessHour): Promise<void> {
const type = this.getBusinessHourType((businessHourData.type as string) || LivechatBusinessHourTypes.DEFAULT) as IBusinessHourType;
const saved = await type.saveBusinessHour(businessHourData);
if (!settings.get('Livechat_enable_business_hours')) {
Expand Down Expand Up @@ -276,12 +276,15 @@ export class BusinessHourManager {

return businessHourType.saveBusinessHour({
...businessHour,
timezone: businessHour.timezone.name,
timezoneName: businessHour.timezone.name,
workHours: businessHour.workHours.map((hour) => ({ ...hour, start: hour.start.time, finish: hour.finish.time })) as Record<
string,
any
>[],
} as ILivechatBusinessHour & { timezoneName: string });
workHours: businessHour.workHours.map((hour) => ({
day: hour.day,
start: hour.start.time,
finish: hour.finish.time,
open: hour.open,
})),
});
}),
);
const failed = result.filter((r) => r.status === 'rejected');
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/app/livechat/server/methods/saveBusinessHour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';

import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
import { businessHourManager } from '../business-hour';

declare module '@rocket.chat/ddp-client' {
Expand All @@ -13,6 +14,7 @@ declare module '@rocket.chat/ddp-client' {

Meteor.methods<ServerMethods>({
async 'livechat:saveBusinessHour'(businessHourData) {
methodDeprecationLogger.method('livechat:saveBusinessHour', '8.0.0', '/v1/livechat/business-hours.save');
try {
await businessHourManager.saveBusinessHour(businessHourData);
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ILivechatBusinessHour, LivechatBusinessHourTypes, Serialized } from '@rocket.chat/core-typings';
import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useMethod, useTranslation, useRouter } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useTranslation, useRouter, useEndpoint } from '@rocket.chat/ui-contexts';
import { useId } from 'react';
import { FormProvider, useForm } from 'react-hook-form';

Expand Down Expand Up @@ -39,7 +39,7 @@ const EditBusinessHours = ({ businessHourData, type }: EditBusinessHoursProps) =
const dispatchToastMessage = useToastMessageDispatch();
const isSingleBH = useIsSingleBusinessHours();

const saveBusinessHour = useMethod('livechat:saveBusinessHour');
const saveBusinessHour = useEndpoint('POST', '/v1/livechat/business-hours.save');
const handleRemove = useRemoveBusinessHour();

const router = useRouter();
Expand Down Expand Up @@ -69,7 +69,7 @@ const EditBusinessHours = ({ businessHourData, type }: EditBusinessHoursProps) =
})),
};

await saveBusinessHour(payload as any);
await saveBusinessHour(payload);
dispatchToastMessage({ type: 'success', message: t('Business_hours_updated') });
router.navigate('/omnichannel/businessHours');
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';

import { IS_EE } from '../config/constants';
Expand All @@ -19,7 +18,6 @@ test.describe('OC - Business Hours', () => {
let department2: Awaited<ReturnType<typeof createDepartment>>;
let agent: Awaited<ReturnType<typeof createAgent>>;

const BHid = faker.string.uuid();
const BHName = 'TEST Business Hours';

test.beforeAll(async ({ api }) => {
Expand Down Expand Up @@ -89,7 +87,6 @@ test.describe('OC - Business Hours', () => {
test('OC - Business hours - Edit BH departments', async ({ api, page }) => {
await test.step('expect to create new businessHours', async () => {
const createBH = await createBusinessHour(api, {
id: BHid,
name: BHName,
departments: [department.data._id],
});
Expand Down Expand Up @@ -139,7 +136,6 @@ test.describe('OC - Business Hours', () => {
test('OC - Business hours - Toggle BH active status', async ({ api, page }) => {
await test.step('expect to create new businessHours', async () => {
const createBH = await createBusinessHour(api, {
id: BHid,
name: BHName,
departments: [department.data._id],
});
Expand Down
57 changes: 24 additions & 33 deletions apps/meteor/tests/e2e/utils/omnichannel/businessHours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,31 @@ type CreateBusinessHoursParams = {
departments?: { departmentId: string }[];
};

export const createBusinessHour = async (api: BaseTest['api'], { id = null, name, departments = [] }: CreateBusinessHoursParams = {}) => {
const departmentIds = departments.join(',');
export const createBusinessHour = async (api: BaseTest['api'], { name, departments = [] }: CreateBusinessHoursParams = {}) => {
const departmentIds = departments.map(({ departmentId }) => departmentId).join(',');

const response = await api.post('/method.call/livechat:saveBusinessHour', {
message: JSON.stringify({
msg: 'method',
id: id || '33',
method: 'livechat:saveBusinessHour',
params: [
{
name,
timezoneName: 'America/Sao_Paulo',
daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
daysTime: [
{ day: 'Monday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
{ day: 'Tuesday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
{ day: 'Wednesday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
{ day: 'Thursday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
{ day: 'Friday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
],
departmentsToApplyBusinessHour: departmentIds,
active: true,
type: 'custom',
timezone: 'America/Sao_Paulo',
workHours: [
{ day: 'Monday', start: '08:00', finish: '18:00', open: true },
{ day: 'Tuesday', start: '08:00', finish: '18:00', open: true },
{ day: 'Wednesday', start: '08:00', finish: '18:00', open: true },
{ day: 'Thursday', start: '08:00', finish: '18:00', open: true },
{ day: 'Friday', start: '08:00', finish: '18:00', open: true },
],
},
],
}),
const response = await api.post('/livechat/business-hours.save', {
name,
timezoneName: 'America/Sao_Paulo',
daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
daysTime: [
{ day: 'Monday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
{ day: 'Tuesday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
{ day: 'Wednesday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
{ day: 'Thursday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
{ day: 'Friday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true },
],
departmentsToApplyBusinessHour: departmentIds,
active: true,
type: 'custom',
timezone: 'America/Sao_Paulo',
workHours: [
{ day: 'Monday', start: '08:00', finish: '18:00', open: true },
{ day: 'Tuesday', start: '08:00', finish: '18:00', open: true },
{ day: 'Wednesday', start: '08:00', finish: '18:00', open: true },
{ day: 'Thursday', start: '08:00', finish: '18:00', open: true },
{ day: 'Friday', start: '08:00', finish: '18:00', open: true },
],
});

return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,17 @@ describe('[OC] BusinessHourManager', () => {
sinon.stub(manager, 'createCronJobsForWorkHours');
sinon.stub(manager, 'hasDaylightSavingTimeChanged').returns(true);
findActiveBusinessHoursStub.resolves([
{ timezone: { name: 'timezoneName' }, workHours: [{ start: { time: 'startTime' }, finish: { time: 'finishTime' } }] },
{
timezone: { name: 'timezoneName' },
workHours: [{ day: 'Monday', start: { time: 'startTime' }, finish: { time: 'finishTime' }, open: true }],
},
]);
await manager.startDaylightSavingTimeVerifier();
expect(
saveBusinessHourStub.calledWith({
timezone: { name: 'timezoneName' },
timezone: 'timezoneName',
timezoneName: 'timezoneName',
workHours: [{ start: 'startTime', finish: 'finishTime' }],
workHours: [{ day: 'Monday', start: 'startTime', finish: 'finishTime', open: true }],
}),
).to.be.true;
expect(manager.createCronJobsForWorkHours.called).to.be.true;
Expand Down
23 changes: 23 additions & 0 deletions packages/core-typings/src/ILivechatBusinessHour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,26 @@ export interface ILivechatBusinessHour {
_updatedAt?: Date;
departments?: ILivechatDepartment[];
}

export interface ISaveLivechatBusinessHour {
_id?: string;
name: string;
active: boolean;
type: LivechatBusinessHourTypes;
daysOpen?: string[];
daysTime?: {
day: string;
start: { time: string };
finish: { time: string };
open: boolean;
}[];
workHours: {
day: string;
start: string;
finish: string;
open: boolean;
}[];
timezone: string;
timezoneName?: string;
departmentsToApplyBusinessHour?: string;
}
80 changes: 79 additions & 1 deletion packages/rest-typings/src/v1/omnichannel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {
import {

Check failure on line 1 in packages/rest-typings/src/v1/omnichannel.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

All imports in the declaration are only used as types. Use `import type`
IOmnichannelCannedResponse,
ILivechatAgent,
ILivechatDepartment,
Expand Down Expand Up @@ -32,8 +32,9 @@
ILivechatContactChannel,
IUser,
OmichannelRoutingConfig,
ISaveLivechatBusinessHour,
} from '@rocket.chat/core-typings';

Check failure on line 36 in packages/rest-typings/src/v1/omnichannel.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

'/home/runner/work/Rocket.Chat/Rocket.Chat/node_modules/@rocket.chat/core-typings/dist/index.js' imported multiple times
import { ILivechatAgentStatus } from '@rocket.chat/core-typings';

Check failure on line 37 in packages/rest-typings/src/v1/omnichannel.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

'/home/runner/work/Rocket.Chat/Rocket.Chat/node_modules/@rocket.chat/core-typings/dist/index.js' imported multiple times
import type { WithId } from 'mongodb';

import { ajv } from './Ajv';
Expand Down Expand Up @@ -3075,6 +3076,83 @@
GETLivechatAgentsAgentIdDepartmentsParamsSchema,
);

const POSTLivechatBusinessHoursSaveSchema = {
type: 'object',
properties: {
_id: { type: 'string', nullable: true },
name: { type: 'string' },
active: { type: 'boolean' },
type: {
type: 'string',
enum: ['default', 'custom'],
},
daysOpen: {
type: 'array',
items: {
type: 'string',
},
nullable: true,
},
daysTime: {
type: 'array',
items: {
type: 'object',
properties: {
day: { type: 'string' },
start: {
type: 'object',
properties: {
time: { type: 'string' },
},
},
finish: {
type: 'object',
properties: {
time: { type: 'string' },
},
},
Comment on lines +3103 to +3113
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Require time in daysTime.start/finish and lock shape

start/finish objects don't require time and allow extraneous keys. This permits invalid payloads to pass AJV while TS expects { time: string }.

Apply this diff:

 					start: {
 						type: 'object',
 						properties: {
 							time: { type: 'string' },
 						},
+						required: ['time'],
+						additionalProperties: false,
 					},
 					finish: {
 						type: 'object',
 						properties: {
 							time: { type: 'string' },
 						},
+						required: ['time'],
+						additionalProperties: false,
 					},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type: 'object',
properties: {
time: { type: 'string' },
},
},
finish: {
type: 'object',
properties: {
time: { type: 'string' },
},
},
type: 'object',
properties: {
time: { type: 'string' },
},
required: ['time'],
additionalProperties: false,
},
finish: {
type: 'object',
properties: {
time: { type: 'string' },
},
required: ['time'],
additionalProperties: false,
},
🤖 Prompt for AI Agents
In packages/rest-typings/src/v1/omnichannel.ts around lines 3103 to 3113, the
schema for daysTime.start and daysTime.finish currently defines time as an
optional property and allows extraneous keys; update both objects to require the
time property by adding required: ['time'] and prevent extra properties by
adding additionalProperties: false so the AJV validation matches the TypeScript
type { time: string } and rejects payloads with missing or extra fields.

open: { type: 'boolean' },
},
required: ['day', 'start', 'finish', 'open'],
additionalProperties: false,
},
nullable: true,
},
workHours: {
type: 'array',
items: {
type: 'object',
properties: {
day: { type: 'string' },
start: { type: 'string' },
finish: { type: 'string' },
open: { type: 'boolean' },
},
required: ['day', 'start', 'finish', 'open'],
additionalProperties: false,
},
},
timezone: { type: 'string' },
timezoneName: { type: 'string', nullable: true },
departmentsToApplyBusinessHour: { type: 'string', nullable: true },
},
required: ['name', 'active', 'type', 'workHours', 'timezone'],
additionalProperties: false,
};

export const isPOSTLivechatBusinessHoursSaveParams = ajv.compile<ISaveLivechatBusinessHour>(POSTLivechatBusinessHoursSaveSchema);

const POSTLivechatBusinessHoursSaveSuccessResponseSchema = {
type: 'object',
properties: {
success: { type: 'boolean', enum: [true] },
},
required: ['success'],
additionalProperties: false,
};

export const POSTLivechatBusinessHoursSaveSuccessResponse = ajv.compile<void>(POSTLivechatBusinessHoursSaveSuccessResponseSchema);

type GETBusinessHourParams = { _id?: string; type?: string };

const GETBusinessHourParamsSchema = {
Expand Down
Loading