fix(medications): keep degenerate ≤1-day rolling on the minute scale

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.
This commit is contained in:
Marc-André Bombeck committed 2026-06-08 17:30:31 +02:00
1 parent 75bf9829a8
commit 56bc46417e
2 files changed
+39 -3

No files matched your search

@@ -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)
// ────────────────────────────────────────────────────────────────────
@@ -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