mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(medications): accept and return per-schedule units-per-dose
Add schedules[].unitsPerDose to the create/update contract with the same whole-number-or-curated-fraction validation as the medication level, built into the Prisma data field-by-field on both the create and the schedule-replace path. Unwrap the nullable Decimal to a JSON number at every medication read/write response through one shared serializer so the wire shape cannot drift, keeping NULL as NULL to mean inherit-the-medication-level.
This commit is contained in:
1 parent
820dde2365
commit
88123c53da
5 files changed
+67
-2
No files matched your search
@@ -33,6 +33,7 @@ import {
|
||||
recomputeMedicationComplianceForDay,
|
||||
} from "@/lib/rollups/medication-compliance-rollups";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto";
|
||||
import { hhmmToMinutesOrNull } from "@/lib/medications/scheduling/hhmm";
|
||||
import { getUserTodayBounds } from "@/lib/tz/local-day";
|
||||
import { NextRequest } from "next/server";
|
||||
@@ -179,6 +180,7 @@ export const GET = apiHandler(
|
||||
return apiSuccess({
|
||||
...medication,
|
||||
unitsPerDose: Number(medication.unitsPerDose),
|
||||
schedules: serializeScheduleUnitsPerDose(medication.schedules),
|
||||
category,
|
||||
nextDueAt: display ? display.at.toISOString() : null,
|
||||
nextDueOverdue: display?.overdue ?? false,
|
||||
@@ -404,6 +406,12 @@ export const PUT = apiHandler(
|
||||
windowEnd: window.windowEnd,
|
||||
label: s.label ?? null,
|
||||
dose: s.dose ?? null,
|
||||
// #219 — per-schedule inventory units, field-by-field. A schedule
|
||||
// replace re-creates the rows, so absent leaves the column NULL
|
||||
// and the consume hook inherits the medication-level value.
|
||||
...(s.unitsPerDose !== undefined && {
|
||||
unitsPerDose: s.unitsPerDose,
|
||||
}),
|
||||
daysOfWeek: serializedDaysOfWeek,
|
||||
// v1.5 first-class times-of-day.
|
||||
timesOfDay: effectiveTimesOfDay,
|
||||
@@ -828,6 +836,7 @@ export const PUT = apiHandler(
|
||||
return apiSuccess({
|
||||
...medication,
|
||||
unitsPerDose: Number(medication.unitsPerDose),
|
||||
schedules: serializeScheduleUnitsPerDose(medication.schedules),
|
||||
category: normalizedCategory,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { annotate, getEvent } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import { serializeScheduleRecurrence } from "@/lib/medication-schedule";
|
||||
import { invalidateUserMedications } from "@/lib/cache/invalidate";
|
||||
import { readMedicationsListCached } from "@/lib/medications/list-read";
|
||||
import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
// v1.32.25 — blast-radius cap on externally-mirrored medications per user.
|
||||
@@ -66,7 +68,11 @@ export const GET = apiHandler(async () => {
|
||||
* nothing was written.
|
||||
*/
|
||||
async function respondWithExistingMirror(
|
||||
medication: Record<string, unknown> & { id: string; unitsPerDose: unknown },
|
||||
medication: Record<string, unknown> & {
|
||||
id: string;
|
||||
unitsPerDose: unknown;
|
||||
schedules: Array<{ unitsPerDose: Prisma.Decimal | null }>;
|
||||
},
|
||||
): Promise<Response> {
|
||||
let category = "OTHER";
|
||||
try {
|
||||
@@ -88,6 +94,8 @@ async function respondWithExistingMirror(
|
||||
return apiSuccess({
|
||||
...medication,
|
||||
unitsPerDose: Number(medication.unitsPerDose),
|
||||
// #219 — Decimal → number for the per-schedule column too.
|
||||
schedules: serializeScheduleUnitsPerDose(medication.schedules),
|
||||
category,
|
||||
});
|
||||
}
|
||||
@@ -311,6 +319,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
windowEnd: s.windowEnd,
|
||||
label: s.label ?? null,
|
||||
dose: s.dose ?? null,
|
||||
// #219 — per-schedule inventory units. Field-by-field (no mass
|
||||
// assignment); absent leaves the column NULL so the consume
|
||||
// hook inherits the medication-level units_per_dose.
|
||||
...(s.unitsPerDose !== undefined && {
|
||||
unitsPerDose: s.unitsPerDose,
|
||||
}),
|
||||
daysOfWeek: serializeScheduleRecurrence({
|
||||
daysOfWeek: s.daysOfWeek ?? [],
|
||||
intervalWeeks: s.intervalWeeks ?? 1,
|
||||
@@ -398,6 +412,8 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
{
|
||||
...medication,
|
||||
unitsPerDose: Number(medication.unitsPerDose),
|
||||
// #219 — Decimal → number for the per-schedule column too.
|
||||
schedules: serializeScheduleUnitsPerDose(medication.schedules),
|
||||
category: normalizedCategory,
|
||||
},
|
||||
201,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate, getEvent } from "@/lib/logging/context";
|
||||
import { getMedicationCategories } from "@/lib/medication-category";
|
||||
import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto";
|
||||
import {
|
||||
computeDisplayDue,
|
||||
OVERDUE_LOOKBACK_MS,
|
||||
@@ -255,6 +256,8 @@ export async function buildMedicationsList(
|
||||
// v1.16.12 — Decimal → number so the wire stays a JSON number, not
|
||||
// the string Prisma would otherwise serialise a Decimal to.
|
||||
unitsPerDose: Number(m.unitsPerDose),
|
||||
// #219 — same Decimal → number unwrap for the per-schedule column.
|
||||
schedules: serializeScheduleUnitsPerDose(m.schedules),
|
||||
category: categoryMap[m.id] ?? "OTHER",
|
||||
// v1.32.25 — provenance echo. Surfacing the mirror source lets the
|
||||
// web UI and an operator tell an externally-mirrored row (today only
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* #219 — per-schedule `unitsPerDose` wire serialisation.
|
||||
*
|
||||
* The column is a nullable `Decimal(10,4)`; Prisma serialises a Decimal to a
|
||||
* JSON STRING, which the client would then have to coerce. The medication-level
|
||||
* `unitsPerDose` is already unwrapped to a JSON number at every response point
|
||||
* (`Number(medication.unitsPerDose)`), so mirror that for the per-schedule
|
||||
* column: convert to a number, keep NULL as NULL (NULL means "inherit the
|
||||
* medication level"). One helper so the five medication read/write responses
|
||||
* cannot drift on the shape.
|
||||
*/
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
|
||||
type ScheduleWithUnits = { unitsPerDose: Prisma.Decimal | null };
|
||||
|
||||
export function serializeScheduleUnitsPerDose<T extends ScheduleWithUnits>(
|
||||
schedules: readonly T[],
|
||||
): Array<Omit<T, "unitsPerDose"> & { unitsPerDose: number | null }> {
|
||||
return schedules.map((schedule) => ({
|
||||
...schedule,
|
||||
unitsPerDose:
|
||||
schedule.unitsPerDose === null ? null : Number(schedule.unitsPerDose),
|
||||
}));
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { SCHEDULE_TYPES } from "@/lib/medications/scheduling/recurrence";
|
||||
import { RRULE_PROPS, doseWindowEntrySchema, timeRegex } from "./base";
|
||||
import {
|
||||
RRULE_PROPS,
|
||||
UNITS_PER_DOSE_MESSAGE,
|
||||
doseWindowEntrySchema,
|
||||
isSupportedUnitsPerDose,
|
||||
timeRegex,
|
||||
} from "./base";
|
||||
|
||||
export const scheduleSchema = z
|
||||
.object({
|
||||
@@ -29,6 +35,13 @@ export const scheduleSchema = z
|
||||
.describe(
|
||||
"Per-schedule dose override. NULL means the schedule inherits `Medication.dose`.",
|
||||
),
|
||||
unitsPerDose: z
|
||||
.number()
|
||||
.refine(isSupportedUnitsPerDose, { message: UNITS_PER_DOSE_MESSAGE })
|
||||
.optional()
|
||||
.describe(
|
||||
"Per-schedule inventory units consumed per dose (#219). A whole number 1-100 or a supported fraction (¼ / ⅓ / ½ / ⅔ / ¾) for a split pill. Omitted / NULL means the schedule inherits `Medication.unitsPerDose`. Lets one medication decrement a different tablet count at different times of day (a whole tablet in the morning, a half at noon).",
|
||||
),
|
||||
daysOfWeek: z
|
||||
.array(z.number().int().min(0).max(6))
|
||||
.optional()
|
||||
|
||||
Reference in new issue
Block a user