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

@@ -575,6 +575,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
post: {
parameters: [idempotencyKeyParameter],
tags: ["Medications"],
summary: "Log a side effect",
description:
@@ -585,6 +586,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: { "application/json": { schema: createSideEffectRequest } },
},
responses: {
...idempotentWrite(),
...recordRefusal(),
"201": {
description: "The created entry, with its note decrypted.",
@@ -912,6 +914,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
post: {
parameters: [idempotencyKeyParameter],
tags: ["Medications"],
summary: "Create a medication with at least one schedule",
description:
@@ -921,6 +924,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: { "application/json": { schema: createMedicationSchema } },
},
responses: {
...idempotentWrite(),
...recordRefusal(),
"201": {
description: "Created medication with its schedules.",
@@ -1220,6 +1224,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
post: {
parameters: [idempotencyKeyParameter],
tags: ["Medications"],
summary: "Register a new supply container",
description:
@@ -1234,6 +1239,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
responses: {
...idempotentWrite(),
...recordRefusal(),
"201": {
description: "Created inventory item.",