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) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-28 08:08:09 +02:00
1 parent 344d1c33b0
commit 39430e16c0
1 file changed
+32
@@ -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);
}