diff --git a/document/06-traps.md b/document/06-traps.md index 81081a1..344b826 100755 --- a/document/06-traps.md +++ b/document/06-traps.md @@ -718,3 +718,28 @@ The general shape: **a rule with three call sites and no name has no place to add a case to.** When a third value joins a two-value decision, the first thing to look for is the other spellings of that decision. `grep` for the ternary, not for the function, because there is no function yet, which is the problem. + +## A parameter with a default that every caller leaves out + +Today read **Stock: 0 doses** for a compound with a full pack of sixty tablets +on the shelf. + +`stockFor` takes a container and defaults it to `"vial"`, which was right when +a vial was the only thing there was. Spray bottles added the parameter; tablets +added a third value for it. All three call sites went on omitting it, so every +figure on every screen counted vials and nothing else. Nothing failed, nothing +warned, and the zero looked like an empty shelf rather than like a question +that was never asked. + +Same shape as the container the log form picks a dose from, one file over. A +default is a decision made once and inherited silently by everybody who does +not know a decision is being made. + +The rule: **when a parameter gains a value, grep the call sites that omit it, +not the ones that pass it.** The callers that already pass something have been +thought about. The ones relying on the default have not, and they are invisible +in a search for the parameter's name. + +`needsReconstitution` was the same sentence one line further down: with the +container finally arriving, it had to stop being true for a pack, which has +nothing to make up. diff --git a/src/app/page.tsx b/src/app/page.tsx index db48817..41948dd 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -40,7 +40,7 @@ import { startOfLocalDay, unloggedDoseTimes, } from "@/lib/calc/schedule"; -import { daysOfSupplyForProtocol, vialConcentration } from "@/lib/calc/inventory"; +import { containerForDose, daysOfSupplyForProtocol, vialConcentration } from "@/lib/calc/inventory"; import { routeHasSite, siteChoices, suggestSite } from "@/lib/calc/sites"; import { currentStreak, @@ -236,7 +236,17 @@ export default function NowPage() { const targetMcg = scheduledDoseMcg(protocol, now); const due = dueStatus(protocol, now, { lastLoggedAt }); - const stock = stockFor(vials, protocol.peptideId, targetMcg, now); + /* + * From the container this compound is actually taken out of. Left to its + * default this counted vials only, so a protocol on tablets read + * "0 doses" with a full pack on the shelf. + */ + const stock = stockFor( + vials, + protocol.peptideId, + targetMcg, + now, + containerForDose(peptide?.preparation, protocol.route)); /** * What "100% of a single-dose peak" is measured against. @@ -957,7 +967,7 @@ export default function NowPage() { {track.lastLoggedAt ? relativeTime(track.lastLoggedAt, now) : t("now_never")} - {track.lastLog?.site && ( + {track.lastLog?.site && routeHasSite(track.lastLog.route) && ( {" ยท "} {siteLabel(track.lastLog.site)} diff --git a/src/app/stock/page.tsx b/src/app/stock/page.tsx index a2ad982..e9c8c75 100755 --- a/src/app/stock/page.tsx +++ b/src/app/stock/page.tsx @@ -25,6 +25,7 @@ import { useSyringeScale } from "@/components/DoseMarks"; import { diluentAfterTopUp, groupSealedVials, + containerForDose, marksFromVial, openPack, stockFor, @@ -170,14 +171,18 @@ export default function StockPage() { if (hit) return hit; const p = protocols.find((x) => x.active && x.peptideId === peptideId); + const container = containerForDose(findPeptide(custom, peptideId)?.preparation, p?.route ?? "subcutaneous"); const out: SupplyOutlook = p - ? supplyOutlook(stockFor(vials, peptideId, scheduledDoseMcg(p, now), now), p, now) + ? supplyOutlook( + stockFor(vials, peptideId, scheduledDoseMcg(p, now), now, container), + p, + now) : { kind: "unknown" }; cache.set(peptideId, out); return out; }; - }, [protocols, vials, now]); + }, [protocols, vials, custom, now]); if (!hydrated) { return
{t("loading")}
; diff --git a/src/components/LogDoseSheet.tsx b/src/components/LogDoseSheet.tsx index a1df027..a73fbbe 100755 --- a/src/components/LogDoseSheet.tsx +++ b/src/components/LogDoseSheet.tsx @@ -326,7 +326,7 @@ export function LogDoseSheet({ // What the stock looks like once this dose is taken. This is the number the // user actually wants: how many more of these are left. - const stock = stockFor(vials, peptideId, doseMcg, at); + const stock = stockFor(vials, peptideId, doseMcg, at, container); const willDeplete = !skipped && !!vialId && doseMcg > 0; const dosesAfter = Math.max(0, stock.dosesRemaining - (willDeplete ? 1 : 0)); const vialLeftAfter = vial diff --git a/src/lib/calc/inventory.test.ts b/src/lib/calc/inventory.test.ts index 2696ea4..2db9173 100755 --- a/src/lib/calc/inventory.test.ts +++ b/src/lib/calc/inventory.test.ts @@ -938,3 +938,35 @@ describe("opening a pack", () => { expect(pickVialForDose(vials, "klow", 10_000, NOW, "pack")?.id).toBe("started"); }); }); + +/* + * The container has to be asked for. It defaults to a vial, and a screen that + * leaves it out counts nothing for a compound sold as tablets: Today read + * "0 doses" with a full pack on the shelf. + */ +describe("stockFor and the container", () => { + const pack = (over: Partial = {}) => + vial({ id: "p", container: "pack", mgPerTablet: 10, strengthMg: 600, state: "sealed", ...over }); + + it("counts nothing for a pack when asked about vials", () => { + expect(stockFor([pack()], "klow", 10_000, NOW).dosesRemaining).toBe(0); + }); + + it("counts the pack when asked about packs", () => { + const s = stockFor([pack()], "klow", 10_000, NOW, "pack"); + expect(s.dosesRemaining).toBe(60); + expect(s.availableMcg).toBe(600_000); + }); + + /* Nothing to make up, so nobody is told to reach for the water. */ + it("never asks for a pack to be reconstituted", () => { + expect(stockFor([pack()], "klow", 10_000, NOW, "pack").needsReconstitution).toBe(false); + expect(stockFor([vial({ id: "v" })], "klow", 10_000, NOW).needsReconstitution).toBe(true); + }); + + it("leaves a vial and a pack of the same compound out of each other's count", () => { + const both = [pack(), vial({ id: "v", strengthMg: 10 })]; + expect(stockFor(both, "klow", 10_000, NOW, "pack").dosesRemaining).toBe(60); + expect(stockFor(both, "klow", 10_000, NOW).dosesRemaining).toBe(1); + }); +}); diff --git a/src/lib/calc/inventory.ts b/src/lib/calc/inventory.ts index cec4c3e..7bae828 100755 --- a/src/lib/calc/inventory.ts +++ b/src/lib/calc/inventory.ts @@ -405,7 +405,12 @@ export function stockFor( openCount, dosesRemaining: per(availableMcg), dosesInOpenVials: per(openMcg), - needsReconstitution: openCount === 0 && sealedCount > 0, + /* + * Only ever true of a vial. A pack and a spray bottle have nothing to make + * up, so telling their owner to reach for the water would be advice about + * a container they are not holding. + */ + needsReconstitution: container === "vial" && openCount === 0 && sealedCount > 0, dosesExpired: per(expiredMcg), }; } diff --git a/src/lib/store.ts b/src/lib/store.ts index 52460a7..affc20a 100755 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -41,6 +41,7 @@ import { reconcileVials, returnToVial, stockFor as computeStock, + type ContainerKind, vialConcentration, vialExpired, vialFractionRemaining, @@ -966,9 +967,20 @@ export function vialStatus(vial: Vial, nowMs = Date.now()): VialStatus { }; } -/** Doses of a peptide still available across every usable vial. */ -export function stockFor(vials: Vial[], peptideId: string, doseMcg: number, nowMs = Date.now()) { - return computeStock(vials, peptideId, doseMcg, nowMs); +/** + * Doses of a peptide still available across every usable container. + * + * The container has to be passed, not defaulted, wherever the answer is shown + * to somebody: the default is a vial, and a caller that leaves it out counts + * nothing for a compound that comes as tablets or lives in a spray bottle. + */ +export function stockFor( + vials: Vial[], + peptideId: string, + doseMcg: number, + nowMs = Date.now(), + container: ContainerKind = "vial") { + return computeStock(vials, peptideId, doseMcg, nowMs, container); } export { vialCapacityMcg, pickVialForDose } from "./calc/inventory";