{"record":{"id":"767725af5b3add68","repo":"RocketChat/Rocket.Chat","slug":"error-business-hour-finish-time-equals-start-time","errorCode":"error-business-hour-finish-time-equals-start-time","errorMessage":"error-business-hour-finish-time-equals-start-time","messagePattern":"error-business-hour-finish-time-equals-start-time","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/lib/omnichannel/business-hour/AbstractBusinessHour.ts","lineNumber":103,"sourceCode":"\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 = {\n\t\t\t\ttime: hour.finish,\n\t\t\t\tutc: {\n\t\t\t\t\tdayOfWeek: finishUtc.clone().format('dddd'),\n\t\t\t\t\ttime: finishUtc.clone().format('HH:mm'),","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/lib/omnichannel/business-hour/AbstractBusinessHour.ts#L85-L121","documentation":"Companion check in AbstractBusinessHour.convertWorkHours: an open work day whose start and finish map to the same UTC instant (e.g. 00:00 to 00:00) throws Error('error-business-hour-finish-time-equals-start-time'). A zero-length open period is treated as a configuration mistake; marking the day closed (open=false) is the supported way to express no service.","triggerScenarios":"Saving a business hour with open=true and identical start/finish values - commonly both '00:00' left at UI defaults - including cases where timezone conversion or rounding maps two distinct local times onto the same UTC instant.","commonSituations":"Admin UI defaults (00:00/00:00) left untouched on an 'open' day; copy-pasting a template day onto others; DST or timezone offsets collapsing a short interval (e.g. 30 minutes) to a single instant.","solutions":["Set a real non-zero interval, or mark the day closed (open=false)","Sanitize work-hours payloads in integrations: convert empty/zero intervals to open=false before submission","Verify the business hour's timezone when distinct local times collapse to the same UTC time"],"exampleFix":"// before - open day with zero length\nworkHours: [{ day: 'Monday', open: true, start: '00:00', finish: '00:00' }]\n\n// after - explicitly closed day\nworkHours: [{ day: 'Monday', open: false, start: '00:00', finish: '00:00' }]","handlingStrategy":"validation","validationCode":"import moment from 'moment-timezone';\n\n// In addition to the finish-after-start check, reject identical instants:\nconst isZeroLength = (h: { day: string; open: boolean; start: string; finish: string }, tz: string): boolean => {\n  if (!h.open) return false;\n  const s = moment.tz(`${h.day}:${h.start}`, 'dddd:HH:mm', tz).utc();\n  const f = moment.tz(`${h.day}:${h.finish}`, 'dddd:HH:mm', tz).utc();\n  return s.isSame(f);\n};\n\nif (businessHour.workHours.some((h) => isZeroLength(h, businessHour.timezone.name))) {\n  // mark those days closed (open=false) instead of submitting zero-length open intervals\n}","typeGuard":"const hasValidIntervals = (workHours: { day: string; open: boolean; start: string; finish: string }[], tz: string): boolean =>\n  workHours.every((h) => {\n    if (!h.open) return true;\n    const s = moment.tz(`${h.day}:${h.start}`, 'dddd:HH:mm', tz).utc();\n    const f = moment.tz(`${h.day}:${h.finish}`, 'dddd:HH:mm', tz).utc();\n    return f.isAfter(s); // excludes both inverted and identical instants\n  });","tryCatchPattern":null,"preventionTips":["Default new work-hour rows to open=false so untouched 00:00-00:00 rows never submit as open","When duplicating days, verify start and finish differ on every open row","Express 'closed all day' as open=false, never as a zero-length open interval"],"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-14T00:17:10.932Z"}