From 146a02319d06dbe77536ef4492edf636f4b2252e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 27 Jul 2026 13:57:25 +0200 Subject: [PATCH] refactor(medications): give the in-use clock list one owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The supply tab carried its own copy of the two container kinds whose first opening starts a degradation clock, with a comment saying it mirrored the state machine's list. Nothing enforced the mirror โ€” the state machine types itself against the generated Prisma client, which the browser bundle cannot carry, so a copy was the only way to say it. Move the list into a client-safe module with no imports and let both sides read it. The state machine re-exports it under the Prisma enum, so a renamed or dropped container kind is now a compile error rather than a member that quietly stops matching. --- .../sections/inventory-section.tsx | 12 ++----- .../inventory/clock-container-types.ts | 33 +++++++++++++++++++ .../medications/inventory/state-machine.ts | 19 +++++++---- 3 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 src/lib/medications/inventory/clock-container-types.ts diff --git a/src/components/medications/sections/inventory-section.tsx b/src/components/medications/sections/inventory-section.tsx index 6473f207f..c31979542 100644 --- a/src/components/medications/sections/inventory-section.tsx +++ b/src/components/medications/sections/inventory-section.tsx @@ -75,6 +75,7 @@ import { apiPost, apiPut, } from "@/lib/api/api-fetch"; +import { startsInUseClock } from "@/lib/medications/inventory/clock-container-types"; import type { SupplySummary } from "@/lib/medications/inventory/summary"; /** @@ -169,15 +170,6 @@ const STATE_BADGE: Record< USED_UP: "outline", }; -/** - * Container kinds whose first opening starts a 30-day degradation clock. - * Mirrors `IN_USE_CLOCK_CONTAINER_TYPES` in - * `src/lib/medications/inventory/state-machine.ts` โ€” the server owns the - * transition; the client only needs to know which helper text to show - * beside the opening date. - */ -const CLOCK_CONTAINER_TYPES: readonly ContainerType[] = ["PEN", "AMPOULE"]; - /** Default container kind for the register flow, from the delivery form. */ function defaultContainerType(deliveryForm: string | undefined): ContainerType { if (deliveryForm === "INJECTION") return "PEN"; @@ -971,7 +963,7 @@ export function AdjustInventoryDialog({ id="inventory-edit-opened-helper" className="text-muted-foreground text-xs" > - {CLOCK_CONTAINER_TYPES.includes(item.containerType) + {startsInUseClock(item.containerType) ? t("medications.detail.bestand.editOpenedHelperClock") : t("medications.detail.bestand.editOpenedHelper")}

diff --git a/src/lib/medications/inventory/clock-container-types.ts b/src/lib/medications/inventory/clock-container-types.ts new file mode 100644 index 000000000..ed37c367f --- /dev/null +++ b/src/lib/medications/inventory/clock-container-types.ts @@ -0,0 +1,33 @@ +/** + * The one owner of "which container kinds start a degradation clock when + * they are first opened". + * + * The state machine decides the transition and the settings client decides + * which helper text to put beside the opening date. Both need the same + * list, but the state machine types itself against the generated Prisma + * client, which a browser bundle cannot carry โ€” so the client kept its own + * copy of the two literals with a comment promising they matched. Nothing + * held the promise, and a release spent fixing a fact that drifted is a bad + * place to keep a second one. + * + * So the list lives here: string literals and a pure predicate, no imports, + * safe on either side of the boundary. `state-machine.ts` re-exports it + * under the Prisma enum (`satisfies`), which turns a renamed or dropped + * container kind into a compile error rather than a member that silently + * stops matching. The per-kind reasoning stays in the state machine's + * docblock, where the transition it drives is. + */ + +export const IN_USE_CLOCK_CONTAINER_TYPES = ["PEN", "AMPOULE"] as const; + +/** + * Does opening this container start the in-use window? Takes a loose + * `string` so the client can ask about a container kind it read off the + * wire without narrowing first; an unrecognised kind answers `false`, + * which is the honest side (no deadline nobody entered). + */ +export function startsInUseClock(containerType: string): boolean { + return (IN_USE_CLOCK_CONTAINER_TYPES as readonly string[]).includes( + containerType, + ); +} diff --git a/src/lib/medications/inventory/state-machine.ts b/src/lib/medications/inventory/state-machine.ts index 10b49884c..e3c5ca27f 100644 --- a/src/lib/medications/inventory/state-machine.ts +++ b/src/lib/medications/inventory/state-machine.ts @@ -79,6 +79,10 @@ import type { MedicationContainerType, MedicationInventoryState, } from "@/generated/prisma/client"; +import { + IN_USE_CLOCK_CONTAINER_TYPES as CLOCK_CONTAINER_TYPES, + startsInUseClock, +} from "./clock-container-types"; /** Default in-use window per EMA EPAR ยง6.3 for the strictest GLP-1 * agonists (Mounjaro, Saxenda). 30 days. */ @@ -90,17 +94,18 @@ export const DEFAULT_IN_USE_WINDOW_DAYS = 30; * Every other kind holds individually sealed units (or an unknown * arrangement) and expires only on its printed date. See the module * docblock for the per-member reasoning. + * + * The list itself lives in `./clock-container-types`, which the settings + * client can import; `satisfies` here is the drift guard, so a container + * kind renamed or dropped from the column stops compiling instead of + * quietly never matching. */ -export const IN_USE_CLOCK_CONTAINER_TYPES = [ - "PEN", - "AMPOULE", -] as const satisfies readonly MedicationContainerType[]; +export const IN_USE_CLOCK_CONTAINER_TYPES = + CLOCK_CONTAINER_TYPES satisfies readonly MedicationContainerType[]; /** Does opening this container start the in-use window? */ export function hasInUseClock(containerType: MedicationContainerType): boolean { - return (IN_USE_CLOCK_CONTAINER_TYPES as readonly string[]).includes( - containerType, - ); + return startsInUseClock(containerType); } const MS_PER_DAY = 24 * 60 * 60 * 1000;