fix(dashboard): count the last hour of a 25-hour day in today's doses

buildMedsTodayBlock ended the day at local midnight plus 24 hours. On the
day the clocks go back that stops an hour early, so a dose between 23:00
and midnight was neither projected nor counted in the doses tally. The
exclusive end is now the next local day's start, as the digest loader
already computes it.
This commit is contained in:
Marc-André Bombeck committed 2026-10-05 07:48:58 +02:00
1 parent 169c7bdfe6
commit d0b680c277
2 files changed
+33 -4

No files matched your search

@@ -181,6 +181,34 @@ describe("buildMedsTodayBlock — projection contract", () => {
});
});
describe("buildMedsTodayBlock — the 25-hour day", () => {
it("covers the last local hour on the day the clocks go back", async () => {
// 2026-10-25 in Berlin runs 25 hours: CEST midnight is 22:00Z the
// evening before, the next CET midnight 23:00Z. A dose at 23:30 local
// (22:30Z) lies inside the day and must be counted.
const dstNow = new Date("2026-10-25T20:00:00.000Z");
await buildMedsTodayBlock(fakePrisma, "user-1", BERLIN, dstNow);
const arg = projectTodayIntakesAndRecompute.mock.calls[0][0] as {
todayStart: Date;
todayEnd: Date;
};
expect(arg.todayStart.toISOString()).toBe("2026-10-24T22:00:00.000Z");
expect(arg.todayEnd.toISOString()).toBe("2026-10-25T23:00:00.000Z");
const where = capturedTodayWhere as {
scheduledFor: { gte: Date; lt: Date };
};
expect(where.scheduledFor.lt.toISOString()).toBe(
"2026-10-25T23:00:00.000Z",
);
expect(
where.scheduledFor.lt.getTime() >
new Date("2026-10-25T22:30:00.000Z").getTime(),
).toBe(true);
});
});
describe("buildMedsTodayBlock — tally", () => {
it("counts scheduled / taken / skipped with the summary-tile semantics", async () => {
todayRows = [
+5 -4
View File
@@ -104,10 +104,11 @@ export async function buildMedsTodayBlock(
): Promise<MedsTodayBlock> {
const { start: todayStart, end: todayEnd } = getUserTodayBounds(now, userTz);
// The projector + tally read use an exclusive upper bound (`lt`),
// matching the summary route's window convention.
const todayEndExclusive = new Date(
todayStart.getTime() + 24 * 60 * 60 * 1000,
);
// matching the summary route's window convention. `todayEnd` is the
// inclusive last millisecond of the real local day, so +1 ms is the next
// day's start: 23 or 25 hours on a DST day, where a fixed 24 h missed the
// last hour of the 25-hour day.
const todayEndExclusive = new Date(todayEnd.getTime() + 1);
// Projection FIRST so the tally read below sees the freshly minted
// pending rows. Idempotent — `skipDuplicates` + the