Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2c2e02c
chore: adds a deprecation warning for livechat:saveBusinessHour
lucas-a-pelegrino Dec 3, 2025
0f1cc99
feat: implements a new endpoint to handle business hours saving
lucas-a-pelegrino Dec 4, 2025
55719ab
feat: implements a new endpoint to handle business hours saving
lucas-a-pelegrino Dec 4, 2025
85bc094
tests: replace business hour e2e tests to use the new endpoint
lucas-a-pelegrino Dec 5, 2025
e2b7687
tests: adds minor fixes to test data handling
lucas-a-pelegrino Dec 5, 2025
f5c3c3b
tests: adds minor improvement to business hours helper functions
lucas-a-pelegrino Dec 5, 2025
b3a0a21
tests: adds minor changes to helper functions to adhere to endpoint i…
lucas-a-pelegrino Dec 8, 2025
7b32374
tests: adds logging to check response in ci
lucas-a-pelegrino Dec 8, 2025
d11300c
docs: adds .changeset
lucas-a-pelegrino Dec 8, 2025
78e734e
tests: fixes and minor improvements
lucas-a-pelegrino Dec 8, 2025
98f3683
tests: fixes to helper functions object structures
lucas-a-pelegrino Dec 8, 2025
91064c1
tests: adds more adjustments to helper functions objects
lucas-a-pelegrino Dec 9, 2025
4d4dfb6
tests: adds a minor fix to createBusinessHour helper func for the UI …
lucas-a-pelegrino Dec 9, 2025
c4433f6
chore: adds improvements to POSTLivechatBusinessHoursSaveParams schem…
lucas-a-pelegrino Dec 10, 2025
49e9d71
chore: removes nullable prop from timezone
lucas-a-pelegrino Dec 10, 2025
ab19df8
docs: removes nullable key from daysTime
lucas-a-pelegrino Dec 10, 2025
05700ac
Merge branch 'develop' into chore/CORE-1550
kodiakhq[bot] Dec 10, 2025
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/brown-llamas-worry.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 a deprecation warning for `livechat:saveBusinessHour` and new endpoint replacing it; `livechat/business-hours.save`
39 changes: 38 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,16 @@
import { isGETBusinessHourParams } from '@rocket.chat/rest-typings';
import type { ILivechatBusinessHour } from '@rocket.chat/core-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 +25,31 @@ 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;

// TODO: Remove typecasting after refactoring saveBusinessHour logic with proper type logic. See: CORE-1552
const result = await businessHourManager.saveBusinessHour(params as unknown as ILivechatBusinessHour);

return API.v1.success(result);
}
);

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 {}
}
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
122 changes: 69 additions & 53 deletions apps/meteor/tests/data/livechat/businessHours.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { POSTLivechatBusinessHoursSaveParams } from '@rocket.chat/rest-typings';
import moment from 'moment';

import { api, credentials, methodCall, request } from '../api-data';
Expand All @@ -9,26 +10,30 @@ type ISaveBhApiWorkHour = Omit<ILivechatBusinessHour, '_id' | 'ts' | 'timezone'>
workHours: { day: string; start: string; finish: string; open: boolean }[];
} & { departmentsToApplyBusinessHour?: string } & { timezoneName: string };

// TODO: Migrate to an API call and return the business hour updated/created
export const saveBusinessHour = async (businessHour: ISaveBhApiWorkHour) => {
const { body } = await request
.post(methodCall('livechat:saveBusinessHour'))
.set(credentials)
.send({ message: JSON.stringify({ params: [businessHour], msg: 'method', method: 'livechat:saveBusinessHour', id: '101' }) })
.expect(200);
export const saveBusinessHour = async (businessHour: POSTLivechatBusinessHoursSaveParams) => {
const { body } = await request.post(api('livechat/business-hours.save')).set(credentials).send(businessHour);

return JSON.parse(body.message);
return body;
};

export const createCustomBusinessHour = async (departments: string[], open = true): Promise<ILivechatBusinessHour> => {
const name = `business-hour-${Date.now()}`;
const businessHour: ISaveBhApiWorkHour = {
const businessHour: POSTLivechatBusinessHoursSaveParams = {
name,
active: true,
type: LivechatBusinessHourTypes.CUSTOM,
workHours: getWorkHours(open),
timezoneName: 'Asia/Calcutta',
timezone: 'Asia/Calcutta',
departmentsToApplyBusinessHour: '',
daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
daysTime: [
{ day: 'Monday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
{ day: 'Tuesday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
{ day: 'Wednesday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
{ day: 'Thursday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
{ day: 'Friday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
],
};

if (departments.length) {
Expand Down Expand Up @@ -56,31 +61,35 @@ export const makeDefaultBusinessHourActiveAndClosed = async () => {
body: { businessHour },
} = await request.get(api('livechat/business-hour')).query({ type: 'default' }).set(credentials).send();

// TODO: Refactor this to use openOrCloseBusinessHour() instead
const workHours = businessHour.workHours as { start: string; finish: string; day: string; open: boolean }[];
const allEnabledWorkHours = workHours.map((workHour) => {
workHour.open = true;
workHour.start = '00:00';
workHour.finish = '00:01'; // if a job runs between 00:00 and 00:01, then this test will fail :P
return workHour;
});
const { workHours } = businessHour;

// Remove properties not accepted by the endpoint schema
const { _updatedAt, ts, ...cleanedBusinessHour } = businessHour;

const enabledBusinessHour = {
...businessHour,
workHours: allEnabledWorkHours,
...cleanedBusinessHour,
timezoneName: 'America/Sao_Paulo',
timezone: 'America/Sao_Paulo',
departmentsToApplyBusinessHour: '',
daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
daysTime: workHours.map((workHour: { open: boolean; start: { time: string }; finish: { time: string }; day: string }) => {
return {
open: true,
start: { time: '00:00' },
finish: { time: '00:01' },
day: workHour.day,
};
}),
workHours: workHours.map((workHour: { open: boolean; start: string; finish: string; day: string; code?: number }) => {
workHour.open = true;
workHour.start = '00:00';
workHour.finish = '00:01'; // if a job runs between 00:00 and 00:01, then this test will fail :P
delete workHour.code;
return workHour;
}),
};

await request
.post(methodCall('livechat:saveBusinessHour'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveBusinessHour',
params: [enabledBusinessHour],
id: 'id',
msg: 'method',
}),
});
return request.post(api('livechat/business-hours.save')).set(credentials).send(enabledBusinessHour).expect(200);
};

export const disableDefaultBusinessHour = async () => {
Expand All @@ -93,31 +102,33 @@ export const disableDefaultBusinessHour = async () => {
body: { businessHour },
} = await request.get(api('livechat/business-hour')).query({ type: 'default' }).set(credentials).send();

// TODO: Refactor this to use openOrCloseBusinessHour() instead
const workHours = businessHour.workHours as { start: string; finish: string; day: string; open: boolean }[];
const allDisabledWorkHours = workHours.map((workHour) => {
workHour.open = false;
workHour.start = '00:00';
workHour.finish = '23:59';
return workHour;
});
const { workHours } = businessHour;

// Remove properties not accepted by the endpoint schema
const { _updatedAt, ts, ...cleanedBusinessHour } = businessHour;
const disabledBusinessHour = {
...businessHour,
workHours: allDisabledWorkHours,
...cleanedBusinessHour,
timezoneName: 'America/Sao_Paulo',
timezone: 'America/Sao_Paulo',
departmentsToApplyBusinessHour: '',
daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
daysTime: workHours.map((workHour: { open: boolean; start: { time: string }; finish: { time: string }; day: string }) => {
return {
open: false,
start: { time: '00:00' },
finish: { time: '23:59' },
day: workHour.day,
};
}),
workHours: workHours.map((workHour: { open: boolean; start: string; finish: string; day: string }) => {
workHour.open = false;
workHour.start = '00:00';
workHour.finish = '23:59';
return workHour;
}),
};

await request
.post(methodCall('livechat:saveBusinessHour'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveBusinessHour',
params: [disabledBusinessHour],
id: 'id',
msg: 'method',
}),
});
return request.post(api('livechat/business-hours.save')).set(credentials).send(disabledBusinessHour).expect(200);
};

const removeCustomBusinessHour = async (businessHourId: string) => {
Expand Down Expand Up @@ -167,10 +178,15 @@ export const getCustomBusinessHourById = async (businessHourId: string): Promise
return response.body.businessHour;
};

// TODO: Refactor logic so object passed is of the correct type for POST /livechat/business-hours.save. See: CORE-1552
export const openOrCloseBusinessHour = async (businessHour: ILivechatBusinessHour, open: boolean) => {
const { _updatedAt, ts, ...cleanedBusinessHour } = businessHour;
const timezoneName = businessHour.timezone.name;

const enabledBusinessHour = {
...businessHour,
timezoneName: businessHour.timezone.name,
...cleanedBusinessHour,
timezoneName,
timezone: timezoneName,
workHours: getWorkHours().map((workHour) => {
return {
...workHour,
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: 23 additions & 34 deletions apps/meteor/tests/e2e/utils/omnichannel/businessHours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,30 @@ type CreateBusinessHoursParams = {
departments?: { departmentId: string }[];
};

export const createBusinessHour = async (api: BaseTest['api'], { id = null, name, departments = [] }: CreateBusinessHoursParams = {}) => {
export const createBusinessHour = async (api: BaseTest['api'], { name, departments = [] }: CreateBusinessHoursParams = {}) => {
const departmentIds = departments.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 },
],
},
],
}),
return 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;
};
Loading
Loading