Publish the resolved per-slot dose units and a slot-aware runway on the medication wire

A medication whose slots carry different doses (a whole tablet in the
morning, a half at noon — #219) was consumed correctly but reported
wrongly: the wire shipped only the raw nullable per-slot column (clients
had to derive the inheritance), stockDosesRemaining divided by the
medication-level units alone, and no runway figure existed outside the
low-stock push.

- Every schedule now carries resolvedUnitsPerDose, resolved server-side
  exactly like the intake consumption resolver.
- stockDosesRemaining divides the units pool by the schedule-weighted
  average units per dose.
- runwayDays (whole days of supply under the slot-aware burn rate) rides
  the list AND detail payloads; the detail GET also gains the stock
  aggregates the list already had.
- The low-stock engine runs the same slot-aware math, so the push and
  the wire cannot disagree; the web cards, table, and detail Übersicht
  prefer the server figure over their client estimate.
- OpenAPI regenerated with the new fields.
This commit is contained in:
Marc-André Bombeck committed 2026-08-14 05:41:15 +02:00
1 parent 542ca91be5
commit 4b009c3a45
15 files changed
+510 -85

No files matched your search

+39 -1
View File
@@ -68,6 +68,17 @@ export const medicationScheduleResource = z
.describe(
"Per-schedule dose override. NULL means the schedule inherits `Medication.dose`.",
),
unitsPerDose: z
.number()
.nullable()
.describe(
"v1.37.10 (#219) — per-slot inventory-units override (may be a split-pill fraction). NULL means the schedule inherits the medication-level `unitsPerDose`. Kept raw so an edit surface can distinguish an explicit value from inheritance; consumers wanting the effective figure read `resolvedUnitsPerDose`.",
),
resolvedUnitsPerDose: z
.number()
.describe(
"v1.37.19 — the EFFECTIVE units one dose of this slot consumes, resolved server-side (`schedule.unitsPerDose ?? medication.unitsPerDose`, matching the intake consumption resolver). Always present; clients never re-derive the inheritance rule.",
),
daysOfWeek: z
.string()
.nullable()
@@ -260,7 +271,14 @@ export const medicationListEntry = medicationResource
.int()
.nullable()
.describe(
"v1.16.10 — dose-derived stock: `floor(stockUnitsRemaining / unitsPerDose)`, where `unitsPerDose` may be a fraction (½ tablet ⇒ twice the doses). Stays a whole-dose count. NULL when inventory tracking is off. Drives the table view's Bestand column. Read-only — aggregated, not stored.",
"v1.16.10 — dose-derived stock as a whole-dose count. v1.37.19 — slot-aware: the divisor is the schedule-weighted average units per dose (each slot's `resolvedUnitsPerDose` weighted by its cadence share), falling back to the medication-level `unitsPerDose` when no schedule derives a consumption rate. `unitsPerDose` may be a fraction (½ tablet ⇒ twice the doses). NULL when inventory tracking is off. Drives the table view's Bestand column. Read-only — aggregated, not stored.",
),
runwayDays: z
.number()
.int()
.nullable()
.describe(
"v1.37.19 — projected whole days the usable stock covers under the slot-aware burn rate (the same math the low-stock notification engine runs, so the wire and the push can never disagree). NULL = inventory tracking off or no consuming cadence derivable; 0 = tracking on, supply ran out. Read-only — computed, not stored.",
),
})
.meta({
@@ -272,6 +290,26 @@ export const medicationListEntry = medicationResource
export const medicationDetailEntry = medicationResource
.extend({
category: medicationCategoryEnum,
stockUnitsRemaining: z
.number()
.nullable()
.describe(
"v1.37.19 — usable inventory units left (same semantics as the list entry's field): NULL = inventory tracking off; 0 = tracking on, supply ran out.",
),
stockDosesRemaining: z
.number()
.int()
.nullable()
.describe(
"v1.37.19 — slot-aware dose-derived stock, mirroring the list entry's field.",
),
runwayDays: z
.number()
.int()
.nullable()
.describe(
"v1.37.19 — projected whole days the usable stock covers under the slot-aware burn rate; NULL = tracking off or no consuming cadence.",
),
})
.meta({
id: "MedicationDetail",