Refuse a first course beside a medication taken continuously

A medication with no course rows and no window is running since its
creation; a course added beside it projected the medication into ending,
which stopped its reminders and dropped its history from compliance. The
create now refuses that (medications.course.currentOpen; End course
writes the first course). A window the rows never recorded is
materialised as the first course before the new one is checked.
This commit is contained in:
Marc-André Bombeck committed 2026-10-03 23:11:28 +02:00
1 parent 1494409209
commit 16c16000e7
2 files changed
+67 -1

No files matched your search

+34 -1
View File
@@ -178,8 +178,41 @@ export async function createCourse(
args.userId,
args.medicationId,
);
// A medication without course rows is not empty history. With no window
// at all it has been taken continuously since creation and is running
// now, so a course cannot be added beside it (it ends its course first,
// "End course" writes one). With a window the rows never recorded (a
// file restored past the backfill), that window is its first course and
// is materialised before the new one is checked against it. Either way
// nothing the person already recorded can be cut off by the projection.
let existing: { startsOn: Date; endsOn: Date | null }[] = courses;
if (courses.length === 0) {
if (medication.startsOn === null && medication.endsOn === null) {
throw new CourseWriteError("currentOpen");
}
const implicit = windowAsCourse(
medication.startsOn,
medication.endsOn,
undefined,
medication.createdAt,
args.timeZone,
);
refuseIfInvalid(
[implicit, { startsOn: args.startsOn, endsOn: args.endsOn }],
todayKey,
medication.oneShot,
);
await tx.medicationCourse.create({
data: {
medicationId: args.medicationId,
userId: args.userId,
...implicit,
},
});
existing = [implicit];
}
refuseIfInvalid(
[...courses, { startsOn: args.startsOn, endsOn: args.endsOn }],
[...existing, { startsOn: args.startsOn, endsOn: args.endsOn }],
todayKey,
medication.oneShot,
);
@@ -275,6 +275,39 @@ describe("course writes keep the projection", () => {
});
});
it("never lets a first course cut off a chronic medication's history", async () => {
// Taken continuously since creation: no window, no course row. A course
// added beside it would project the medication into ending.
const chronic = await makeMedication({});
const refused = await postCourse(chronic.id, {
startsOn: "2026-03-01",
endsOn: "2026-03-31",
});
expect(refused.status).toBe(422);
expect((await refused.json()).meta.errorCode).toBe(
"medications.course.currentOpen",
);
expect(await courses(chronic.id)).toEqual([]);
expect(await window(chronic.id)).toEqual({ startsOn: null, endsOn: null });
// A window the rows never recorded (written past the course writer, as
// an older restore could) becomes the first course before the new one.
const legacy = await makeMedication({});
await getPrismaClient().medication.update({
where: { id: legacy.id },
data: { startsOn: D("2026-01-10"), endsOn: D("2026-01-20") },
});
const added = await postCourse(legacy.id, {
startsOn: "2026-03-01",
endsOn: "2026-03-05",
});
expect(added.status).toBe(201);
expect(await courses(legacy.id)).toEqual([
["2026-01-10", "2026-01-20"],
["2026-03-01", "2026-03-05"],
]);
});
it("lets the medication's own PUT edit the latest course", async () => {
const med = await makeMedication({
startsOn: "2026-03-01",