Patient and prescription API contracts
Current implementation
Patient CRUD is implemented through Doctor-owned /api/v1/doctors/patients routes with persistence, authentication, organization access, subscription write gates, optimistic versions and audit. See the Doctor patient workflow for the active portal contract and provisioning.
The seven Prescription clinical operations below remain documentation-first Huma contracts: malformed JSON returns 400, invalid contracts return 422, and valid requests return 501. Their clinical lifecycle and storage are deferred. Patient remains a separate service with no dependency on Prescription; its public reader now performs authorized profile lookups.
The old in-memory prescription HTTP handlers are replaced. PUT draft updates and DELETE cancellation are removed; use PATCH and POST cancellation below. Existing in-memory persistence scaffolding remains unused for clinical documents. These clinical operations do not add independent deployments, frontend changes, or lab-result workflows. The separate global suggestion catalogs have implemented PostgreSQL-backed CRUD and their own migrations.
Operations
| Method | Path | Intended success |
|---|---|---|
| POST | /api/v1/doctors/patients | 201 Patient |
| GET | /api/v1/doctors/patients | 200 Page of Patient |
| GET | /api/v1/doctors/patients/{patientId} | 200 Patient |
| PATCH | /api/v1/doctors/patients/{patientId} | 200 Patient |
| POST | /api/v1/prescriptions | 201 Prescription |
| GET | /api/v1/prescriptions | 200 Page of Summary |
| GET | /api/v1/prescriptions/{prescriptionId} | 200 Prescription |
| PATCH | /api/v1/prescriptions/{prescriptionId} | 200 Prescription |
| POST | /api/v1/prescriptions/{prescriptionId}/issue | 200 Prescription |
| POST | /api/v1/prescriptions/{prescriptionId}/cancel | 200 Prescription |
| GET | /api/v1/prescriptions/patient-history/{patientId} | 200 PatientPrescriptionHistory |
Protected operations declare the existing bearerAuth scheme. For Prescription clinical stubs the header remains optional: valid requests with no credentials or a placeholder bearer token still return 501. No endpoint returns a success example as if it were persisted data.
Minimal registration and creation
Register a patient using organization context, name, age and sex:
{"organization_id":"11111111-1111-4111-8111-111111111111","name":"Patient name","age":"25 years","sex":"unknown"}Name, age and sex are required. Age is a nonblank string accepting any text, without numeric or unit validation. Sex must be male, female, other or unknown; no value is supplied implicitly. Email, phone, blood group and a single address string are optional. Birth date, allergies and chronic conditions are removed from profiles and the shared patient snapshot contract. Patient ID, organization-scoped MRN, creator identity and timestamps are server-owned. Duplicate names and phone numbers do not automatically merge or reject patients.
Patient records age_recorded_at on creation and when normalized age changes. PATCH preserves omitted fields and permits null to clear optional fields. Name, age and sex cannot be null or blank when supplied. PATCH requires expected_version plus an editable field. Patient ID, MRN and organization are immutable. Doctor HTTP requires a professional profile, exact affiliation and scoped grants.
Start a new consultation with:
{"patient_id":"patient_123"}The future implementation loads the authorized patient using PatientReader, derives their organization, and resolves the authoring doctor from authentication. Never accept doctor identity or organization from spoofable X-User-ID/X-Org-ID headers. Patient and organization references are immutable. Each new consultation receives its own prescription ID and version 1. Multiple drafts for a patient are allowed; patient-keyed singleton drafts and speculative /drafts routes are not part of this contract. Encounter date defaults to the server's current date; visit reason, specialty ID, notes and all clinical sections are optional in a draft.
Clinical content and PATCH
The full workspace contract retains ordered complaints and detailed qualifiers, custom clarification answers, positive/negative history, textual observations, investigation orders, diagnoses, medication orders with structured SIG, advice, and follow-up. Optional catalog references are stored independently from their labels; manually named medicines remain representable without a catalog API. Medication SIG preserves frequency, finite/ongoing duration, meal relation, timing and instruction. Drafts may have incomplete directions. Ongoing and finite duration cannot be supplied together. Relative follow-up requires value and unit; it cannot be combined with an exact date. Clinical duration units are singular hour, day, week, month, and year; patient age is independent free text.
Complete workspace example is embedded into the OpenAPI request examples and exercised by contract tests.
Every prescription edit, issue and cancel requires expected_version:
{"expected_version":3,"advice":[{"id":"advice_1","text":"Rest"}]}A supplied array replaces that entire section; [] clears it. Omitted sections remain unchanged. Follow-up is replaced as a whole, with null clearing it. Nullable visit reason, specialty and notes can also be cleared. At least one editable field is required alongside the version. Null arrays are invalid. Clinical entry IDs are local identifiers within the document and ordering is preserved.
Supplied maps on public PATCH DTOs retain explicit null versus absence and are excluded from JSON/OpenAPI. Use those maps, not pointer nil alone, when applying future patches. Nullable object schemas use anyOf with null because the pinned Huma version does not support nullable tags on referenced objects. A schema-aware HTTP check also rejects null for non-nullable optional fields that Huma otherwise skips validating, including nested clinical arrays.
The intended implementation atomically compares versions and increments on success. Stale versions return 409. Clients update their local version from the mutation response, serialize/debounce saves, and refetch rather than blindly retry stale operations. No concurrency behavior is implemented by the stubs.
Lifecycle, sharing and history
Allowed transitions are draft to issued, draft to canceled, and issued to canceled. Only the authoring doctor with active organization affiliation can mutate records. Issuance requires a linked patient and at least one diagnosis, medication, investigation or advice entry. Every medicine requires a frequency or meaningful free-text instruction. These checks require persisted state and are TODO rules, not validation of the issue request body's version alone.
Issuance freezes the ordered clinical content, patient and doctor display snapshots, and issued_at. Later profile changes must not rewrite issued documents. Canceling requires a nonblank reason and expected_version, retaining content, snapshots and issuance metadata while recording cancellation actor/time/reason. Repeat issue or cancel attempts return 409; after ambiguous network responses, read current state. Corrections require canceling the original and creating a new draft. Printing is a frontend action, separate from issuance.
Active authorized organization members share issued/canceled history. Drafts are author-private. Patient and prescription access requires active identity and membership/affiliation. Out-of-scope identifiers return 404; known forbidden actions return 403. Authentication returns 401 once implemented. State/version conflicts return 409, and incomplete persisted clinical content at issuance returns 422. Unexpected errors return a generic 500 without internal details.
Both collection APIs require organization_id. Patients accept optional search (or the deprecated q alias) across name, ID, MRN, email, phone and address. Prescriptions accept patient_id, doctor_id and status. Prescription lists use {items, next_cursor?}; Patient lists use the success envelope with a data array and pagination containing next_cursor, current_page, per_page, total_pages and total_items. Both default to limit 20/max 100, newest first by created_at and ID descending. Cursors are opaque and bound to filters; Patient cursors also bind page size and current read access. Patient totals count only authorized profiles matching the search. Unknown query parameters are rejected on these contracts.
History is prescription-owned: its visits page contains issued/canceled summaries, including visible cancellation status, while recent observations derive only from issued records. Read prescription details to obtain previous medication/advice content and copy selected entries into a new draft without changing the source. There is no separate encounter repository, external lab result model, or claim that investigation orders are pending/completed.
Frontend mapping and verification
| Reference frontend behavior/type | API integration |
|---|---|
| Patient selection and registration | Patient search/detail plus POST patient with name, age and sex; never invent demographic values |
| Patient age | Send and display the reported age string without parsing or appending a unit |
| Patient.date / visitReason | Prescription encounter_date / visit_reason, not profile fields |
| camelCase clinical objects | Convert keys to snake_case recursively, including complaint details and SIG |
| patientId / medicationId / displayName / followUp | patient_id / medication_id / display_name / follow_up |
| Patient-keyed local draft restore | List author drafts filtered by patient, then restore a selected prescription ID |
| Full draft autosave | PATCH changed sections with current expected_version |
| Previous Rx and encounter drawers | Prescription history summaries and prescription detail; canceled status remains visible |
| Patient previous lab results/pending investigations | Deferred; do not invent data from prescription orders |
| Frontend array list responses | Patient: unwrap data and pagination.next_cursor; Prescription: items/next_cursor |
| Browser print | Remains local and does not implicitly issue |
This release does not change frontend services or local storage. A configured remote request returning 501 must not be mistaken for a saved server record. Global diagnosis, advice, history and investigation lookup catalogs are available through the prescription suggestion routes. Templates, layout settings, patient merging/deletion, lab entry and patient account connections remain outside this release.
Run go run ./tools/cli generate openapi to refresh the static document; both runtime OpenAPI and Scalar/Swagger use the existing tooling. Relevant checks include:
go test -race -short ./services/patient/... ./services/prescription/... ./services/api/...
make test
test -z "$(gofmt -l -s pkg services tools)"
make lintContract tests cover all routes, minimal creates, validation errors, PATCH presence/clearing and context forwarding, rich clinical content round trips, OpenAPI schemas/examples, generated/runtime parity, and removed legacy mutations. Future functional work must additionally test organization isolation, author-only writes, draft privacy, persisted issuance requirements, immutable snapshots, version races, cancellation retention, transactional persistence and post-commit events. None of those business scenarios is claimed as implemented here.