feat(medications): keep a medication as a record without tracking intake

A per-medication trackIntake flag (migration 0351, default on for every
existing and new row). With it off the dose, dates and schedule stay stored,
but nothing derives a due dose from them: the reminder worker, the manual
reminder check, the today projector, the auto-miss pass, the low-stock pass,
the doses card, the today list, the outstanding-dose badge, compliance and
its heatmap, the doctor report, the Coach, the BP-status gate, insights,
targets, achievements, the efficacy view and the MCP schedule tool all skip
it.

The medication wire serves such a medication with schedules: [] and the
stored rows in an additive recordedSchedules field, so a client that arms
local reminders from schedules stays silent without an update. Switching
tracking off or back on archives a schedule revision at that instant, the
untracked stretch with an empty payload, so it never counts as missed and
the projector does not put a dose from before the switch back on the card.

A structural guard freezes the set of schedule readers: each one filters on
the flag, derives from dueSchedules, shapes the wire with scheduleWireFields,
or is allowlisted with the reason it derives nothing due. The flag travels
through backup, restore and the medication CSV export.
This commit is contained in:
Marc-André Bombeck committed 2026-09-25 08:57:10 +02:00
1 parent 700de6d228
commit b4c2706ec9
57 files changed
+1490 -182

No files matched your search

@@ -92,7 +92,7 @@ describe("POST /api/admin/notifications/reminder-check", () => {
expect(res.status).toBe(200);
const json = (await res.json()) as { data: { scoped: boolean } };
expect(json.data.scoped).toBe(false);
expect(whereOfLastFindMany()).toEqual({ active: true });
expect(whereOfLastFindMany()).toEqual({ active: true, trackIntake: true });
});
it("narrows the sweep to the named account", async () => {
@@ -102,6 +102,7 @@ describe("POST /api/admin/notifications/reminder-check", () => {
expect(json.data.scoped).toBe(true);
expect(whereOfLastFindMany()).toEqual({
active: true,
trackIntake: true,
userId: "cku0mock0account0id",
});
});
@@ -8,6 +8,7 @@ import { adminReminderCheckSchema } from "@/lib/validations/notifications";
import { parseScheduleRecurrence } from "@/lib/medication-schedule";
import { dispatchLocalisedNotification } from "@/lib/notifications/dispatch-localised";
import { getUserTodayBounds, getDayOfWeekInTz } from "@/lib/tz/local-day";
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
export const dynamic = "force-dynamic";
@@ -99,6 +100,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
const medications = await prisma.medication.findMany({
where: {
active: true,
// v1.39.1 (#1033) — the manual check reminds exactly what the worker
// reminds: never a medication with intake tracking off.
...TRACKED_INTAKE_WHERE,
// Absent selector ⇒ no `userId` key ⇒ the instance-wide sweep.
...(scopedUserId ? { userId: scopedUserId } : {}),
},