{"record":{"id":"1a9291877086f4e7","repo":"RocketChat/Rocket.Chat","slug":"error-business-hour-finish-time-before-start-time","errorCode":"error-business-hour-finish-time-before-start-time","errorMessage":"error-business-hour-finish-time-before-start-time","messagePattern":"error-business-hour-finish-time-before-start-time","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/lib/omnichannel/business-hour/AbstractBusinessHour.ts","lineNumber":99,"sourceCode":"\t\tbusinessHourData.active = Boolean(businessHourData.active);\n\t\tbusinessHourData = this.convertWorkHours(businessHourData);\n\t\tif (businessHourData._id) {\n\t\t\tawait this.BusinessHourRepository.updateOne({ _id: businessHourData._id }, {\n\t\t\t\t$set: businessHourData,\n\t\t\t} as UpdateFilter<ILivechatBusinessHour>); // TODO: Remove this cast when TypeScript is updated\n\t\t\treturn businessHourData._id;\n\t\t}\n\t\tconst { insertedId } = await this.BusinessHourRepository.insertOne(businessHourData);\n\t\treturn insertedId;\n\t}\n\n\tprivate convertWorkHours(businessHourData: ILivechatBusinessHour): ILivechatBusinessHour {\n\t\tbusinessHourData.workHours.forEach((hour: any) => {\n\t\t\tconst startUtc = moment.tz(`${hour.day}:${hour.start}`, 'dddd:HH:mm', businessHourData.timezone.name).utc();\n\t\t\tconst finishUtc = moment.tz(`${hour.day}:${hour.finish}`, 'dddd:HH:mm', businessHourData.timezone.name).utc();\n\n\t\t\tif (hour.open && finishUtc.isBefore(startUtc)) {\n\t\t\t\tthrow new Error('error-business-hour-finish-time-before-start-time');\n\t\t\t}\n\n\t\t\tif (hour.open && startUtc.isSame(finishUtc)) {\n\t\t\t\tthrow new Error('error-business-hour-finish-time-equals-start-time');\n\t\t\t}\n\n\t\t\thour.start = {\n\t\t\t\ttime: hour.start,\n\t\t\t\tutc: {\n\t\t\t\t\tdayOfWeek: startUtc.clone().format('dddd'),\n\t\t\t\t\ttime: startUtc.clone().format('HH:mm'),\n\t\t\t\t},\n\t\t\t\tcron: {\n\t\t\t\t\tdayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'dddd'),\n\t\t\t\t\ttime: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'HH:mm'),\n\t\t\t\t},\n\t\t\t};\n\t\t\thour.finish = {","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/lib/omnichannel/business-hour/AbstractBusinessHour.ts#L81-L117","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Fix the entry so finish is after start in the business hour's own timezone","Split overnight coverage into two entries (e.g. open until 23:59, reopen at 00:00) since inverted ranges are rejected","Double-check the timezone selected on the business hour matches the timezone the times were entered in","For 24/7 coverage use the 24-hour open business-hour type instead of work-hour entries"],"exampleFix":"// before - overnight span in one entry, throws after UTC conversion\nworkHours: [{ day: 'Monday', open: true, start: '22:00', finish: '02:00' }]\n\n// after - split into two non-inverted entries\nworkHours: [\n  { day: 'Monday', open: true, start: '00:00', finish: '02:00' },\n  { day: 'Monday', open: true, start: '22:00', finish: '23:59' },\n]","handlingStrategy":"validation","validationCode":"import moment from 'moment-timezone';\n\nconst validateWorkHours = (workHours: any[], tz: string) =>\n  workHours.every((h) => {\n    if (!h.open) return true;\n    const start = moment.tz(`${h.day}:${h.start}`, 'dddd:HH:mm', tz).utc();\n    const finish = moment.tz(`${h.day}:${h.finish}`, 'dddd:HH:mm', tz).utc();\n    return finish.isAfter(start);\n  });\n\n// before saving a business hour:\nif (!validateWorkHours(businessHour.workHours, businessHour.timezone.name)) {\n  // fix intervals or split overnight ranges - the server will reject the save\n}","typeGuard":"const isValidWorkHour = (hour: { day: string; open: boolean; start: string; finish: string }, tz: string): boolean => {\n  if (!hour.open) return true;\n  const s = moment.tz(`${hour.day}:${hour.start}`, 'dddd:HH:mm', tz).utc();\n  const f = moment.tz(`${hour.day}:${hour.finish}`, 'dddd:HH:mm', tz).utc();\n  return f.isAfter(s);\n};","tryCatchPattern":null,"preventionTips":["Never model overnight coverage as a single entry with finish before start - split it across two entries","Always set the business hour's timezone explicitly and enter times in that timezone","Mirror the server's UTC-conversion check in the admin UI before submit"],"tags":["omnichannel","business-hours","validation","timezone"],"backgroundTag":"time-range-validation-failed","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}