Medicine catalog API
Medicine owns a global catalog shared across organizations. All 32 operations are implemented using PostgreSQL. Authentication, authorization, audit recording, and events are not enabled yet; missing and placeholder credentials do not block requests. Malformed JSON returns 400, invalid fields or cursors return 422, missing records/references return 404, and lifecycle/duplicate conflicts return 409. An unconfigured database returns 503.
Data model
| Resource | Required creation fields | Meaning |
|---|---|---|
| manufacturers | name | Manufacturer of product brands |
| generics | name | Single ingredient name or named combination |
| brands | name, manufacturer_id, generic_id | Product name linked to manufacturer and generic |
| drugs | brand_id, form, strength | A brand/form/strength variant |
All resources accept optional description and status (active by default or inactive). Form and strength are nonblank display text, including combination strengths such as 500 mg + 65 mg. Drug responses include brand, generic and manufacturer summaries. IDs, created_at, updated_at and deleted_at are server-owned; timestamps use RFC 3339. Description is optional and nullable.
No pricing, packaging, clinical guidance, ingredient entities, dosage-form APIs, or imports are included. Medicine owns its schema migrations.
Routes and access
For each resource in the table above:
| Method | Path | Behavior |
|---|---|---|
| GET | /api/v1/medicines/ | Catalog list/search |
| GET | /api/v1/medicines/{resource}/ | Catalog detail |
| GET | /api/v1/admin/medicines/ | Administrative list/search |
| POST | /api/v1/admin/medicines/ | Create (201) |
| GET | /api/v1/admin/medicines/{resource}/ | Administrative detail |
| PATCH | /api/v1/admin/medicines/{resource}/ | Update supplied fields |
| DELETE | /api/v1/admin/medicines/{resource}/ | Soft-delete (200, data: {}) |
| POST | /api/v1/admin/medicines/{resource}/{id}/restore | Restore the same record |
Other success responses use 200. No authentication or permission checks run on these routes in this initial implementation. There is no organization context. Medicine remains the data owner. Admin orchestrates through independently injected medicine/public capabilities; it never imports Medicine internals.
Search and pagination
All lists accept optional q, cursor, and limit (default 20, range 1–100). Blank q lists records; text search is case-insensitive. Names are searched for manufacturers, generics and brands; drugs search related names, form and strength. Responses use the same envelope as admin organization routes:
{
"message": "Medicines retrieved successfully.",
"data": [],
"status_code": 200,
"pagination": {
"next_cursor": "",
"current_page": 1,
"per_page": 20,
"total_pages": 0,
"total_items": 0
}
}Detail and mutation responses place the entry in data and omit pagination. DELETE returns 200 with the success envelope and data: {}. Lists sort by created_at then ID descending. Opaque cursors bind to the resource, reader/admin visibility, page size, and all filters. An empty next_cursor marks the final page. Totals are computed before the cursor boundary is applied.
Brand lists additionally accept manufacturer_id and generic_id. Drug lists also accept brand_id and form. Form filtering matches the displayed form. Ordinary reads expose active, non-deleted entries with active, non-deleted parents. Admin lists include active and inactive entries by default, with optional status and deletion filters (live by default, deleted, or all). Admin detail can retrieve deleted records for restoration. Unknown body fields and query parameters are rejected.
Updates and removal
PATCH accepts each entity's creation fields plus description/status. Omitted fields remain unchanged; {"description":null} clears description. Null is invalid for names, relationships, form, strength and status. Empty patches are rejected.
Mutations validate references and duplicate conflicts and commit transactionally. Deleted records cannot be edited. Manufacturer and generic names are unique among live entries, ignoring case and surrounding spaces. Brand names are unique within a manufacturer. Drugs are unique by brand, form, and strength. Inactive entries participate in uniqueness checks; deleted entries release their identity, so restoring them can conflict with a replacement.
Catalog write transactions take a shared PostgreSQL advisory lock before loading state. This serializes maintenance writes so dependency checks cannot race with child creation, reparenting, or restoration. Reads remain concurrent. Database unique indexes enforce duplicate identity independently.
DELETE retains identity and prior active/inactive status. A parent with non-deleted dependents, even inactive ones, cannot be deleted (409); admins remove dependents first. Deletion never cascades. Repeated deletion returns 409. Restoration requires a deleted record and non-deleted parents; it retains the ID and prior status. Lifecycle and duplicate conflicts return 409. Restore parents before children. Inactive status is independent of soft deletion.
Examples and prescription follow-up
Create a drug:
{"brand_id":"brand_example","form":"Tablet","strength":"500 mg + 65 mg"}Deactivate an entry:
{"status":"inactive"}OpenAPI includes creation, update/clearing, and success-envelope examples. Prescription request/response contracts are unchanged. TODO comments in create, patch and issue use cases describe future catalog lookups, availability validation and immutable medication display data. No medicine reader is wired into Prescription in this release.
Apply the schema with go run ./tools/cli database migrate --service medicine. Run go run ./tools/cli generate openapi after changing registrations, then make test, make lint and test -z "$(gofmt -l -s pkg services tools)". Both the runtime API and offline generator register Medicine and inject the same Admin capabilities.