Files
HealthLog/tests/integration/medication-rolling-overdue-consumers.test.ts
T

298 lines
9.1 KiB
TypeScript

/**
* v1.34.1 (#664 follow-up) — one persisted rolling occurrence must have
* one identity across every medication consumer.
*
* This is intentionally a RED contract for the diagnosed Mounjaro defect:
* the weekly occurrence anchored on 2026-07-28 remains actionable after the
* user's local day changes. The list and Dashboard are the authoritative
* server reads; the GLP-1 card and take-all derivations consume the list
* payload exactly as production does.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { resolveDisplayedSlotInstant } from "@/components/medications/card-parts/displayed-slot-instant";
import {
deriveDueMedications,
type DueDerivationMedication,
} from "@/components/medications/take-all-due";
import { buildMedsTodayBlock } from "@/lib/dashboard/meds-today";
import { buildMedicationsList } from "@/lib/medications/list-read";
import { applyCanonicalSlotWrite } from "@/lib/medications/scheduling/slot-upsert";
import { getPrismaClient, truncateAllTables } from "./setup";
const USER_ID = "user-rolling-overdue-consumers";
const USER_TZ = "Europe/Berlin";
const NOW = new Date("2026-07-29T10:00:00.000Z");
const LAST_TAKEN = new Date("2026-07-21T06:00:00.000Z");
const OVERDUE_OCCURRENCE = new Date("2026-07-28T06:00:00.000Z");
const NEXT_OCCURRENCE = new Date("2026-08-04T06:00:00.000Z");
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
beforeEach(async () => {
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(NOW);
const prisma = getPrismaClient();
await truncateAllTables(prisma);
await prisma.user.create({
data: {
id: USER_ID,
username: "rolling-overdue-consumers",
email: "rolling-overdue-consumers@example.test",
timezone: USER_TZ,
},
});
});
afterEach(() => {
vi.useRealTimers();
});
async function createMounjaro(): Promise<string> {
const prisma = getPrismaClient();
const medication = await prisma.medication.create({
data: {
userId: USER_ID,
name: "Mounjaro",
dose: "5mg",
active: true,
treatmentClass: "GLP1",
deliveryForm: "INJECTION",
startsOn: new Date("2026-01-01T00:00:00.000Z"),
createdAt: new Date("2026-01-01T00:00:00.000Z"),
schedules: {
create: {
windowStart: "08:00",
windowEnd: "08:00",
timesOfDay: ["08:00"],
daysOfWeek: null,
rrule: null,
rollingIntervalDays: 7,
scheduleType: "SCHEDULED",
},
},
},
});
await prisma.medicationIntakeEvent.create({
data: {
userId: USER_ID,
medicationId: medication.id,
scheduledFor: LAST_TAKEN,
takenAt: LAST_TAKEN,
skipped: false,
source: "WEB",
},
});
return medication.id;
}
async function readConsumerOccurrence(
medicationId: string,
complianceStatus: "overdue" | "upcoming" = "overdue",
) {
const prisma = getPrismaClient();
const list = await buildMedicationsList(USER_ID, USER_TZ);
const raw = list.find((row) => row.id === medicationId);
expect(raw).toBeDefined();
const medication = raw as unknown as DueDerivationMedication;
const listOccurrence = medication.nextDueAt ?? null;
const dashboard = await buildMedsTodayBlock(prisma, USER_ID, USER_TZ, NOW);
// A prior-day rolling slot is outside today's clock window. The GLP-1
// card therefore displays the server's canonical nextDueAt verbatim.
const glp1Occurrence =
resolveDisplayedSlotInstant({
currentWindowStatus: {
status: null,
schedule: null,
window: null,
},
nextDueAt: listOccurrence,
now: NOW,
timeZone: USER_TZ,
})?.toISOString() ?? null;
const takeAll = deriveDueMedications([medication], {
now: NOW,
tz: USER_TZ,
// Mirrors the compliance row that keeps the card action enabled for an
// unresolved weekly dose after its same-day window has passed.
doseStatusById: new Map([[medicationId, complianceStatus]]),
});
return {
list: {
occurrence: listOccurrence,
overdue: medication.nextDueOverdue === true,
},
dashboard: {
occurrence:
dashboard.dueCandidates?.find(
(candidate) => candidate.medicationId === medicationId,
)?.dueAt ?? null,
overdue:
dashboard.dueCandidates?.find(
(candidate) => candidate.medicationId === medicationId,
)?.overdue ?? false,
},
glp1Card: glp1Occurrence,
takeAllDue:
takeAll
.find((entry) => entry.id === medicationId)
?.scheduledFor?.toISOString() ?? null,
};
}
describe("rolling weekly overdue occurrence — shared consumers", () => {
it("keeps the prior-day occurrence authoritative in list, Dashboard, GLP-1, and take-all", async () => {
const medicationId = await createMounjaro();
await expect(readConsumerOccurrence(medicationId)).resolves.toEqual({
list: {
occurrence: OVERDUE_OCCURRENCE.toISOString(),
overdue: true,
},
dashboard: {
occurrence: OVERDUE_OCCURRENCE.toISOString(),
overdue: true,
},
glp1Card: OVERDUE_OCCURRENCE.toISOString(),
takeAllDue: OVERDUE_OCCURRENCE.toISOString(),
});
});
it.each(["taken", "skipped", "autoMissed"] as const)(
"an exact %s action suppresses the overdue occurrence everywhere and advances",
async (status) => {
const prisma = getPrismaClient();
const medicationId = await createMounjaro();
await prisma.medicationIntakeEvent.create({
data: {
userId: USER_ID,
medicationId,
scheduledFor: OVERDUE_OCCURRENCE,
takenAt:
status === "taken" ? new Date("2026-07-28T06:10:00.000Z") : null,
skipped: status === "skipped",
autoMissed: status === "autoMissed",
source: "WEB",
},
});
const state = await readConsumerOccurrence(medicationId, "upcoming");
expect(state.list).toEqual({
occurrence: NEXT_OCCURRENCE.toISOString(),
overdue: false,
});
expect(state.dashboard).toEqual({
occurrence: NEXT_OCCURRENCE.toISOString(),
overdue: false,
});
expect(state.glp1Card).toBe(NEXT_OCCURRENCE.toISOString());
expect(state.takeAllDue).toBeNull();
},
);
it("concurrent and retried writes converge onto one live exact-occurrence row", async () => {
const prisma = getPrismaClient();
const medicationId = await createMounjaro();
const write = (idempotencyKey: string) =>
applyCanonicalSlotWrite({
client: prisma,
userId: USER_ID,
medicationId,
canonicalSlot: OVERDUE_OCCURRENCE,
takenAt: new Date("2026-07-29T10:01:00.000Z"),
skipped: false,
isExplicitTaken: true,
isExplicitSkip: false,
idempotencyKey,
createSource: "WEB",
});
const results = await Promise.all([
write("rolling-concurrent-a"),
write("rolling-concurrent-b"),
]);
await write("rolling-concurrent-retry");
expect(results).toHaveLength(2);
const rows = await prisma.medicationIntakeEvent.findMany({
where: {
userId: USER_ID,
medicationId,
scheduledFor: OVERDUE_OCCURRENCE,
deletedAt: null,
},
});
expect(rows).toHaveLength(1);
expect(rows[0]?.takenAt).not.toBeNull();
});
it("different occurrence, archived row, and sibling medication never block the exact write", async () => {
const prisma = getPrismaClient();
const medicationId = await createMounjaro();
const siblingId = await createMounjaro();
await prisma.medicationIntakeEvent.createMany({
data: [
{
userId: USER_ID,
medicationId,
scheduledFor: new Date("2026-07-27T06:00:00.000Z"),
takenAt: new Date("2026-07-27T06:05:00.000Z"),
skipped: false,
source: "WEB",
},
{
userId: USER_ID,
medicationId,
scheduledFor: OVERDUE_OCCURRENCE,
takenAt: new Date("2026-07-27T06:05:00.000Z"),
skipped: false,
source: "REMINDER",
deletedAt: new Date("2026-07-27T12:00:00.000Z"),
},
{
userId: USER_ID,
medicationId: siblingId,
scheduledFor: OVERDUE_OCCURRENCE,
takenAt: new Date("2026-07-28T06:05:00.000Z"),
skipped: false,
source: "WEB",
},
],
});
const result = await applyCanonicalSlotWrite({
client: prisma,
userId: USER_ID,
medicationId,
canonicalSlot: OVERDUE_OCCURRENCE,
takenAt: new Date("2026-07-29T10:02:00.000Z"),
skipped: false,
isExplicitTaken: true,
isExplicitSkip: false,
idempotencyKey: "rolling-isolation-target",
createSource: "WEB",
});
expect(result.outcome).toBe("inserted");
expect(result.row.scheduledFor).toEqual(OVERDUE_OCCURRENCE);
expect(
await prisma.medicationIntakeEvent.count({
where: {
userId: USER_ID,
medicationId,
scheduledFor: OVERDUE_OCCURRENCE,
deletedAt: null,
},
}),
).toBe(1);
});
});