fix(medications): count the whole cadence in the supply runway

The runway estimate read FREQ=MONTHLY as one dose every 30 days whatever
its INTERVAL, so a quarterly injection with two doses left showed about 60
days of supply instead of 180. FREQ=DAILY;INTERVAL=2 counted as daily, a
yearly rule ignored INTERVAL and BYMONTH, and a monthly rule ignored extra
BYMONTHDAY values. A CYCLIC schedule was counted as if it never paused,
and a PRN schedule as if it were due every day. The rate now honours
INTERVAL for every frequency, counts the listed days and months, scales a
cyclic schedule by on / (on + off) and leaves PRN out. The low-stock job
selects the schedule type and cycle lengths so it reads the same rate.

Whole-dose counts no longer lose one to binary floating point: 2.4 mL at
0.8 mL per dose is 2.9999999999999996 in IEEE 754 and used to read as two
doses. The stock summary, both runway estimators, the list and detail
routes, the GLP-1 route, the Coach snapshot and the inventory readout now
floor through one helper that treats anything within 1e-9 of the next whole
number as that number.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 15:32:29 +02:00
1 parent 888b738546
commit 91571d4674
12 files changed
+180 -25

No files matched your search

+2 -1
View File
@@ -47,6 +47,7 @@ import {
} from "@/lib/crypto/note-cipher";
import { NextRequest } from "next/server";
import { dueSchedules } from "@/lib/medications/intake-tracking";
import { floorWhole } from "@/lib/medications/units-per-dose";
type RouteParams = { params: Promise<{ id: string }> };
@@ -175,7 +176,7 @@ export const GET = apiHandler(
0,
);
const pensRemaining = usable.length;
const dosesRemaining = Math.floor(
const dosesRemaining = floorWhole(
unitsRemaining / (Number(medication.unitsPerDose) || 1),
);
const weeksOfSupply = dosesRemaining;
+2 -1
View File
@@ -46,6 +46,7 @@ import {
isRecordOnly,
scheduleWireFields,
} from "@/lib/medications/intake-tracking";
import { floorWhole } from "@/lib/medications/units-per-dose";
type RouteParams = { params: Promise<{ id: string }> };
@@ -216,7 +217,7 @@ export const GET = apiHandler(
const stockDosesRemaining =
stockUnitsRemaining === null
? null
: Math.floor(
: floorWhole(
stockUnitsRemaining /
effectiveUnitsPerDose(
schedulesDto,