diff --git a/src/lib/doctor-report/collect.ts b/src/lib/doctor-report/collect.ts index 0effcf3ba..83157acda 100644 --- a/src/lib/doctor-report/collect.ts +++ b/src/lib/doctor-report/collect.ts @@ -189,7 +189,34 @@ export async function collectDoctorReportData( }), wantsMedications ? prisma.medication.findMany({ - where: { userId, active: true }, + // The report describes its window, and a link can pin that window + // in the past: a medication whose course had not begun by the + // window's end, or had ended before it began, is not part of it. + // One with a dose scheduled inside the window was taken then, + // whenever it was entered. Dose history and the last intake stop + // at the window's end too, so a report for March does not show a + // dose set in June. + where: { + userId, + active: true, + AND: [ + { + OR: [ + { startsOn: { lte: end } }, + { startsOn: null, createdAt: { lte: end } }, + { + intakeEvents: { + some: { + deletedAt: null, + scheduledFor: { gte: start, lte: end }, + }, + }, + }, + ], + }, + { OR: [{ endsOn: null }, { endsOn: { gte: start } }] }, + ], + }, include: { schedules: { select: { ...SCHEDULE_COMPLIANCE_SELECT, label: true }, @@ -205,9 +232,12 @@ export async function collectDoctorReportData( }, }, pauseEras: { select: { pausedAt: true, resumedAt: true } }, - doseChanges: { orderBy: { effectiveFrom: "asc" } }, + doseChanges: { + where: { effectiveFrom: { lte: end } }, + orderBy: { effectiveFrom: "asc" }, + }, intakeEvents: { - where: { takenAt: { not: null } }, + where: { takenAt: { not: null, lte: end } }, orderBy: { takenAt: "desc" }, take: 1, select: { takenAt: true, injectionSite: true }, diff --git a/tests/integration/doctor-report-fixed-window.test.ts b/tests/integration/doctor-report-fixed-window.test.ts new file mode 100644 index 000000000..72116145a --- /dev/null +++ b/tests/integration/doctor-report-fixed-window.test.ts @@ -0,0 +1,141 @@ +/** + * A doctor report whose window ends in the past describes that window only. + * + * A share link can pin the end date. The medication list, the dose history + * and the last injection were read as of today, so a report for the first + * quarter listed a medication started in June, a dose set in June and an + * injection logged last week. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +process.env.ENCRYPTION_KEY ??= + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +import { collectDoctorReportData } from "@/lib/doctor-report-data"; +import { ALL_LEAF_IDS } from "@/lib/report-selection/catalogue"; +import { selectionFromLeaves } from "@/lib/report-selection/selection"; + +import { getPrismaClient, truncateAllTables } from "./setup"; + +vi.mock("@/lib/db-compat", () => ({ + ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), +})); + +const RANGE = { + start: new Date("2026-01-01T00:00:00.000Z"), + end: new Date("2026-03-31T23:59:59.999Z"), + days: 90, +}; + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("doctor report with a fixed end date", () => { + it("leaves out medications, doses and injections that came after the window", async () => { + const prisma = getPrismaClient(); + const user = await prisma.user.create({ + data: { + username: "fixed-window", + email: "fixed-window@example.test", + timezone: "UTC", + }, + }); + const during = await prisma.medication.create({ + data: { + userId: user.id, + name: "Semaglutide", + dose: "0.5 mg", + treatmentClass: "GLP1", + startsOn: new Date("2026-01-10T00:00:00.000Z"), + createdAt: new Date("2026-01-10T00:00:00.000Z"), + }, + }); + await prisma.medication.create({ + data: { + userId: user.id, + name: "Started later", + dose: "10 mg", + startsOn: new Date("2026-06-01T00:00:00.000Z"), + createdAt: new Date("2026-06-01T00:00:00.000Z"), + }, + }); + await prisma.medication.create({ + data: { + userId: user.id, + name: "Entered later, no start date", + dose: "5 mg", + createdAt: new Date("2026-07-01T00:00:00.000Z"), + }, + }); + // Entered after the window, but with a dose scheduled inside it: it was + // taken then, so it belongs on the report. + const enteredLater = await prisma.medication.create({ + data: { + userId: user.id, + name: "Entered later, taken in March", + dose: "20 mg", + createdAt: new Date("2026-08-01T00:00:00.000Z"), + }, + }); + await prisma.medicationIntakeEvent.create({ + data: { + userId: user.id, + medicationId: enteredLater.id, + scheduledFor: new Date("2026-03-05T08:00:00.000Z"), + takenAt: new Date("2026-03-05T08:10:00.000Z"), + }, + }); + await prisma.medicationDoseChange.createMany({ + data: [ + { + medicationId: during.id, + effectiveFrom: new Date("2026-01-10T00:00:00.000Z"), + doseValue: 0.25, + doseUnit: "mg", + }, + { + medicationId: during.id, + effectiveFrom: new Date("2026-06-15T00:00:00.000Z"), + doseValue: 1, + doseUnit: "mg", + }, + ], + }); + await prisma.medicationIntakeEvent.createMany({ + data: [ + { + userId: user.id, + medicationId: during.id, + scheduledFor: new Date("2026-03-20T08:00:00.000Z"), + takenAt: new Date("2026-03-20T08:05:00.000Z"), + }, + { + userId: user.id, + medicationId: during.id, + scheduledFor: new Date("2026-07-20T08:00:00.000Z"), + takenAt: new Date("2026-07-20T08:05:00.000Z"), + }, + ], + }); + + const data = await collectDoctorReportData( + user.id, + RANGE, + selectionFromLeaves(ALL_LEAF_IDS), + ); + + expect(data.medications?.map((m) => m.name).sort()).toEqual([ + "Entered later, taken in March", + "Semaglutide", + ]); + const glp1 = data.glp1?.medications[0]; + expect(glp1?.doseHistory.map((d) => d.value)).toEqual([0.25]); + expect(glp1?.currentDose?.value).toBe(0.25); + expect(glp1?.lastInjection?.date).toBe("2026-03-20T08:05:00.000Z"); + }); +});