diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index 764ab9c8a..500a18a5d 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -104,12 +104,13 @@ export const eventDraftSchema = z ctx.addIssue({ code: "custom", message: "End must be after a valid start", path: ["end"] }); } const dateOnly = z.iso.date(); - const timed = /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/; - // Validate the calendar date separately so timed values can retain minute precision. + const timed = z.union([ + z.iso.datetime({ offset: true }), + z.iso.datetime({ offset: true, precision: -1 }), + ]); + // Keep minute precision while validating the clock time as well as the date. const validTimestamp = (timestamp: string) => - value.allDay - ? dateOnly.safeParse(timestamp).success - : timed.test(timestamp) && dateOnly.safeParse(timestamp.slice(0, 10)).success; + value.allDay ? dateOnly.safeParse(timestamp).success : timed.safeParse(timestamp).success; if (!validTimestamp(value.start) || !validTimestamp(value.end)) { ctx.addIssue({ code: "custom", diff --git a/tests/domain.test.ts b/tests/domain.test.ts index a397cd912..7c78849d8 100644 --- a/tests/domain.test.ts +++ b/tests/domain.test.ts @@ -129,6 +129,25 @@ test("events accept valid leap days, month ends, and explicit offsets", () => { } }); +test("timed events reject 24:00 instead of moving the appointment to the next day", () => { + for (const time of ["24:00", "24:00:00", "24:00:00.000"]) { + for (const offset of ["Z", "-07:00", "+05:30"]) { + for (const field of ["start", "end"] as const) { + assert.equal( + eventDraftSchema.safeParse({ + ...event, + start: "2000-01-01T10:00:00Z", + end: "2200-01-01T10:00:00Z", + [field]: `2026-10-10T${time}${offset}`, + }).success, + false, + `${field} must reject ${time}${offset}`, + ); + } + } + } +}); + test("events default calendarId, location, and description", () => { const parsed = eventDraftSchema.parse(event); assert.equal(parsed.calendarId, "primary");