fix(meds): tighten the v1.5 schedule schema invariants

Adds three Zod refines that close gaps the audit surfaced: one-shot
medications now require a startsOn, course windows refuse endsOn
before startsOn, and rolling-cadence schedules cap timesOfDay at one
entry. The wizard's reminders toggle is now part of the create
payload so the user's choice actually ships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-28 10:23:28 +02:00
1 parent b4b099003f
commit be7a2f135f
3 files changed
+43

No files matched your search

+2
View File
@@ -1663,6 +1663,8 @@ components:
type: integer
minimum: 1
maximum: 100
notificationsEnabled:
type: boolean
startsOn:
description: Date the medication course begins (ISO `YYYY-MM-DD`). Anchors RRULE BYDAY / BYMONTHDAY patterns and the
rolling-interval countdown's first window. NULL = active from creation (the legacy implicit behaviour).
+5
View File
@@ -121,6 +121,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
category,
treatmentClass,
dosesPerUnit,
notificationsEnabled,
schedules,
startsOn,
endsOn,
@@ -180,6 +181,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
// wire schema; Prisma fills the default GENERIC when omitted.
...(treatmentClass !== undefined && { treatmentClass }),
...(dosesPerUnit !== undefined && { dosesPerUnit }),
// v1.5 — wizard's reminders toggle now ships through the create
// payload (was orphaned in the initial diff). Prisma defaults to
// true when omitted, matching the legacy form's behaviour.
...(notificationsEnabled !== undefined && { notificationsEnabled }),
// v1.5 scheduling primitives — pass-through when supplied.
...(startsOn !== undefined && { startsOn }),
...(normalisedEndsOn !== undefined && { endsOn: normalisedEndsOn }),
+36
View File
@@ -143,6 +143,17 @@ export const scheduleSchema = z
path: ["rrule"],
},
)
.refine(
(s) =>
s.rollingIntervalDays === undefined ||
s.rollingIntervalDays === null ||
!s.timesOfDay ||
s.timesOfDay.length <= 1,
{
message: "rolling-cadence schedules accept at most one time of day",
path: ["timesOfDay"],
},
)
.meta({
id: "MedicationScheduleInput",
description:
@@ -203,9 +214,22 @@ export const createMedicationSchema = z
treatmentClass: z.enum(MEDICATION_TREATMENT_CLASS_VALUES).optional(),
/** v1.4.25 W4d — doses per pen/vial for inventory tracking. */
dosesPerUnit: z.number().int().min(1).max(100).optional(),
notificationsEnabled: z.boolean().optional(),
...courseWindowFields,
schedules: z.array(scheduleSchema).min(1, "Mindestens ein Zeitfenster"),
})
.refine((b) => b.oneShot !== true || !!b.startsOn, {
message: "startsOn is required when oneShot is true",
path: ["startsOn"],
})
.refine(
(b) =>
!b.startsOn || !b.endsOn || b.endsOn.getTime() >= b.startsOn.getTime(),
{
message: "endsOn must be on or after startsOn",
path: ["endsOn"],
},
)
.meta({
id: "CreateMedicationRequest",
description:
@@ -224,6 +248,18 @@ export const updateMedicationSchema = z
...courseWindowFields,
schedules: z.array(scheduleSchema).optional(),
})
.refine((b) => b.oneShot !== true || !!b.startsOn, {
message: "startsOn is required when oneShot is true",
path: ["startsOn"],
})
.refine(
(b) =>
!b.startsOn || !b.endsOn || b.endsOn.getTime() >= b.startsOn.getTime(),
{
message: "endsOn must be on or after startsOn",
path: ["endsOn"],
},
)
.meta({
id: "UpdateMedicationRequest",
description: