fix(insights): keep a medication course in the storyline through its last day

endsOn is a calendar date, and the storyline compared it with the instant
now, so a course dropped out on its final day. Compare it with the user's
own today as the column's date value instead.
This commit is contained in:
Marc-André Bombeck committed 2026-10-01 09:35:11 +02:00
1 parent 67d88de3d5
commit 9662a3d104
3 files changed
+35 -2

No files matched your search

@@ -194,3 +194,20 @@ describe("adherence storyline ↔ dashboard-tile engine parity", () => {
expect(story).toBeNull();
});
});
describe("adherence storyline course window", () => {
it("keeps a course on its last day in the user's zone", async () => {
// 2026-06-21 20:00 in Los Angeles, already the 22nd in UTC. A course
// whose `endsOn` (a calendar date) is the 21st is still running.
const now = new Date("2026-06-22T03:00:00.000Z");
await buildAdherenceStoryline("user-1", "America/Los_Angeles", now);
const where = mocks.medicationFindMany.mock.calls[0]![0].where as {
OR: [unknown, { endsOn: { gte: Date } }];
};
const lastDay = new Date("2026-06-21T00:00:00.000Z"); // @db.Date value
expect(where.OR[1].endsOn.gte.getTime()).toBeLessThanOrEqual(
lastDay.getTime(),
);
expect(where.OR[1].endsOn.gte.toISOString()).toBe(lastDay.toISOString());
});
});
@@ -28,6 +28,7 @@ import { probeRollupCoverage } from "@/lib/rollups/measurement-coverage";
import { readDayMeanSeries } from "@/lib/insights/derived/baseline";
import { buildScheduleAnchoredComplianceBuckets } from "@/lib/analytics/schedule-anchored-compliance";
import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver";
import { userDayKey } from "@/lib/tz/format";
import { sanitizeForPrompt } from "@/lib/insights/sanitize";
import {
inferMedTargetClass,
@@ -35,6 +36,7 @@ import {
type MedTargetClass,
} from "@/lib/medications/med-target-map";
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
import { dbDate } from "@/lib/tz/date-only";
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const MAD_TO_SIGMA = 1.4826;
@@ -160,7 +162,11 @@ export async function buildAdherenceStoryline(
timezone: string | null,
now: Date = new Date(),
): Promise<AdherenceStoryline | null> {
// 1. Active medications with a confidently-known target class.
// 1. Active medications with a confidently-known target class. `endsOn` is
// a calendar date, so a course is over only once the user's own day has
// passed it; comparing the column against the instant `now` dropped the
// course's last day.
const todayKey = userDayKey(now, timezone ?? DEFAULT_TIMEZONE);
const meds = await prisma.medication.findMany({
where: {
userId,
@@ -168,7 +174,7 @@ export async function buildAdherenceStoryline(
// v1.39.1 (#1033) — the adherence figure below counts tracked
// medications only, so a record-only one cannot be its subject.
...TRACKED_INTAKE_WHERE,
OR: [{ endsOn: null }, { endsOn: { gte: now } }],
OR: [{ endsOn: null }, { endsOn: { gte: dbDate(todayKey) } }],
},
select: { name: true, treatmentClass: true },
});
+10
View File
@@ -34,3 +34,13 @@ export function dateOnlyAtNoonUtc(key: string): Date {
export function dateOnlyKey(value: Date): string {
return value.toISOString().slice(0, 10);
}
/**
* The value a `@db.Date` column holds for calendar date `key` (Prisma reads
* and writes those columns as UTC midnight). Use it to compare such a column
* with a day: `endsOn >= dbDate(todayKey)` keeps a course through its last
* day, where comparing against the instant `now` dropped that day.
*/
export function dbDate(key: string): Date {
return new Date(`${key}T00:00:00.000Z`);
}