fix(medications): accept an Idempotency-Key on the three replayed creates

The native client replays its offline outbox with the same
Idempotency-Key on every attempt, and `POST /api/medications`, the
side-effect log and the inventory register ran the handler again on the
replay: after a lost success response the account was left with a
duplicate medication, a duplicate symptom entry or a second pen in the
supply count. The dedupe on `(externalSource, externalId)` only ever
covered a MIRRORED create; a manually entered medication carries
neither field and had nothing to collapse a retry onto.

All three wrap in `withIdempotency`, the composition the batch ingest
already ships, and the three operations publish the header so a client
generated against the contract can tell that a retry is safe. Zod
parsing and `requireRecordAuth` are untouched.

An integration test drives each route twice against real Postgres and
asserts what a status check cannot: one row in the table, and a replayed
201 carrying `X-Idempotent-Replay: true`.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 01:04:57 +02:00
1 parent 1d372df3a0
commit 6fe1897941
6 files changed
+464 -187

No files matched your search

+7
View File
@@ -4834,6 +4834,7 @@ paths:
schema:
type: string
required: true
- *a6
requestBody:
required: true
content:
@@ -4855,6 +4856,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"409": *a7
"422": *a4
"429":
description: More than 30 side-effect writes in the trailing minute for this ACTOR.
@@ -5213,6 +5215,8 @@ paths:
medications.mirror.limit_exceeded`) once the caller already holds 30 medications mirrored from the same external
source; an idempotent re-post of a medication already mirrored is unaffected, and native creates are never
gated.
parameters:
- *a6
requestBody:
required: true
content:
@@ -5228,6 +5232,7 @@ paths:
$ref: "#/components/schemas/CreateMedicationResponse"
"401": *a1
"403": *a8
"409": *a7
"422": *a4
"429": *a5
/api/medications/layout:
@@ -5563,6 +5568,7 @@ paths:
schema:
type: string
required: true
- *a6
requestBody:
required: true
content:
@@ -5584,6 +5590,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"409": *a7
"422": *a4
"429": *a5
/api/medications/{id}/inventory/{itemId}: