fix(doctor-report): compare course dates as days in the report zone

A medication's start and end dates are calendar dates, but the report
compared them against the window's instants. West of UTC the window
begins hours after UTC midnight, so a course that ended on the first day
was left out, and one that started the day after the window was listed.
Both now compare against the window's first and last day in the report
zone.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 22:11:27 +02:00
1 parent 6769f90f4c
commit 04eb8dd7f8
2 files changed
+51 -3

No files matched your search

+9 -3
View File
@@ -61,7 +61,7 @@ import { loadDenseMeasurementBuckets } from "./dense-buckets";
import { summariseDenseBuckets } from "./measurement-series";
import { emptyGlucoseClinical } from "./glucose-panel";
import { buildAdministrationLedger, buildGlp1Block } from "./medications";
import { validTimezoneOr, DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { userDayKey, validTimezoneOr, DEFAULT_TIMEZONE } from "@/lib/tz/format";
import {
loadAllergies,
loadAnamnesis,
@@ -148,6 +148,12 @@ export async function collectDoctorReportData(
},
});
const reportTz = validTimezoneOr(userProfile?.timezone, DEFAULT_TIMEZONE);
// `startsOn` / `endsOn` are calendar dates (stored as UTC midnight), so
// they compare against the window's first and last day in the report zone,
// not its instants: west of UTC the window starts hours after UTC midnight,
// and a course that ended on the first day read as over before it began.
const firstDay = new Date(`${userDayKey(start, reportTz)}T00:00:00.000Z`);
const lastDay = new Date(`${userDayKey(end, reportTz)}T00:00:00.000Z`);
const aggregateDenseTypes = days > DENSE_REPORT_RAW_WINDOW_DAYS;
const densePulse = aggregateDenseTypes && !excluded.includes("PULSE");
@@ -202,7 +208,7 @@ export async function collectDoctorReportData(
AND: [
{
OR: [
{ startsOn: { lte: end } },
{ startsOn: { lte: lastDay } },
{ startsOn: null, createdAt: { lte: end } },
{
intakeEvents: {
@@ -214,7 +220,7 @@ export async function collectDoctorReportData(
},
],
},
{ OR: [{ endsOn: null }, { endsOn: { gte: start } }] },
{ OR: [{ endsOn: null }, { endsOn: { gte: firstDay } }] },
],
},
include: {
@@ -138,4 +138,46 @@ describe("doctor report with a fixed end date", () => {
expect(glp1?.currentDose?.value).toBe(0.25);
expect(glp1?.lastInjection?.date).toBe("2026-03-20T08:05:00.000Z");
});
it("compares a course's start and end dates as days in the report zone", async () => {
// West of UTC the window's instants sit hours after UTC midnight, and the
// course dates are calendar days stored as UTC midnight.
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "west-window",
email: "west-window@example.test",
timezone: "America/New_York",
},
});
const course = (name: string, startsOn: string, endsOn: string | null) =>
prisma.medication.create({
data: {
userId: user.id,
name,
dose: "1 mg",
startsOn: new Date(`${startsOn}T00:00:00.000Z`),
endsOn: endsOn ? new Date(`${endsOn}T00:00:00.000Z`) : null,
createdAt: new Date("2025-06-01T00:00:00.000Z"),
},
});
await course("Ended on the first day", "2025-10-01", "2026-01-01");
await course("Ended the day before", "2025-10-01", "2025-12-31");
await course("Started on the last day", "2026-03-31", null);
await course("Started the day after", "2026-04-01", null);
const data = await collectDoctorReportData(
user.id,
{
start: new Date("2026-01-01T05:00:00.000Z"),
end: new Date("2026-04-01T03:59:59.999Z"),
days: 90,
},
selectionFromLeaves(ALL_LEAF_IDS),
);
expect(data.medications?.map((m) => m.name).sort()).toEqual([
"Ended on the first day",
"Started on the last day",
]);
});
});