mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(medications): every taken dose moves the stock, in units that match the product
Stock only decremented on the web card's take button — the status toggle, the phone sync, the Telegram buttons and the token API all recorded the dose and left the inventory untouched, which is why a fresh pack kept reading full. Consumption now stamps the intake row itself: every path that turns a row taken consumes through one module — open container first, then earliest expiry with auto-open, spilling into the next pack and flooring at zero — and every path that takes it back (undo, delete, a take turned skip, a converged duplicate) refunds exactly what the stamp recorded. Replays and pending sync echoes can never double-charge, and the nightly convergence pass refunds the rows it tombstones. A medication can declare units per dose, so two 2 mg tablets book as one 4 mg dose and stock reads in doses with the unit count alongside; containers carry a named type including tablet packs, hold up to a thousand units for inhalers, and the GLP-1 card reads the same stock as the inventory tab instead of a separate ledger. The inventory endpoints join the documented API.
This commit is contained in:
1 parent
dacdcccfd2
commit
7d3557a3fd
53 files changed
+4118
-641
No files matched your search
@@ -0,0 +1,36 @@
|
||||
-- v1.16.10 — multi-unit inventory consumption.
|
||||
--
|
||||
-- `units_per_dose` (medications): how many inventory UNITS one dose
|
||||
-- consumes (2 × 2 mg tablets for a 4 mg dose). Default 1 keeps every
|
||||
-- existing medication on the one-unit-per-dose behaviour.
|
||||
--
|
||||
-- `container_type` (medication_inventory_items): what kind of physical
|
||||
-- container the item is (pen / ampoule / blister pack / inhaler /
|
||||
-- bottle). Display-level only. Backfilled PEN for items whose
|
||||
-- medication is an INJECTION (the pre-v1.16.10 surface was pen-only),
|
||||
-- OTHER for everything else.
|
||||
--
|
||||
-- `inventory_consumption` (medication_intake_events): the per-event
|
||||
-- consumption stamp `[{itemId, units}]` — the exactly-once gate for
|
||||
-- the consume hook and the refund ledger for the restore hook. NULL =
|
||||
-- never consumed (pending / skipped / pre-v1.16.10 rows).
|
||||
--
|
||||
-- The Prisma fields `dosesTotal` / `dosesRemaining` are renamed to
|
||||
-- `unitsTotal` / `unitsRemaining` at the client level only; the DB
|
||||
-- columns keep their names via @map — zero data migration.
|
||||
|
||||
CREATE TYPE "medication_container_type" AS ENUM ('PEN', 'AMPOULE', 'BLISTER', 'INHALER', 'BOTTLE', 'OTHER');
|
||||
|
||||
ALTER TABLE "medications" ADD COLUMN "units_per_dose" INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
ALTER TABLE "medication_inventory_items" ADD COLUMN "container_type" "medication_container_type" NOT NULL DEFAULT 'OTHER';
|
||||
|
||||
ALTER TABLE "medication_intake_events" ADD COLUMN "inventory_consumption" JSONB;
|
||||
|
||||
-- Backfill: every existing item was registered through the pen-framed
|
||||
-- surface; items on injection medications are pens.
|
||||
UPDATE "medication_inventory_items" i
|
||||
SET "container_type" = 'PEN'
|
||||
FROM "medications" m
|
||||
WHERE i."medication_id" = m."id"
|
||||
AND m."delivery_form" = 'INJECTION';
|
||||
+42
-5
@@ -1261,6 +1261,12 @@ model Medication {
|
||||
/// (Mounjaro KwikPen = 4, Ozempic 0.25/0.5 mg = 4, Wegovy single-dose
|
||||
/// pen = 1). NULL = inventory tracking off.
|
||||
dosesPerUnit Int? @map("doses_per_unit")
|
||||
/// v1.16.10 — units consumed per dose. The inventory items count UNITS
|
||||
/// (tablets, ampoules, puffs); a single dose may consume several of
|
||||
/// them (2 × 2 mg tablets for a 4 mg dose). The intake consumption
|
||||
/// hook multiplies by this factor; dose-level readouts divide by it.
|
||||
/// Default 1 = one unit per dose (the overwhelmingly common case).
|
||||
unitsPerDose Int @default(1) @map("units_per_dose")
|
||||
active Boolean @default(true)
|
||||
notificationsEnabled Boolean @default(true) @map("notifications_enabled")
|
||||
pausedAt DateTime? @map("paused_at")
|
||||
@@ -1599,6 +1605,15 @@ model MedicationIntakeEvent {
|
||||
/// log time (half tablet, titration day). NULL = the configured
|
||||
/// medication / schedule dose applies; read paths fall back to it.
|
||||
doseTaken String? @map("dose_taken")
|
||||
/// v1.16.10 — inventory consumption stamp: `[{itemId, units}]` for the
|
||||
/// containers this taken event decremented. NULL = never consumed
|
||||
/// (pending / skipped / pre-v1.16.10 rows). The stamp is the exactly-
|
||||
/// once gate for the consume hook (a stamped row never decrements
|
||||
/// again, however often a sync replays it) and the refund ledger for
|
||||
/// the restore hook (taken → skipped / deleted refunds per item). The
|
||||
/// stamp freezes history: editing `Medication.unitsPerDose` later
|
||||
/// never changes what an already-taken dose consumed.
|
||||
inventoryConsumption Json? @map("inventory_consumption")
|
||||
// v1.7.0 sync — server-set mutation timestamp. The model had no
|
||||
// `updatedAt`; the offline-sync keyset feed orders on it, so it is
|
||||
// added here. `@updatedAt` makes Prisma maintain it on every write.
|
||||
@@ -1706,16 +1721,38 @@ enum MedicationInventoryState {
|
||||
@@map("medication_inventory_state")
|
||||
}
|
||||
|
||||
/// v1.16.10 — what kind of physical container an inventory item is.
|
||||
/// Display-level only (no state-machine behaviour hangs off it); the
|
||||
/// register dialog defaults it from the medication's delivery form
|
||||
/// (INJECTION → PEN, ORAL → BLISTER, else OTHER).
|
||||
enum MedicationContainerType {
|
||||
PEN
|
||||
AMPOULE
|
||||
BLISTER
|
||||
INHALER
|
||||
BOTTLE
|
||||
OTHER
|
||||
|
||||
@@map("medication_container_type")
|
||||
}
|
||||
|
||||
model MedicationInventoryItem {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
medicationId String @map("medication_id")
|
||||
state MedicationInventoryState @default(ACTIVE)
|
||||
/// Doses the pen / vial shipped with. Mounjaro KwikPen = 4,
|
||||
/// Trulicity single-dose = 1, Byetta = 60.
|
||||
dosesTotal Int @map("doses_total")
|
||||
/// Remaining doses; decremented by the intake hook.
|
||||
dosesRemaining Int @map("doses_remaining")
|
||||
/// v1.16.10 — container kind (pen / ampoule / blister pack / …).
|
||||
/// Backfilled PEN for items on INJECTION medications, OTHER otherwise.
|
||||
containerType MedicationContainerType @default(OTHER) @map("container_type")
|
||||
/// Units the container shipped with (v1.16.10 — renamed from
|
||||
/// `dosesTotal`; the DB column stays `doses_total`). A unit is one
|
||||
/// tablet / ampoule / puff; `Medication.unitsPerDose` maps units to
|
||||
/// doses. Mounjaro KwikPen = 4, Trulicity single-dose = 1.
|
||||
unitsTotal Int @map("doses_total")
|
||||
/// Remaining units; decremented by the intake consumption hook
|
||||
/// (v1.16.10 — renamed from `dosesRemaining`; column stays
|
||||
/// `doses_remaining`).
|
||||
unitsRemaining Int @map("doses_remaining")
|
||||
/// NULL until the pen is first opened. Sets the 30-day in-use
|
||||
/// clock per EMA EPAR §6.3.
|
||||
firstUseAt DateTime? @map("first_use_at")
|
||||
|
||||
Reference in new issue
Block a user