RocketChat/Rocket.Chat · error · Error

error-business-hour-finish-time-before-start-time

error-business-hour-finish-time-before-start-time

Error message

error-business-hour-finish-time-before-start-time

What it means

When saving a business hour, AbstractBusinessHour.convertWorkHours converts each work day's start and finish from the business hour's timezone to UTC and rejects an open day whose finish instant is before its start instant, throwing Error('error-business-hour-finish-time-before-start-time'). This forbids intervals that are invalid or inverted after timezone conversion, which also rules out overnight spans modeled as a single entry.

Source

Thrown at apps/meteor/server/lib/omnichannel/business-hour/AbstractBusinessHour.ts:99

		businessHourData.active = Boolean(businessHourData.active);
		businessHourData = this.convertWorkHours(businessHourData);
		if (businessHourData._id) {
			await this.BusinessHourRepository.updateOne({ _id: businessHourData._id }, {
				$set: businessHourData,
			} as UpdateFilter<ILivechatBusinessHour>); // TODO: Remove this cast when TypeScript is updated
			return businessHourData._id;
		}
		const { insertedId } = await this.BusinessHourRepository.insertOne(businessHourData);
		return insertedId;
	}

	private convertWorkHours(businessHourData: ILivechatBusinessHour): ILivechatBusinessHour {
		businessHourData.workHours.forEach((hour: any) => {
			const startUtc = moment.tz(`${hour.day}:${hour.start}`, 'dddd:HH:mm', businessHourData.timezone.name).utc();
			const finishUtc = moment.tz(`${hour.day}:${hour.finish}`, 'dddd:HH:mm', businessHourData.timezone.name).utc();

			if (hour.open && finishUtc.isBefore(startUtc)) {
				throw new Error('error-business-hour-finish-time-before-start-time');
			}

			if (hour.open && startUtc.isSame(finishUtc)) {
				throw new Error('error-business-hour-finish-time-equals-start-time');
			}

			hour.start = {
				time: hour.start,
				utc: {
					dayOfWeek: startUtc.clone().format('dddd'),
					time: startUtc.clone().format('HH:mm'),
				},
				cron: {
					dayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'dddd'),
					time: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'HH:mm'),
				},
			};
			hour.finish = {

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Fix the entry so finish is after start in the business hour's own timezone
  2. Split overnight coverage into two entries (e.g. open until 23:59, reopen at 00:00) since inverted ranges are rejected
  3. Double-check the timezone selected on the business hour matches the timezone the times were entered in
  4. For 24/7 coverage use the 24-hour open business-hour type instead of work-hour entries

Example fix

// before - overnight span in one entry, throws after UTC conversion
workHours: [{ day: 'Monday', open: true, start: '22:00', finish: '02:00' }]

// after - split into two non-inverted entries
workHours: [
  { day: 'Monday', open: true, start: '00:00', finish: '02:00' },
  { day: 'Monday', open: true, start: '22:00', finish: '23:59' },
]
Defensive patterns

Strategy: validation

Validate before calling

import moment from 'moment-timezone';

const validateWorkHours = (workHours: any[], tz: string) =>
  workHours.every((h) => {
    if (!h.open) return true;
    const start = moment.tz(`${h.day}:${h.start}`, 'dddd:HH:mm', tz).utc();
    const finish = moment.tz(`${h.day}:${h.finish}`, 'dddd:HH:mm', tz).utc();
    return finish.isAfter(start);
  });

// before saving a business hour:
if (!validateWorkHours(businessHour.workHours, businessHour.timezone.name)) {
  // fix intervals or split overnight ranges - the server will reject the save
}

Type guard

const isValidWorkHour = (hour: { day: string; open: boolean; start: string; finish: string }, tz: string): boolean => {
  if (!hour.open) return true;
  const s = moment.tz(`${hour.day}:${hour.start}`, 'dddd:HH:mm', tz).utc();
  const f = moment.tz(`${hour.day}:${hour.finish}`, 'dddd:HH:mm', tz).utc();
  return f.isAfter(s);
};

Prevention

When it happens

Trigger: Saving a Livechat business hour whose workHours entry has open=true and finish earlier than start once converted to UTC - e.g. start 22:00 finish 02:00 in the business hour's timezone, or a locally-valid range that flips because the configured timezone differs from the one the operator entered times in.

Common situations: Overnight shifts entered as one entry (Mon 22:00 - Tue 06:00); business-hour timezone set to UTC while times were given in local time; DST transitions shifting an interval across midnight.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/1a9291877086f4e7. Report an issue: GitHub.