# Prescriptions API — v1

Base: `/api/v1/prescriptions` (+ `/api/v1/formulary/search`) · Auth: Sanctum
bearer token · Branch: session if present, else `X-Branch` header. Every
endpoint is policy-gated.

There is **no DELETE for a prescription**: an issued one is cancelled or
superseded, a draft is cancelled too, and the record always survives. The one
DELETE removes a line from a draft — working material, not yet a record.
Formulary management and printing are deliberately web-only in v1.

## Endpoints

| Method | Path | Ability | Purpose |
|---|---|---|---|
| GET | `/formulary/search` | `formulary.view` | Type-ahead with per-patient allergy warnings |
| GET | `/prescriptions` | `prescriptions.view` | Paginated list, filterable |
| POST | `/prescriptions` | `prescriptions.write` | Start a draft |
| GET | `/prescriptions/{ulid}` | `prescriptions.view` | Full read with items |
| POST | `/prescriptions/{ulid}/issue` | `prescriptions.issue` | Issue — immutable from here |
| POST | `/prescriptions/{ulid}/cancel` | `prescriptions.cancel` | Cancel with a reason |
| POST | `/prescriptions/{ulid}/reissue` | `prescriptions.cancel` | Supersede with a corrected draft |
| POST | `/prescriptions/{ulid}/items` | `prescriptions.write` | Add a line to a draft |
| DELETE | `/prescriptions/{ulid}/items/{ulid}` | `prescriptions.write` | Remove a draft line |

### GET /formulary/search

Query: `q` (required, min 2 chars — a short query is a 422, not a silent
empty list), `patient` (optional ULID). With a patient, every result carries
`warnings` — the prescriber sees the conflict while choosing, not after
issuing.

### POST /prescriptions

Body: `patient` (ULID, required), `encounter` (ULID, optional). The
encounter must belong to the same patient — `422
prescription_encounter_mismatch` otherwise; the guard lives in the service,
so no door can cross-attach. The caller becomes the prescriber.
`201 prescription_started`.

### POST /prescriptions/{ulid}/items

Body: `drug` (formulary ULID, optional — free-text prescribing is never
blocked by an incomplete formulary), `drug_name` (required), `dose` +
`frequency` (required, free text by design), plus `generic_name`, `strength`,
`form`, `route`, `duration`, `quantity`, `quantity_unit`, `is_prn`,
`indication`, `instructions`. Formulary values are **copied onto the line**
at write time — the record must still read in 2031 as it was printed, and
the formulary is editable. `201 prescription_item_added`.

### GET /prescriptions/{ulid}

Items always embedded. For a **draft**, `meta.warnings` carries the live
allergy check — advice about now. For an **issued** prescription,
`data.allergy_warnings` and `data.allergy_override_reason` are what the
prescriber actually saw and wrote at the moment of issue, frozen with the
row. A superseded one links forward via `data.superseded_by`.

### POST /prescriptions/{ulid}/issue

Optional body: `allergy_override_reason` (min 10 chars when supplied).

- Empty draft → `422 prescription_not_editable` with `meta.context.reason:
  empty`.
- Conflicts with a recorded allergy and no reason → `409
  allergy_override_required`, warnings in `meta.context.warnings`. **Not a
  refusal to prescribe** — resubmit with a documented reason; what is refused
  is doing it silently.
- Success → `200 prescription_issued`; `valid_until` fixed on the row
  (shorter window when any line is a controlled drug), immutable from here —
  further writes are `403` (policy) or `422 prescription_not_editable` (the
  service backstop).

### POST /prescriptions/{ulid}/reissue

Body: `reason` (min 10 chars). Closes the original as `superseded` and
returns a **new draft with every line copied** (`201 prescription_reissued`,
`meta.supersedes` names the original) — the prescriber corrects rather than
retypes.

## Error codes

Registered permanently in [errors.md](errors.md) — see the Prescriptions
section. Cross-branch reads are `404 not_found`, byte-identical to a
nonexistent ULID.
