fix: bh does not work on weekends if you use a different TZ (#36544)

Co-authored-by: Kevin Aleman <kaleman960@gmail.com>
pull/35680/merge
Guilherme Gazzo 5 months ago committed by GitHub
parent 6de1abf972
commit 8f62a39807
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/thick-hotels-occur.md
  2. 428
      apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.spec.ts
  3. 23
      apps/meteor/app/livechat/server/business-hour/filterBusinessHoursThatMustBeOpened.ts
  4. 129
      apps/meteor/tests/end-to-end/api/livechat/01-agents.ts

@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---
fixes an issue where bussines hours are not working on weekends when the timezone of bh slip into another day

@ -2,7 +2,7 @@ import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import { filterBusinessHoursThatMustBeOpened } from './filterBusinessHoursThatMustBeOpened';
describe('different timezones between server and business hours', () => {
describe('different timezones between server and business hours saturday ', () => {
beforeEach(() => jest.useFakeTimers().setSystemTime(new Date('2024-04-20T20:10:11Z')));
afterEach(() => jest.useRealTimers());
it('should return a bh when the finish time resolves to a different day on server', async () => {
@ -52,3 +52,429 @@ describe('different timezones between server and business hours', () => {
expect(bh.length).toEqual(1);
});
});
describe('different timezones between server and business hours sunday ', () => {
beforeEach(() => jest.useFakeTimers().setSystemTime(new Date('2025-07-27T11:02:11Z')));
afterEach(() => jest.useRealTimers());
it('should return a bh when the finish time resolves to a different day on server', async () => {
const bh = await filterBusinessHoursThatMustBeOpened([
{
_id: '68516f256ebb4bdceda2757e',
active: true,
type: LivechatBusinessHourTypes.DEFAULT,
ts: new Date(),
name: '',
workHours: [
{
day: 'Sunday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Saturday',
time: '18:30',
},
cron: {
dayOfWeek: 'Saturday',
time: '15:30',
},
},
finish: {
time: '23:59',
utc: {
dayOfWeek: 'Sunday',
time: '18:29',
},
cron: {
dayOfWeek: 'Sunday',
time: '15:29',
},
},
open: true,
code: '',
},
{
day: 'Monday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Sunday',
time: '18:30',
},
cron: {
dayOfWeek: 'Sunday',
time: '15:30',
},
},
finish: {
time: '23:59',
utc: {
dayOfWeek: 'Monday',
time: '18:29',
},
cron: {
dayOfWeek: 'Monday',
time: '15:29',
},
},
open: true,
code: '',
},
{
day: 'Tuesday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Monday',
time: '18:30',
},
cron: {
dayOfWeek: 'Monday',
time: '15:30',
},
},
finish: {
time: '23:59',
utc: {
dayOfWeek: 'Tuesday',
time: '18:29',
},
cron: {
dayOfWeek: 'Tuesday',
time: '15:29',
},
},
open: true,
code: '',
},
{
day: 'Wednesday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Tuesday',
time: '18:30',
},
cron: {
dayOfWeek: 'Tuesday',
time: '15:30',
},
},
finish: {
time: '23:59',
utc: {
dayOfWeek: 'Wednesday',
time: '18:29',
},
cron: {
dayOfWeek: 'Wednesday',
time: '15:29',
},
},
open: true,
code: '',
},
{
day: 'Thursday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Wednesday',
time: '18:30',
},
cron: {
dayOfWeek: 'Wednesday',
time: '15:30',
},
},
finish: {
time: '23:59',
utc: {
dayOfWeek: 'Thursday',
time: '18:29',
},
cron: {
dayOfWeek: 'Thursday',
time: '15:29',
},
},
open: true,
code: '',
},
{
day: 'Friday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Thursday',
time: '18:30',
},
cron: {
dayOfWeek: 'Thursday',
time: '15:30',
},
},
finish: {
time: '23:59',
utc: {
dayOfWeek: 'Friday',
time: '18:29',
},
cron: {
dayOfWeek: 'Friday',
time: '15:29',
},
},
open: true,
code: '',
},
{
day: 'Saturday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Friday',
time: '18:30',
},
cron: {
dayOfWeek: 'Friday',
time: '15:30',
},
},
finish: {
time: '23:59',
utc: {
dayOfWeek: 'Saturday',
time: '18:29',
},
cron: {
dayOfWeek: 'Saturday',
time: '15:29',
},
},
open: true,
code: '',
},
],
timezone: {
name: 'Asia/Kolkata',
utc: '+05:30',
},
},
]);
expect(bh.length).toEqual(1);
});
});
describe('regular business hours', () => {
beforeEach(() => jest.useFakeTimers().setSystemTime(new Date('2025-08-07T22:02:11Z')));
afterEach(() => jest.useRealTimers());
it('should return a bh when the finish time resolves to a different day on server', async () => {
const bh = await filterBusinessHoursThatMustBeOpened([
{
_id: '68516f256ebb4bdceda2757e',
active: true,
type: LivechatBusinessHourTypes.DEFAULT,
ts: new Date(),
name: '',
workHours: [
{
day: 'Sunday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Sunday',
time: '03:00',
},
cron: {
dayOfWeek: 'Sunday',
time: '00:00',
},
},
finish: {
time: '00:01',
utc: {
dayOfWeek: 'Sunday',
time: '03:01',
},
cron: {
dayOfWeek: 'Sunday',
time: '00:01',
},
},
open: true,
code: '',
},
{
day: 'Monday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Monday',
time: '03:00',
},
cron: {
dayOfWeek: 'Monday',
time: '00:00',
},
},
finish: {
time: '00:01',
utc: {
dayOfWeek: 'Monday',
time: '03:01',
},
cron: {
dayOfWeek: 'Monday',
time: '00:01',
},
},
open: true,
code: '',
},
{
day: 'Tuesday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Tuesday',
time: '03:00',
},
cron: {
dayOfWeek: 'Tuesday',
time: '00:00',
},
},
finish: {
time: '00:01',
utc: {
dayOfWeek: 'Tuesday',
time: '03:01',
},
cron: {
dayOfWeek: 'Tuesday',
time: '00:01',
},
},
open: true,
code: '',
},
{
day: 'Wednesday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Wednesday',
time: '03:00',
},
cron: {
dayOfWeek: 'Wednesday',
time: '00:00',
},
},
finish: {
time: '00:01',
utc: {
dayOfWeek: 'Wednesday',
time: '03:01',
},
cron: {
dayOfWeek: 'Wednesday',
time: '00:01',
},
},
open: true,
code: '',
},
{
day: 'Thursday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Thursday',
time: '03:00',
},
cron: {
dayOfWeek: 'Thursday',
time: '00:00',
},
},
finish: {
time: '00:01',
utc: {
dayOfWeek: 'Thursday',
time: '03:01',
},
cron: {
dayOfWeek: 'Thursday',
time: '00:01',
},
},
open: true,
code: '',
},
{
day: 'Friday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Friday',
time: '03:00',
},
cron: {
dayOfWeek: 'Friday',
time: '00:00',
},
},
finish: {
time: '00:01',
utc: {
dayOfWeek: 'Friday',
time: '03:01',
},
cron: {
dayOfWeek: 'Friday',
time: '00:01',
},
},
open: true,
code: '',
},
{
day: 'Saturday',
start: {
time: '00:00',
utc: {
dayOfWeek: 'Saturday',
time: '03:00',
},
cron: {
dayOfWeek: 'Saturday',
time: '00:00',
},
},
finish: {
time: '00:01',
utc: {
dayOfWeek: 'Saturday',
time: '03:01',
},
cron: {
dayOfWeek: 'Saturday',
time: '00:01',
},
},
open: true,
code: '',
},
],
timezone: {
name: 'America/Sao_Paulo',
utc: '-3',
},
},
]);
expect(bh.length).toEqual(0);
});
});

@ -16,11 +16,32 @@ export const filterBusinessHoursThatMustBeOpened = async (
const localTimeStart = moment(`${hour.start.cron.dayOfWeek}:${hour.start.cron.time}:00`, 'dddd:HH:mm:ss');
const localTimeFinish = moment(`${hour.finish.cron.dayOfWeek}:${hour.finish.cron.time}:00`, 'dddd:HH:mm:ss');
// The way we create the instances sunday will be the first day of the current week not the next one, that way it will never met isBefore
/** because we use `dayOfWeek` moment decides if saturday/sunday belongs to the current week or the next one, this is a bit
* confusing and for that reason we need this workaround
*/
const currentDay = currentTime.format('dddd');
const localTimeStartDay = localTimeStart.format('dddd');
// This only works for sundays (where we can test if sunday is before saturday = something is wrong)
if (localTimeStart.isAfter(localTimeFinish)) {
localTimeStart.subtract(1, 'week');
}
if (localTimeFinish.isBefore(localTimeStart)) {
localTimeFinish.add(1, 'week');
}
// During Saturday, if current weekday is the same but the start time is after the current time, we need to subtract a week
if (currentDay === localTimeStartDay && localTimeStart.diff(currentTime, 'days') > 0) {
localTimeStart.subtract(1, 'week');
}
// During Saturday, if current weekday is the same but the finish time is before the current time, we need to add a week
if (currentDay === localTimeStartDay && localTimeFinish.diff(currentTime, 'days') < 0) {
localTimeFinish.add(1, 'week');
}
return currentTime.isSameOrAfter(localTimeStart) && currentTime.isBefore(localTimeFinish);
}),
)

@ -570,8 +570,12 @@ describe('LIVECHAT - Agents', () => {
});
describe('with no manage-agent permission', () => {
before(async () => {
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: 'not-available', agentId: agent2.user._id })
.expect(200);
await removePermissionFromAllRoles('manage-livechat-agents');
console.log('Permissions removed');
});
after(async () => {
await restorePermissionToRoles('manage-livechat-agents');
@ -624,69 +628,80 @@ describe('LIVECHAT - Agents', () => {
expect(res.body).to.have.property('error', 'Agent not found');
});
});
it('should change logged in users status', async () => {
const currentUser: ILivechatAgent = await getMe(agent2.credentials);
const currentStatus = currentUser.statusLivechat;
const newStatus = currentStatus === 'available' ? 'not-available' : 'available';
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: newStatus, agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', newStatus);
});
});
it('should allow managers to change other agents status', async () => {
const currentUser: ILivechatAgent = await getMe(agent2.credentials);
const currentStatus = currentUser.statusLivechat;
const newStatus = currentStatus === 'available' ? 'not-available' : 'available';
describe('cases for valid agents', () => {
let currentUser: ILivechatAgent;
before(async () => {
currentUser = await getMe(agent2.credentials);
});
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: newStatus, agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', newStatus);
});
});
it('should throw an error if agent tries to make themselves available outside of Business hour', async () => {
await makeDefaultBusinessHourActiveAndClosed();
afterEach(async () => {
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'not-available', agentId: currentUser._id })
.expect(200);
});
const currentUser: ILivechatAgent = await getMe(agent2.credentials);
const currentStatus = currentUser.statusLivechat;
const newStatus = currentStatus === 'available' ? 'not-available' : 'available';
it('should be able to set a logged in users status to available', async () => {
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'available', agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', 'available');
});
});
it('should allow managers to set other agents status to available', async () => {
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: 'available', agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', 'available');
});
});
describe('outside of business hours', () => {
before(async () => {
await makeDefaultBusinessHourActiveAndClosed();
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: newStatus, agentId: currentUser._id })
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-business-hours-are-closed');
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'not-available', agentId: currentUser._id })
.expect(200);
});
});
it('should not allow managers to make other agents available outside business hour', async () => {
const currentUser: ILivechatAgent = await getMe(agent2.credentials);
const currentStatus = currentUser.statusLivechat;
const newStatus = currentStatus === 'available' ? 'not-available' : 'available';
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: newStatus, agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', currentStatus);
after(async () => {
await disableDefaultBusinessHour();
});
await disableDefaultBusinessHour();
it('should throw an error if agent tries to make themselves available outside of Business hour', async () => {
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'available', agentId: currentUser._id })
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-business-hours-are-closed');
});
});
it('should return success but not change the agent when admin make try other agents available outside business hour', async () => {
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: 'available', agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', 'not-available');
});
});
});
});
});

Loading…
Cancel
Save