mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
fix(medications): release-QA findings on the inventory overhaul
- The legacy GLP-1 ledger reads again: GET and the Coach snapshot fall back to ledger-derived pen/dose figures when a medication has no inventory items; items always win once they exist. The delta branch on the POST is marked deprecated in favour of the inventory endpoints. - Expired containers count as available nowhere: one shared supply summary (ACTIVE/IN_USE with units) feeds the overview row, the Bestand summary, the list payload, the GLP-1 card, and the runway estimate; expired units surface as a muted suffix instead. - The inventory endpoints speak one wire dialect: request fields renamed to unitsTotal/unitsRemaining to match the responses, OpenAPI regenerated. - The reorder dialog mirrors the page's grouping: active and inactive sections, each sortable within itself, so a saved order can never interleave them. - Polish strings avoid the 2-4 plural trap, sub-dose quantities render as below one dose instead of approximately zero, and the table's name sort collates by the active locale instead of hardcoded German.
This commit is contained in:
1 parent
b765febc97
commit
9dc0ccd03a
28 files changed
+1017
-180
No files matched your search
@@ -32,6 +32,7 @@ function fakeMedication(overrides: Partial<Record<string, unknown>> = {}) {
|
||||
],
|
||||
unitsPerDose: 1,
|
||||
inventoryItems: [],
|
||||
inventoryEvents: [],
|
||||
intakeEvents: [],
|
||||
...overrides,
|
||||
};
|
||||
@@ -156,4 +157,47 @@ describe("buildGlp1SnapshotBlock", () => {
|
||||
weeksOfSupplyApprox: 3,
|
||||
});
|
||||
});
|
||||
|
||||
// W1 — ledger-only accounts (the pre-item delta writer) keep their
|
||||
// pen count in the Coach prompt instead of a silent omission.
|
||||
it("falls back to the legacy ledger when the medication has zero items", async () => {
|
||||
prismaMock.medication.findMany.mockResolvedValue([
|
||||
fakeMedication({
|
||||
dosesPerUnit: 4,
|
||||
inventoryItems: [],
|
||||
// +2 purchased, −1 used ⇒ 1 pen ⇒ 4 doses.
|
||||
inventoryEvents: [{ delta: 2 }, { delta: -1 }],
|
||||
}),
|
||||
]);
|
||||
const out = await buildGlp1SnapshotBlock("user-1");
|
||||
expect(out?.medications[0].penInventory).toEqual({
|
||||
pensRemaining: 1,
|
||||
dosesRemaining: 4,
|
||||
weeksOfSupplyApprox: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores the ledger whenever items exist (items win)", async () => {
|
||||
prismaMock.medication.findMany.mockResolvedValue([
|
||||
fakeMedication({
|
||||
dosesPerUnit: 4,
|
||||
unitsPerDose: 1,
|
||||
inventoryItems: [{ state: "ACTIVE", unitsTotal: 4, unitsRemaining: 4 }],
|
||||
// Stale delta history must not override the per-item truth.
|
||||
inventoryEvents: [{ delta: 9 }],
|
||||
}),
|
||||
]);
|
||||
const out = await buildGlp1SnapshotBlock("user-1");
|
||||
expect(out?.medications[0].penInventory).toEqual({
|
||||
pensRemaining: 1,
|
||||
dosesRemaining: 4,
|
||||
weeksOfSupplyApprox: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it("omits pen inventory when neither items nor ledger rows exist", async () => {
|
||||
prismaMock.medication.findMany.mockResolvedValue([fakeMedication()]);
|
||||
const out = await buildGlp1SnapshotBlock("user-1");
|
||||
expect(out?.medications[0].penInventory).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -267,8 +267,12 @@ export async function buildGlp1SnapshotBlock(
|
||||
schedules: true,
|
||||
doseChanges: { orderBy: { effectiveFrom: "asc" } },
|
||||
// v1.16.10 — pen inventory reads the per-item entities (the rows
|
||||
// the intake consumption hook moves), not the legacy event ledger.
|
||||
// the intake consumption hook moves). The legacy event ledger
|
||||
// stays as a READ fallback for ledger-only accounts: when a
|
||||
// medication has zero items, the running-sum numbers surface so
|
||||
// the Coach still knows the pen count.
|
||||
inventoryItems: { orderBy: { createdAt: "asc" } },
|
||||
inventoryEvents: { orderBy: { occurredAt: "asc" } },
|
||||
intakeEvents: {
|
||||
where: { takenAt: { not: null } },
|
||||
orderBy: { takenAt: "desc" },
|
||||
@@ -371,14 +375,21 @@ export async function buildGlp1SnapshotBlock(
|
||||
const nextInjection = predictNextInjection(schedule, lastInjection, now);
|
||||
|
||||
// Inventory math over the per-item entities (v1.16.10 — the same
|
||||
// rows the Bestand tab and the consumption hook move; the legacy
|
||||
// event ledger no longer receives writes). `pensRemaining` counts
|
||||
// usable containers; `dosesRemaining` pools the units and divides
|
||||
// by `unitsPerDose` so the Coach can answer "how long until my
|
||||
// next refill?".
|
||||
// rows the Bestand tab and the consumption hook move).
|
||||
// `pensRemaining` counts usable containers; `dosesRemaining` pools
|
||||
// the units and divides by `unitsPerDose` so the Coach can answer
|
||||
// "how long until my next refill?". Ledger-only accounts (the
|
||||
// pre-item delta writer) fall back to the running sum — items win
|
||||
// whenever any exist.
|
||||
let dosesRemaining: number | null = null;
|
||||
let pensRemaining: number | null = null;
|
||||
let weeksOfSupplyApprox: number | null = null;
|
||||
const dosesPerWeek =
|
||||
schedule?.cadence === "weekly"
|
||||
? 1
|
||||
: schedule?.cadence === "daily"
|
||||
? 7
|
||||
: 1;
|
||||
if (med.inventoryItems.length > 0) {
|
||||
const usable = med.inventoryItems.filter(
|
||||
(item) =>
|
||||
@@ -393,12 +404,13 @@ export async function buildGlp1SnapshotBlock(
|
||||
dosesRemaining = Math.floor(
|
||||
unitsRemaining / Math.max(1, med.unitsPerDose),
|
||||
);
|
||||
const dosesPerWeek =
|
||||
schedule?.cadence === "weekly"
|
||||
? 1
|
||||
: schedule?.cadence === "daily"
|
||||
? 7
|
||||
: 1;
|
||||
weeksOfSupplyApprox = Math.round(dosesRemaining / dosesPerWeek);
|
||||
} else if (med.dosesPerUnit && med.inventoryEvents.length > 0) {
|
||||
// Legacy-ledger fallback — the pre-item contract verbatim: the
|
||||
// delta sum counts pens, `dosesPerUnit` maps pens to doses.
|
||||
const pens = med.inventoryEvents.reduce((sum, ev) => sum + ev.delta, 0);
|
||||
pensRemaining = Math.max(0, pens);
|
||||
dosesRemaining = pensRemaining * med.dosesPerUnit;
|
||||
weeksOfSupplyApprox = Math.round(dosesRemaining / dosesPerWeek);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* v1.16.10 — shared supply-summary math (`summariseSupply`).
|
||||
*
|
||||
* Pins the release-QA disagreement scenario: a single EXPIRED pen with
|
||||
* units left must read as ZERO available supply on every surface
|
||||
* (Übersicht supply row, Bestand summary, list payload, GLP-1 endpoint)
|
||||
* while the expired units stay visible separately. Both detail-page
|
||||
* surfaces render from this helper, so the predicate cannot drift from
|
||||
* the list / GLP-1 one (ACTIVE / IN_USE with units only).
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
isAvailableSupply,
|
||||
summariseSupply,
|
||||
} from "@/lib/medications/inventory/summary";
|
||||
|
||||
describe("summariseSupply — expired stock is visible but never available", () => {
|
||||
it("one expired pen with units ⇒ 0 available, expired units surfaced separately", () => {
|
||||
const out = summariseSupply(
|
||||
[{ state: "EXPIRED", unitsTotal: 4, unitsRemaining: 4 }],
|
||||
1,
|
||||
);
|
||||
expect(out).toEqual({
|
||||
unitsRemaining: 0,
|
||||
unitsTotal: 0,
|
||||
dosesRemaining: 0,
|
||||
dosesTotal: 0,
|
||||
expiredUnits: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it("pools only ACTIVE / IN_USE containers with units left", () => {
|
||||
const out = summariseSupply(
|
||||
[
|
||||
{ state: "IN_USE", unitsTotal: 4, unitsRemaining: 3 },
|
||||
{ state: "ACTIVE", unitsTotal: 4, unitsRemaining: 4 },
|
||||
{ state: "USED_UP", unitsTotal: 4, unitsRemaining: 0 },
|
||||
{ state: "EXPIRED", unitsTotal: 4, unitsRemaining: 4 },
|
||||
],
|
||||
2,
|
||||
);
|
||||
// 7 available units / 2 per dose = 3 doses; capacity 8 units = 4 doses.
|
||||
expect(out).toEqual({
|
||||
unitsRemaining: 7,
|
||||
unitsTotal: 8,
|
||||
dosesRemaining: 3,
|
||||
dosesTotal: 4,
|
||||
expiredUnits: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it("a drained (used-up) container counts nowhere", () => {
|
||||
const out = summariseSupply(
|
||||
[{ state: "USED_UP", unitsTotal: 4, unitsRemaining: 0 }],
|
||||
1,
|
||||
);
|
||||
expect(out).toEqual({
|
||||
unitsRemaining: 0,
|
||||
unitsTotal: 0,
|
||||
dosesRemaining: 0,
|
||||
dosesTotal: 0,
|
||||
expiredUnits: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("matches the list-route availability predicate per state", () => {
|
||||
expect(
|
||||
isAvailableSupply({ state: "ACTIVE", unitsTotal: 4, unitsRemaining: 4 }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isAvailableSupply({ state: "IN_USE", unitsTotal: 4, unitsRemaining: 1 }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isAvailableSupply({ state: "EXPIRED", unitsTotal: 4, unitsRemaining: 4 }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isAvailableSupply({ state: "USED_UP", unitsTotal: 4, unitsRemaining: 0 }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isAvailableSupply({ state: "ACTIVE", unitsTotal: 4, unitsRemaining: 0 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("supply surfaces — both detail-page readouts ride the shared helper (source guard)", () => {
|
||||
const read = (rel: string) =>
|
||||
readFileSync(join(process.cwd(), rel), "utf8");
|
||||
|
||||
it("the Übersicht supply row sums through summariseSupply and feeds the runway with the available-only figure", () => {
|
||||
const src = read(
|
||||
"src/components/medications/detail/MedicationDetailTabs.tsx",
|
||||
);
|
||||
expect(src).toContain('from "@/lib/medications/inventory/summary"');
|
||||
expect(src).toMatch(/summariseSupply\(inventoryItems, perDose\)/);
|
||||
expect(src).toMatch(
|
||||
/estimateRunwayDays\(dosesRemaining, medication\.schedules\)/,
|
||||
);
|
||||
// No hand-rolled "everything but USED_UP" filter remains.
|
||||
expect(src).not.toContain('state !== "USED_UP"');
|
||||
expect(src).toContain("medications.detail.bestand.expiredSuffix");
|
||||
});
|
||||
|
||||
it("the Bestand summary sums through summariseSupply and surfaces the expired suffix", () => {
|
||||
const src = read(
|
||||
"src/components/medications/sections/inventory-section.tsx",
|
||||
);
|
||||
expect(src).toContain('from "@/lib/medications/inventory/summary"');
|
||||
expect(src).toMatch(/summariseSupply\(items, perDose\)/);
|
||||
expect(src).not.toContain('state !== "USED_UP"');
|
||||
expect(src).toContain("medications.detail.bestand.expiredSuffix");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* v1.16.10 — shared supply-summary math for every stock readout.
|
||||
*
|
||||
* One predicate decides what counts as AVAILABLE supply: ACTIVE or
|
||||
* IN_USE containers with units left — the same filter the medications
|
||||
* list route (`stockUnitsRemaining`), the GLP-1 detail endpoint and the
|
||||
* Coach snapshot run. EXPIRED stock is visible but never available:
|
||||
* it surfaces separately (`expiredUnits`) so the UI can show a muted
|
||||
* "expired" suffix without folding it into the headline or the runway
|
||||
* estimate. USED_UP containers count nowhere.
|
||||
*
|
||||
* The Übersicht supply row and the Bestand summary both render from
|
||||
* this helper, so the surfaces cannot disagree about what "remaining"
|
||||
* means.
|
||||
*/
|
||||
|
||||
export type SupplyItemState = "ACTIVE" | "IN_USE" | "EXPIRED" | "USED_UP";
|
||||
|
||||
export interface SupplyItem {
|
||||
state: SupplyItemState;
|
||||
unitsTotal: number;
|
||||
unitsRemaining: number;
|
||||
}
|
||||
|
||||
export interface SupplySummary {
|
||||
/** Pooled units across available (ACTIVE / IN_USE, units left) containers. */
|
||||
unitsRemaining: number;
|
||||
/** Pooled capacity across the same available containers. */
|
||||
unitsTotal: number;
|
||||
/** Dose-derived headline: floor(unitsRemaining / unitsPerDose). */
|
||||
dosesRemaining: number;
|
||||
/** Dose-derived capacity: floor(unitsTotal / unitsPerDose). */
|
||||
dosesTotal: number;
|
||||
/** Units still sitting in EXPIRED containers — visible, never available. */
|
||||
expiredUnits: number;
|
||||
}
|
||||
|
||||
/** The list-route / GLP-1 availability predicate, verbatim. */
|
||||
export function isAvailableSupply(item: SupplyItem): boolean {
|
||||
return (
|
||||
(item.state === "ACTIVE" || item.state === "IN_USE") &&
|
||||
item.unitsRemaining > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function summariseSupply(
|
||||
items: readonly SupplyItem[],
|
||||
unitsPerDose: number,
|
||||
): SupplySummary {
|
||||
const perDose = Math.max(1, unitsPerDose);
|
||||
const available = items.filter(isAvailableSupply);
|
||||
const unitsRemaining = available.reduce(
|
||||
(sum, item) => sum + item.unitsRemaining,
|
||||
0,
|
||||
);
|
||||
const unitsTotal = available.reduce((sum, item) => sum + item.unitsTotal, 0);
|
||||
const expiredUnits = items
|
||||
.filter((item) => item.state === "EXPIRED")
|
||||
.reduce((sum, item) => sum + item.unitsRemaining, 0);
|
||||
return {
|
||||
unitsRemaining,
|
||||
unitsTotal,
|
||||
dosesRemaining: Math.floor(unitsRemaining / perDose),
|
||||
dosesTotal: Math.floor(unitsTotal / perDose),
|
||||
expiredUnits,
|
||||
};
|
||||
}
|
||||
@@ -1005,7 +1005,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Medications"],
|
||||
summary: "Register a new supply container",
|
||||
description:
|
||||
"Creates an ACTIVE inventory item with `unitsRemaining = unitsTotal`. The request's `dosesTotal` field carries UNITS (1–1000). Rate-limited 30/min/user. Audits as `medication.inventory.create`.",
|
||||
"Creates an ACTIVE inventory item with `unitsRemaining = unitsTotal`. The request's `unitsTotal` field carries UNITS (1–1000). Rate-limited 30/min/user. Audits as `medication.inventory.create`.",
|
||||
requestParams: {
|
||||
path: z.object({ id: z.string() }),
|
||||
},
|
||||
@@ -1040,7 +1040,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Medications"],
|
||||
summary: "Mutate a supply container",
|
||||
description:
|
||||
"Per-item operations: manual first-use (`markAsFirstUseAt`), used-up override (`markAsUsedUp`), printed-expiry correction, absolute remaining-unit correction (`dosesRemaining`, clamped to the item's capacity), notes. The canonical state machine re-derives the state after every mutation. Audits as `medication.inventory.update`.",
|
||||
"Per-item operations: manual first-use (`markAsFirstUseAt`), used-up override (`markAsUsedUp`), printed-expiry correction, absolute remaining-unit correction (`unitsRemaining`, clamped to the item's capacity), notes. The canonical state machine re-derives the state after every mutation. Audits as `medication.inventory.update`.",
|
||||
requestParams: {
|
||||
path: z.object({ id: z.string(), itemId: z.string() }),
|
||||
},
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
* v1.16.10 — inventory + units-per-dose validation bounds.
|
||||
*
|
||||
* Pins the raised unit caps (100 → 1000 on container capacity and the
|
||||
* stock correction; `dosesPerUnit` rides along) and the new
|
||||
* `unitsPerDose` (1–100) and `containerType` fields. The GLP-1 legacy
|
||||
* ledger delta deliberately KEEPS its ±100 bound — it counts pens, not
|
||||
* units.
|
||||
* stock correction; `dosesPerUnit` rides along), the new
|
||||
* `unitsPerDose` (1–100) and `containerType` fields, and the symmetric
|
||||
* request field names (`unitsTotal` / `unitsRemaining` — the response
|
||||
* always carried them). The GLP-1 legacy ledger delta deliberately
|
||||
* KEEPS its ±100 bound — it counts pens, not units.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -30,17 +31,17 @@ const MINIMAL_MEDICATION = {
|
||||
|
||||
describe("createInventoryItemSchema — unit cap 1000", () => {
|
||||
it("accepts 1000 units", () => {
|
||||
const r = createInventoryItemSchema.safeParse({ dosesTotal: 1000 });
|
||||
const r = createInventoryItemSchema.safeParse({ unitsTotal: 1000 });
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects 1001 units", () => {
|
||||
const r = createInventoryItemSchema.safeParse({ dosesTotal: 1001 });
|
||||
const r = createInventoryItemSchema.safeParse({ unitsTotal: 1001 });
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects zero", () => {
|
||||
const r = createInventoryItemSchema.safeParse({ dosesTotal: 0 });
|
||||
const r = createInventoryItemSchema.safeParse({ unitsTotal: 0 });
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
@@ -54,19 +55,19 @@ describe("createInventoryItemSchema — unit cap 1000", () => {
|
||||
"OTHER",
|
||||
]) {
|
||||
const r = createInventoryItemSchema.safeParse({
|
||||
dosesTotal: 4,
|
||||
unitsTotal: 4,
|
||||
containerType: ct,
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
}
|
||||
const r = createInventoryItemSchema.safeParse({ dosesTotal: 4 });
|
||||
const r = createInventoryItemSchema.safeParse({ unitsTotal: 4 });
|
||||
expect(r.success).toBe(true);
|
||||
expect(r.success && r.data.containerType).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects an unknown container type", () => {
|
||||
const r = createInventoryItemSchema.safeParse({
|
||||
dosesTotal: 4,
|
||||
unitsTotal: 4,
|
||||
containerType: "BAG",
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
@@ -75,12 +76,12 @@ describe("createInventoryItemSchema — unit cap 1000", () => {
|
||||
|
||||
describe("updateInventoryItemSchema — stock correction cap 1000", () => {
|
||||
it("accepts 1000", () => {
|
||||
const r = updateInventoryItemSchema.safeParse({ dosesRemaining: 1000 });
|
||||
const r = updateInventoryItemSchema.safeParse({ unitsRemaining: 1000 });
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects 1001", () => {
|
||||
const r = updateInventoryItemSchema.safeParse({ dosesRemaining: 1001 });
|
||||
const r = updateInventoryItemSchema.safeParse({ unitsRemaining: 1001 });
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -842,16 +842,16 @@ export type MedicationContainerTypeValue =
|
||||
export const createInventoryItemSchema = z
|
||||
.object({
|
||||
/** Units the container ships with. v1.16.10 raises the cap from
|
||||
* 100 to 1000 (large tablet packs); the wire field name stays
|
||||
* `dosesTotal` — it counts units, mapped to doses via
|
||||
* 100 to 1000 (large tablet packs) and renames the wire field to
|
||||
* `unitsTotal` — it counts units, mapped to doses via
|
||||
* `Medication.unitsPerDose`. */
|
||||
dosesTotal: z
|
||||
unitsTotal: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(1000)
|
||||
.describe(
|
||||
"Units the container ships with (tablets / ampoules / puffs; 1–1000). Stored as `unitsTotal`; dose-derived readouts divide by the medication's `unitsPerDose`.",
|
||||
"Units the container ships with (tablets / ampoules / puffs; 1–1000). Dose-derived readouts divide by the medication's `unitsPerDose`.",
|
||||
),
|
||||
/** v1.16.10 — container kind. Defaults to OTHER when absent. */
|
||||
containerType: z
|
||||
@@ -875,7 +875,7 @@ export const createInventoryItemSchema = z
|
||||
.meta({
|
||||
id: "CreateMedicationInventoryItemRequest",
|
||||
description:
|
||||
"Register a new supply container (pen / blister pack / bottle). `dosesTotal` counts UNITS (1–1000); the item starts ACTIVE with `unitsRemaining = unitsTotal` and the intake consumption hook decrements it per taken dose.",
|
||||
"Register a new supply container (pen / blister pack / bottle). `unitsTotal` counts UNITS (1–1000); the item starts ACTIVE with `unitsRemaining = unitsTotal` and the intake consumption hook decrements it per taken dose.",
|
||||
});
|
||||
|
||||
export const updateInventoryItemSchema = z
|
||||
@@ -903,9 +903,11 @@ export const updateInventoryItemSchema = z
|
||||
* flow). Sets the remaining-unit count directly; the route clamps to
|
||||
* `unitsTotal` and re-runs the canonical state machine (0 ⇒ USED_UP,
|
||||
* a raise out of 0 re-evaluates against the expiry clocks).
|
||||
* v1.16.10 raises the cap to 1000 alongside the capacity cap.
|
||||
* v1.16.10 raises the cap to 1000 alongside the capacity cap and
|
||||
* renames the wire field to `unitsRemaining` (it always counted
|
||||
* units), matching the response side.
|
||||
*/
|
||||
dosesRemaining: z
|
||||
unitsRemaining: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
@@ -972,6 +974,14 @@ export const glp1DoseChangePostSchema = z.object({
|
||||
note: z.string().max(MAX_NOTE_CHARS).nullable().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* DEPRECATED write path (v1.16.10) — the `inventory.delta` branch feeds
|
||||
* the legacy `MedicationInventoryEvent` running-sum ledger. The per-item
|
||||
* endpoints (`POST /api/medications/[id]/inventory`,
|
||||
* `PATCH /api/medications/[id]/inventory/[itemId]`) replaced it; reads
|
||||
* fall back to the ledger only while a medication has zero inventory
|
||||
* items. New callers must register containers instead of posting deltas.
|
||||
*/
|
||||
export const glp1InventoryPostSchema = z.object({
|
||||
delta: z
|
||||
.number()
|
||||
@@ -979,7 +989,10 @@ export const glp1InventoryPostSchema = z.object({
|
||||
.finite()
|
||||
.min(-100)
|
||||
.max(100)
|
||||
.refine((n) => n !== 0, { message: "delta must be non-zero" }),
|
||||
.refine((n) => n !== 0, { message: "delta must be non-zero" })
|
||||
.describe(
|
||||
"Deprecated since v1.16.10: pen-count delta on the legacy running-sum ledger. Register containers via the inventory endpoints instead; reads use the ledger only while the medication has no inventory items.",
|
||||
),
|
||||
reason: z.string().min(1).max(MAX_REASON_CHARS),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user