From 56bc46417e278ae1f8101378c919f1af91d377b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 8 Jun 2026 17:30:31 +0200 Subject: [PATCH] =?UTF-8?q?fix(medications):=20keep=20degenerate=20?= =?UTF-8?q?=E2=89=A41-day=20rolling=20on=20the=20minute=20scale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review: a rolling interval of 1 day is effectively daily, so a ±1-day day-scale on-time window would over-widen and let one take claim adjacent slots. Classify rolling by its interval — ≥2 days is day-scale (the canonical weekly+ injection), ≤1 day stays minute-scale. Two tests pin the distinction. --- .../scheduling/__tests__/band-minter.test.ts | 30 +++++++++++++++++++ src/lib/medications/scheduling/band-minter.ts | 12 ++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/lib/medications/scheduling/__tests__/band-minter.test.ts b/src/lib/medications/scheduling/__tests__/band-minter.test.ts index af94cfdd2..e2975302f 100644 --- a/src/lib/medications/scheduling/__tests__/band-minter.test.ts +++ b/src/lib/medications/scheduling/__tests__/band-minter.test.ts @@ -224,6 +224,36 @@ describe("rolling GLP-1 (every 7 days) — retrospective bands", () => { }); }); +describe("rolling — a ≤1-day interval stays minute-scale (daily family)", () => { + // A degenerate every-1-day rolling interval is effectively daily; a ±1-day + // on-time window would over-widen, so it must classify daily (minute-scale). + const m = med({ startsOn: new Date("2026-06-01T00:00:00Z") }); + const sched = schedule({ rollingIntervalDays: 1, timesOfDay: ["08:00"] }); + const shot = new Date("2026-06-08T08:05:00Z"); + const out = buildBandsForMedication({ + medication: m, + schedule: sched, + ctx: ctxFor(m, shot), + userTz: TZ, + range: { + from: new Date("2026-06-07T00:00:00Z"), + to: new Date("2026-06-09T00:00:00Z"), + }, + now: new Date("2026-06-09T00:00:00Z"), + intakeInstants: [shot], + }); + + it("uses the daily family for a 1-day rolling cadence", () => { + expect(out.family).toBe("daily"); + }); + + it("the on-time band is minute-scale (±60min), not day-scale", () => { + const b = out.bands[0]; + const span = b.onTimeEnd.getTime() - b.onTimeStart.getTime(); + expect(span).toBe(2 * 60 * MIN); + }); +}); + // ──────────────────────────────────────────────────────────────────── // 4. Cyclic (on/off weeks) // ──────────────────────────────────────────────────────────────────── diff --git a/src/lib/medications/scheduling/band-minter.ts b/src/lib/medications/scheduling/band-minter.ts index d7e44f7ac..4d62ba424 100644 --- a/src/lib/medications/scheduling/band-minter.ts +++ b/src/lib/medications/scheduling/band-minter.ts @@ -165,9 +165,15 @@ export function buildBandsForMedication( input.intakeInstants ?? [], now, ); - // Rolling is inherently day-scale (every-N-days). Mint day-scale bands. - const bands = mintBands(occ, "weekly", input.windowConfig, userTz); - return { bands, hasExpectedSlots: true, family: "weekly" }; + // The canonical rolling case is a once-weekly+ injection (day-scale). A + // degenerate ≤1-day rolling interval is effectively daily, so a ±1-day + // on-time window would over-widen and let one take claim adjacent slots — + // keep it minute-scale. The realised every-N-days cadence is the field + // here (intakes drive the anchors), so the interval is the right signal. + const family: "daily" | "weekly" = + schedule.rollingIntervalDays >= 2 ? "weekly" : "daily"; + const bands = mintBands(occ, family, input.windowConfig, userTz); + return { bands, hasExpectedSlots: true, family }; } // Daily / weekly / cyclic / rrule — `occurrencesBetween` already filters