{"record":{"id":"276e08eb3e421127","repo":"calcom/cal.diy","slug":"to-must-not-be-before-from","errorCode":null,"errorMessage":"'to' must not be before 'from'","messagePattern":"'to' must not be before 'from'","errorType":"validation","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"apps/api/v2/src/modules/cal-unified-calendars/inputs/freebusy-unified.input.ts","lineNumber":19,"sourceCode":"import { BadRequestException } from \"@nestjs/common\";\nimport { ApiProperty, ApiPropertyOptional } from \"@nestjs/swagger\";\nimport {\n  IsISO8601,\n  IsOptional,\n  IsTimeZone,\n  Validate,\n  ValidationArguments,\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from \"class-validator\";\n\n@ValidatorConstraint({ name: \"isAfterFrom\", async: false })\nclass IsAfterFrom implements ValidatorConstraintInterface {\n  validate(to: string, args: ValidationArguments) {\n    const obj = args.object as { from?: string };\n    if (!obj.from || !to) return true;\n    if (new Date(to).getTime() < new Date(obj.from).getTime()) {\n      throw new BadRequestException(\"'to' must not be before 'from'\");\n    }\n    return true;\n  }\n  defaultMessage() {\n    return \"'to' must not be before 'from'\";\n  }\n}\n\nexport class FreebusyUnifiedInput {\n  @IsISO8601()\n  @ApiProperty({\n    type: String,\n    description: \"Start of the date range (ISO 8601 date or date-time)\",\n    example: \"2026-03-10\",\n  })\n  from!: string;\n\n  @IsISO8601()","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/cal-unified-calendars/inputs/freebusy-unified.input.ts#L1-L37","documentation":"A class-validator custom constraint (IsAfterFrom) on FreebusyUnifiedInput throws a BadRequestException when the `to` field, parsed as a Date, is earlier than the `from` field. The validator deliberately throws rather than returning false, so the message reads exactly as written. The fields are validated as ISO8601 strings by @IsISO8601 first.","triggerScenarios":"POST /v2/.../free-busy (or any endpoint accepting FreebusyUnifiedInput) with body { from: '2026-03-10', to: '2026-03-09' }, or any payload where new Date(to).getTime() < new Date(from).getTime(). Also triggered by timezone-shifted date-times that make `to` fall before `from` in UTC.","commonSituations":"Client swaps from/to; timezone conversion bug makes a 23:00 `to` slip to the previous UTC day; copy-paste of the same date-time on both sides with millisecond differences.","solutions":["Ensure the client sends `from` <= `to` (preferably from < to).","Validate the range client-side before the request: `if (new Date(to) < new Date(from)) throw ...`.","When converting local times to ISO, convert both ends through the same timezone so they stay ordered."],"exampleFix":"// before\nconst body = { from: '2026-03-10T23:00:00Z', to: '2026-03-10T01:00:00Z' };\n\n// after\nconst body = { from: '2026-03-10T01:00:00Z', to: '2026-03-10T23:00:00Z' };","handlingStrategy":"validation","validationCode":"function isValidRange(from: string, to: string): boolean {\n  const f = new Date(from).getTime();\n  const t = new Date(to).getTime();\n  return !Number.isNaN(f) && !Number.isNaN(t) && t >= f;\n}\nif (!isValidRange(body.from, body.to)) throw new Error(\"'to' must not be before 'from'\");","typeGuard":"function isISO8601Pair(v: unknown): v is { from: string; to: string } {\n  if (typeof v !== 'object' || v === null) return false;\n  const o = v as any;\n  return typeof o.from === 'string' && typeof o.to === 'string'\n    && !Number.isNaN(new Date(o.from).getTime())\n    && !Number.isNaN(new Date(o.to).getTime());\n}","tryCatchPattern":null,"preventionTips":["Validate from <= to client-side before submitting.","Convert both endpoints through the same timezone when building the ISO strings.","Use date-time strings (not date-only) when crossing timezones to avoid off-by-one days."],"tags":["validation","class-validator","freebusy","date","api-v2"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}