diff --git a/src/lib/medications/scheduling/recurrence.ts b/src/lib/medications/scheduling/recurrence.ts index b83bf0b63..71d2717b3 100644 --- a/src/lib/medications/scheduling/recurrence.ts +++ b/src/lib/medications/scheduling/recurrence.ts @@ -317,21 +317,27 @@ export function nextOccurrenceAfter( Math.min(dayFloor.getTime(), rollingDoseDayFloor(schedule, ctx)), ); // The rolling slot is a single deterministic instant — `lastIntakeAt + N` - // (or the anchor) — not an open-ended walk, so the 10-year anti-spin - // `hardCap` must never gate it. It collides at the fencepost: a reminder at - // the maximum ten-year rolling interval, satisfied so that `after === - // lastIntakeAt`, produces a next slot exactly `after + 3650 days` — equal - // to `hardCap` to the millisecond, so the notify-hour slot spills a few - // hours past the raw boundary and `expandRolling` clips it to nothing + // (or the anchor) at the notify-hour — not an open-ended walk, so the + // 10-year anti-spin `hardCap` must never gate it. It collides at the + // fencepost: a reminder at the maximum ten-year rolling interval, satisfied + // so that `after === lastIntakeAt`, produces a next slot exactly `after + + // 3650 days` — equal to `hardCap` to the millisecond, so the notify-hour + // slot spills past the raw boundary and `expandRolling` clips it to nothing // (a ten-year booster silently lost its next due on the very dose that - // should re-anchor it). Bound the search by the deterministic slot's own - // day instead; `endsOn` stays enforced inside `expandRolling`, so a - // course-window reminder still self-expires. + // should re-anchor it). Bound the search by the deterministic slot's OWN + // end-of-day. The bound must key off the applied notify-hour instant, not + // the raw anchor: when the anchor sits in the late-UTC evening its local + // notify-hour slot falls on the NEXT UTC day, so an anchor-day ceiling is a + // day short and re-clips the very slot it meant to admit. `endsOn` stays + // enforced inside `expandRolling`, so a course-window reminder still + // self-expires. + const rollingSlotInstant = applyTimeOfDayToDate( + rollingDoseAnchor(schedule, ctx), + schedule.timesOfDay[0] ?? schedule.windowStart, + ctx.timeZone, + ); const rollingCeil = new Date( - Math.max( - limit.getTime(), - endOfUtcDay(rollingDoseAnchor(schedule, ctx)).getTime(), - ), + Math.max(limit.getTime(), endOfUtcDay(rollingSlotInstant).getTime()), ); const slots = expandRolling(schedule, ctx, floor, rollingCeil); for (const s of slots) {