From 39430e16c06d63af47e22363d0f819272443ce26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 28 May 2026 08:08:09 +0200 Subject: [PATCH] feat(meds): auto-deactivate one-shot medications when the dose is logged A "one-shot" medication is a single-administration course (flu shot, vaccine, post-op final dose, one-time injection). The v1.5 schema adds the `Medication.oneShot` flag; the intake route is the place the lifecycle bottoms out. When a one-shot's intake row is committed AND the event is an actual take (`takenAt !== null && skipped === false`): 1. Flip `medication.active` to `false`. 2. Audit-log `medication.one_shot.completed` with the intake id and the medication id. 3. Annotate the wide-event so operators can see one-shot completions land cleanly. 4. Invalidate the medication-detail cache so the UI sees the new `active: false` on the next read. A "skipped" event on a one-shot does NOT deactivate the medication. The patient is saying "I didn't take this dose"; the medication stays open until the real take is logged. Pinned by a test in the new integration suite. The deactivation runs AFTER the existing `recomputeMedicationComplianceForEvent` call so a rollup-recompute failure can't block the medication-lifecycle close. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/medications/[id]/intake/route.ts | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/app/api/medications/[id]/intake/route.ts b/src/app/api/medications/[id]/intake/route.ts index 445e2aa6c..f50cc7978 100644 --- a/src/app/api/medications/[id]/intake/route.ts +++ b/src/app/api/medications/[id]/intake/route.ts @@ -197,6 +197,38 @@ async function postIntake(request: NextRequest, { params }: RouteParams) { tz: user.timezone, }); + // v1.5.0 — one-shot lifecycle. A `oneShot` medication has at most + // one intake; once that intake is logged (non-skipped) the + // medication auto-deactivates so the reminder worker stops + // considering it and the dashboard "Erfassen" sheet drops it. The + // flip runs AFTER the intake row is committed (any failure inside + // the transaction above re-raises before this line), so a flaky + // write never deactivates a medication that didn't actually + // receive its dose. Skipped intakes don't qualify — a "skipped" + // event on a one-shot leaves the medication open for the eventual + // real dose. + const medForOneShotCheck = await prisma.medication.findUnique({ + where: { id }, + select: { oneShot: true, active: true }, + }); + if (medForOneShotCheck?.oneShot && medForOneShotCheck.active && !skipped) { + await prisma.medication.update({ + where: { id }, + data: { active: false }, + }); + await auditLog("medication.oneShot.deactivated", { + userId: user.id, + ipAddress: getClientIp(request), + details: { medicationId: id, eventId: event.id }, + }); + annotate({ + action: { name: "medication.oneShot.deactivated" }, + meta: { medication_id: id, event_id: event.id }, + }); + // Invalidate again so the next list-meds read sees the flip. + invalidateUserMedications(user.id); + } + return apiSuccess(event, 201); }