mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(medications): clamp compliance expected slots to creation date
`expectedSlotsBetween` walked the full requested window even for a medication younger than that window. The legacy weekday expander floors on `startsOn` but not on `createdAt`, so a two-day-old daily med reported seven expected doses over a seven-day window and thirty over a thirty-day window — counting days before it existed. Clamp the window lower bound to `createdAt` so the expected-dose counts, and the window selection that reads them, reflect the medication's real age. The displayed rates already clamp independently via `calculateCompliance`, so only the slot counts change.
This commit is contained in:
1 parent
38867439f3
commit
068cb168d9
2 files changed
+92
-1
No files matched your search
@@ -1221,6 +1221,33 @@ describe("buildComplianceDisplay — two rows, cadence-scaled windows", () => {
|
||||
expect(display.short).toBeDefined();
|
||||
expect(display.long).toBeDefined();
|
||||
});
|
||||
|
||||
it("a 2-day-old daily med's expected counts reflect its real age", () => {
|
||||
// Created two days ago: the expected-dose counts must reflect ~2 days
|
||||
// of daily doses, not the 7 / 30 a full-window walk would report. No
|
||||
// rung clears the floor, so the selection falls back to the widest
|
||||
// rung while keeping the counts age-accurate.
|
||||
const schedules: ComplianceSchedule[] = [
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: null,
|
||||
rrule: "FREQ=DAILY",
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
];
|
||||
const createdAt = new Date(NOW.getTime() - 2 * DAY_MS);
|
||||
const display = buildComplianceDisplay([], schedules, ctx({ createdAt }), {
|
||||
now: NOW,
|
||||
});
|
||||
// ~2 days of daily doses — nowhere near the 7 a 7-day window or the 30
|
||||
// a 30-day window would report before the createdAt clamp.
|
||||
expect(display.expectedShort).toBeLessThanOrEqual(3);
|
||||
expect(display.expectedLong).toBeLessThanOrEqual(3);
|
||||
// No rung clears the floor → widest windows still render two rows.
|
||||
expect(display.shortDays).toBe(90);
|
||||
expect(display.longDays).toBe(365);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expectedSlotsBetween", () => {
|
||||
@@ -1283,4 +1310,57 @@ describe("expectedSlotsBetween", () => {
|
||||
);
|
||||
expect(slots.length).toBe(0);
|
||||
});
|
||||
|
||||
it("clamps the window lower bound to createdAt for a young med", () => {
|
||||
// A daily med created two days before the window's upper bound: the
|
||||
// legacy weekday walker floors on `startsOn` (none here) but not on
|
||||
// `createdAt`, so without the clamp this 30-day window would count
|
||||
// ~30 days of pre-creation slots. With the clamp only the ~2 days the
|
||||
// med actually existed emit a slot.
|
||||
const to = new Date("2025-06-15T00:00:00Z");
|
||||
const from = new Date(to.getTime() - 30 * DAY_MS);
|
||||
const createdAt = new Date(to.getTime() - 2 * DAY_MS);
|
||||
const schedules: ComplianceSchedule[] = [
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: null,
|
||||
rrule: "FREQ=DAILY",
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
];
|
||||
const slots = expectedSlotsBetween(schedules, from, to, ctx({ createdAt }));
|
||||
// Daily cadence over ~2 days → at most 3 slots, never the ~30 a full
|
||||
// 30-day window would otherwise emit.
|
||||
expect(slots.length).toBeLessThanOrEqual(3);
|
||||
expect(slots.length).toBeGreaterThanOrEqual(1);
|
||||
// Every emitted slot lands at or after the creation instant.
|
||||
for (const s of slots) {
|
||||
expect(s.at.getTime()).toBeGreaterThanOrEqual(createdAt.getTime());
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves the window untouched when createdAt predates it", () => {
|
||||
// createdAt well before `from` → the clamp is a no-op and the full
|
||||
// window's slots survive.
|
||||
const to = new Date("2025-06-15T00:00:00Z");
|
||||
const from = new Date(to.getTime() - 7 * DAY_MS);
|
||||
const schedules: ComplianceSchedule[] = [
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: null,
|
||||
rrule: "FREQ=DAILY",
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
];
|
||||
const slots = expectedSlotsBetween(
|
||||
schedules,
|
||||
from,
|
||||
to,
|
||||
ctx({ createdAt: new Date("2025-01-01T00:00:00Z") }),
|
||||
);
|
||||
// A full week of daily doses → ~7 slots.
|
||||
expect(slots.length).toBeGreaterThanOrEqual(7);
|
||||
});
|
||||
});
|
||||
@@ -340,6 +340,15 @@ export function expectedSlotCountForDay(
|
||||
* `occurrencesBetween` is inclusive of both ends; the caller passes a
|
||||
* `[from, to]` window and we sort the union of every schedule's slots so
|
||||
* a multi-schedule medication interleaves its slots in time order.
|
||||
*
|
||||
* v1.8.6 QA — the window lower bound is clamped to `ctx.createdAt`. The
|
||||
* legacy weekday walker floors on `startsOn` but not on `createdAt`, so a
|
||||
* brand-new daily med queried over a 30-day window would otherwise emit
|
||||
* slots for every day before it existed (7/30 expected on a 2-day-old med).
|
||||
* Clamping here keeps the expected-dose denominator — and the window
|
||||
* selection that reads it — honest about the medication's real age. The
|
||||
* displayed rates clamp independently via `calculateCompliance`'s
|
||||
* `medicationCreatedAt` argument, so this only fixes the slot counts.
|
||||
*/
|
||||
export function expectedSlotsBetween(
|
||||
schedules: ComplianceSchedule[],
|
||||
@@ -348,12 +357,14 @@ export function expectedSlotsBetween(
|
||||
ctx: ComplianceMedicationContext,
|
||||
): Occurrence[] {
|
||||
const recurrenceCtx = toRecurrenceCtx(ctx, "compliance-slots");
|
||||
const effectiveFrom =
|
||||
ctx.createdAt.getTime() > from.getTime() ? ctx.createdAt : from;
|
||||
const all: Occurrence[] = [];
|
||||
for (let i = 0; i < schedules.length; i++) {
|
||||
all.push(
|
||||
...occurrencesBetween(
|
||||
toCanonicalSchedule(schedules[i], `compliance-slots-${i}`),
|
||||
from,
|
||||
effectiveFrom,
|
||||
to,
|
||||
recurrenceCtx,
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user