mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(medications): stock claims are atomic and only a dose becoming taken consumes
Two hardening passes on the consumption stamp: - consumeForIntake claims the stamp with a single conditional UPDATE (NULL-stamp predicate + row lock) instead of read-then-write, so a double tap racing a sync replay decrements exactly once; the loser sees count 0. restoreForIntake clears the stamp the same way, so concurrent restores cannot refund twice. - Every seam consumes only on the pending-to-taken transition. A sync replay that re-posts an already-taken history row, or an edit that corrects the timestamp of an old dose, never touches today's stock.
This commit is contained in:
1 parent
e21dfd040e
commit
7eeab2ae98
9 files changed
+345
-90
No files matched your search
@@ -191,6 +191,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
|
||||
let event;
|
||||
// v1.16.10 — only the genuine pending→taken transition consumes
|
||||
// inventory units (the slot upsert's `consumedTransition` flag). A
|
||||
// re-post converging onto an already-taken slot row — including a
|
||||
// pre-v1.16.10 row that predates the consumption stamp — must not
|
||||
// decrement stock again. A fresh ad-hoc create is always a take.
|
||||
let consumed = true;
|
||||
if (attribution.slotInstant) {
|
||||
// Scheduled dose — converge onto the one canonical slot row (the
|
||||
// pending REMINDER row when one exists) through the shared upsert:
|
||||
@@ -209,6 +215,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
attributionSource: "AUTO",
|
||||
});
|
||||
event = applied.row;
|
||||
consumed = applied.consumedTransition;
|
||||
} else {
|
||||
// Ad-hoc / PRN / off-window — standalone row under the documented
|
||||
// contract (`scheduledFor = takenAt`).
|
||||
@@ -227,14 +234,18 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
// v1.16.10 — the external ingest records a take; consume inventory
|
||||
// units. The stamp on the row keeps a retried POST exactly-once (the
|
||||
// idempotency probe above already short-circuits the common replay).
|
||||
await consumeForIntake({
|
||||
client: prisma,
|
||||
userId: apiToken.userId,
|
||||
medicationId: medication.id,
|
||||
eventId: event.id,
|
||||
intakeAt: effectiveTakenAt,
|
||||
});
|
||||
// idempotency probe above already short-circuits the common replay)
|
||||
// and the transition gate keeps a slot-converged re-post from
|
||||
// double-decrementing through a pre-stamp row.
|
||||
if (consumed) {
|
||||
await consumeForIntake({
|
||||
client: prisma,
|
||||
userId: apiToken.userId,
|
||||
medicationId: medication.id,
|
||||
eventId: event.id,
|
||||
intakeAt: effectiveTakenAt,
|
||||
});
|
||||
}
|
||||
|
||||
await auditLog("medication.ingest.external", {
|
||||
userId: apiToken.userId,
|
||||
|
||||
Reference in new issue
Block a user