feat(medications): thread timeZone into cadence + compliance helpers

The W19e pure cadence + compliance helpers used `Date.setHours`,
`getDay`, and `setHours(0, 0, 0, 0)` for every local-day boundary,
window-time application, and streak day-key. Those methods read the
host process's system time — every medication detail page on the
cadence route returned values relative to whatever zone the server
ran in (Europe/Berlin in prod, often UTC in CI), not the user's. A
Tokyo user reading the same data saw their 08:00 window land at
16:00 wall-clock and the day-of-week filter ran off the Berlin
weekday.

Thread an optional IANA `timeZone` argument through
`expandScheduleSlots`, `pairDoses`, `buildCadenceTimeline`,
`computeNextDose`, `missedDoses`, and `complianceChips`. Every local
operation now resolves through `Intl.DateTimeFormat`. When the
argument is omitted the helpers fall back to system-local — every
existing unit test (system-local by construction) keeps its
assertions green. The cadence route resolves the user's zone via
`resolveUserTimezone(user.id)` and forwards.

Regression tests pin the 08:00 wall-clock contract across Berlin /
Tokyo / Los Angeles and check that a "Mondays only" schedule lands
on each zone's local Monday.

Fix-O / code-H2.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 22:42:37 +02:00
1 parent 701fd5f251
commit 2a65f2b179
5 files changed
+361 -50

No files matched your search

+12 -1
View File
@@ -26,6 +26,7 @@ import {
computeNextDose,
} from "@/lib/medications/scheduling/cadence";
import { complianceChips } from "@/lib/medications/scheduling/compliance";
import { resolveUserTimezone } from "@/lib/tz/resolver";
type RouteParams = { params: Promise<{ id: string }> };
@@ -76,12 +77,21 @@ export const GET = apiHandler(
// chart disagree on the bi-weekly grid.
const anchor = med.createdAt;
// v1.4.25 W21 Fix-O — resolve the user's IANA zone so the cadence
// helpers compute every local-day boundary, window-time application,
// and streak day-key in the user's clock instead of the host's
// system time. A Tokyo user reading the same medication gets the
// same chip values as a Berlin user — the host-relative day flip
// was a sneaky bug at the 08:00-Berlin / 16:00-Tokyo intersection.
const userTz = await resolveUserTimezone(user.id);
const timeline = buildCadenceTimeline(
med.schedules,
events,
asOf,
windowDays,
anchor,
userTz,
);
const chips = complianceChips(
med.schedules,
@@ -89,8 +99,9 @@ export const GET = apiHandler(
asOf,
windowDays,
anchor,
userTz,
);
const next = computeNextDose(med.schedules, asOf, 14, anchor);
const next = computeNextDose(med.schedules, asOf, 14, anchor, userTz);
annotate({
action: {