Stateful mock of the Pharmetika compounding-pharmacy provider portal for test suites: clinic and patient lookup, patient create, medication-order validate / EPCS prepare / submit / lookup, the v7 cancel, the medication-template catalog, and the status webhooks the pharmacy posts back. Orders move only when a test says so (an admin transition or an auto-advance path on the mock clock).
- Operation coverage: SUPPORT.md
- The vendor publishes no spec: the contract (
openapi.yaml) is hand-authored from the wire shapes our consumer reads and writes (pharmetika-fulfillment.adapter.ts,pharmetika-live.client.ts, and the bodies recorded in the adapter's spec). - The success rule. Every body carries Pharmetika's
successflag. Onlytrueor1is success: the mock answers business failures (unknown patient, controlled substance on submit, a reused order id, …) as HTTP 200 withsuccess: 0andmessages: [{message, type}], the case our adapter guards against.
Install
npm install -D @crvouga/mockingbird-service-pharmetikaESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
npx mockingbird-pharmetika serve, createServer from ./server (Node), or createRuntime with
any Fetch server.
Usage
Point PHARMETIKA_API_URL at the mock and give the app any PHARMETIKA_API_TOKEN (or pin one
with --api-token). Set PHARMETIKA_WEBHOOK_SECRET in the app and pass the same value as
--webhook-secret. PHARMETIKA_PRACTITIONER_IDENTIFIER can be any string;
PHARMETIKA_CLINIC_NAME=Geviti picks the seeded clinic.
npx mockingbird-pharmetika serve --port 8801 \
--webhook-url http://127.0.0.1:3000/prescriptions/webhooks/pharmetika \
--webhook-secret "$PHARMETIKA_WEBHOOK_SECRET" \
--auto-advance "2000:data_entry,shipped,completed"import { createRuntime } from "@crvouga/mockingbird-service-pharmetika"
const pmk = createRuntime({
webhooks: { url: "http://127.0.0.1:3000/prescriptions/webhooks/pharmetika", secret: "whsec" },
})
const admin = (path: string, body: unknown) =>
pmk.fetch(
new Request(`http://pharmetika.test/__admin${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
)
// …the app validates and submits PUT /api/v5/provider_portal/medication_order/id/<uuid>/submit…
// Move the order the way the pharmacy would; each step posts the status webhook.
await admin("/orders/<uuid>/transition", { to: "data_entry" })
await admin("/orders/<uuid>/transition", { to: "shipped", tracking_id: "1Z999" })
await admin("/orders/<uuid>/transition", { to: "completed" })Routes
All provider-portal routes need x-pmk-authentication-token (any non-empty token unless
tokens is set); a missing or rejected token is 401 {success: 0, messages}.
| Route | Behaviour |
|---|---|
GET /api/v5/provider_portal/clinic/clinic_list |
{success: 1, data: [{identifier, data: {name}}]}. Seeded with Geviti and Geviti West. |
GET /api/v5/provider_portal/provider/patient_list |
{success: 1, data: [{patient_id, demographics: {first_name, last_name, DOB, email, phone_primary, line_1, postal_code}}]}. Starts with one sandbox patient (id 1, "Sandbox Patient"). |
POST /api/v5/provider_portal/patient/create_new |
A FHIR Patient plus clinic_identifier. Created: {success: true, patient_id, messages: [{message: "Added Patient!"}]}. Same first/last name and DOB as an existing patient: {success: 0, duplicate_entry_count, duplicate_entries: [...]} (our adapter adopts the match). |
PUT …/medication_order/id/{uuid}/validate |
Dry run. Checks the clinic, the patient id, each product_identifier against the templates, and a non-empty sig ("Please provide instructions"). Answers data: {controlled_substance_list_count, medication_list: [{…entry, medication_order_entry_identifier, controlled, control_level}]}; a scheduled product (testosterone is C-III) sets controlled. |
PUT …/medication_order/id/{uuid} |
EPCS prepare (needs prepared_by). Parks the order at pending_prescriber_approval; it is never submitted. The prescriber "signs" with an admin transition to signed. |
PUT …/medication_order/id/{uuid}/submit |
Creates the order at prescription_entered. A controlled substance is refused (success: 0). Idempotent by the UUIDv7: a resubmit of the same clinic / patient / products / quantities / sigs (our adapter regenerates date_issued and entry ids on every attempt, so those are ignored) answers as the first did; different contents under a used id is 409 {success: 0}; a concurrent submit of the same id is 409. |
GET …/medication_order/id/{uuid} |
{success: 1, data: {order_status, medication_order_identifier, electronic_prescription_order_number, data: {order_status, …, ancillary_order_data: {medication_order_status: {workflow_status, tracking_id}}}}}; unknown ids 404. |
PUT /api/v7/provider_portal/medication_order/entry/cancel |
{note, prescriber_order_number: <uuid>} → {success: 1, messages: []} until the order ships; afterwards (or unknown) success: 0. Emits the webhook. |
GET /api/pharmetika/provider_access/profile/medication_templates |
{success: 1, data: [{template_identifier, medication_display_name, description, map_dose_to_product: {dose: product_identifier}, qty_options, default_sigs, available_states, controlled}]}. Accepts the token, Basic (PHARMETIKA_USERNAME/PASSWORD, any pair unless basic is set), or no credentials (unless anonymousCatalog: false) — our client's three fallbacks. |
Webhooks
Every status change posts to --webhook-url with header x-pharmetika-webhook-secret: <secret>
(plain equality, as our receiver checks). The body shape follows the namespace's
webhookVariant, one per field fallback our receiver reads:
| Variant | Body |
|---|---|
workflow_status (default) |
{event_type, event_data: {electronic_prescription_order_number, medication_order_identifier, medication_order_status: {workflow_status, tracking_id}, tracking_id?, updated_at}} |
status |
{event_type, event_data: {medication_order_identifier, status, tracking_id?}} |
flat |
{medication_order_identifier, medication_order_workflow_status, tracking_id?} |
Both id fields carry our UUIDv7 (the receiver looks payments up by it). Non-2xx answers are
retried (immediately, 5 s, 5 min, 30 min, 2 h). GET /__admin/webhooks lists deliveries,
GET /__admin/webhooks/events the payloads, POST /__admin/webhooks/flush runs pending retries,
PUT /__admin/webhook-endpoints sets per-namespace receivers.
Status map
Transitions set workflow_status verbatim, so any string works. The ones our mapper knows:
| Workflow status | Our status |
|---|---|
completed, shipped-received, delivered |
delivered |
shipped, Completed Orders |
shipped |
cancelled |
cancelled |
data_entry*, lab_formulation, contacting_patient, compounding*, filled, dispensed, dispense_checked, dispense_verified, order_reconciliation, shipping, signed, verified, checked, ready, ready-ship, Complete Processing |
processing |
anything else (prescription_entered, pending_prescriber_approval) |
submitted |
Shipped-or-later statuses generate a tracking_id when none is given; a tracking id promotes
submitted/processing to shipped in our receiver.
Admin (beyond the standard contract)
| Route | Effect |
|---|---|
POST /__admin/orders/:uuid/transition |
{to, tracking_id?}: set the workflow status and emit the webhook. |
GET /__admin/orders |
The namespace's orders (ids and statuses only). |
GET /__admin/patients, POST /__admin/patients |
Read or seed the roster: {patient_id?, clinic_identifier?, demographics: {first_name, last_name, DOB, …}}. |
PUT /__admin/templates |
Replace the medication templates ({templates: [...]}). |
GET/PUT /__admin/settings |
{tokens?, basic?, anonymousCatalog?, webhookVariant?, autoAdvance?: {afterMs, path} | null} for the calling namespace. |
POST /__admin/tick |
Apply due auto-advance steps (the served mock ticks every 100 ms). Orders awaiting prescriber approval never auto-advance. |
Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}; GET /__admin/faults/presets):
| Preset | Effect |
|---|---|
validate_success_zero, submit_success_zero, prepare_success_zero, lookup_success_zero |
HTTP 200 with success: 0 and a message |
validate_422 |
422 {success: 0, messages: [{message: "Please provide instructions"}]} |
success_boolean |
successes answer success: true instead of 1 |
success_string |
successes answer success: "1" (our client reads it as failure) |
cancel_success_true |
cancel answers success: true (our cancel accepts only 1) |
submitted_but_500 |
submit records the order, then answers 500; the retry replays success |
patient_create_duplicate, patient_create_500 |
the create is refused as a duplicate (naming the patient), or fails |
clinic_list_keyed |
clinic_list answers data as an object keyed by identifier |
controlled_count_string, controlled_nested_requests |
validate's controlled-substance signal as a string count, or under data.medication_requests |
unauthorized, server_error |
401 on every call; 500 on every provider-portal call |
webhook_duplicate, webhook_reorder, webhook_drop |
delivery faults on the next status webhooks |
Namespaces
Our backend's fetch cannot add headers, so a namespace can be chosen three ways:
x-mockingbird-namespace, a /ns/<name> prefix on PHARMETIKA_API_URL, or by credential:
PUT /__admin/credentials {"credentials": {"<PHARMETIKA_API_TOKEN>": "<namespace>"}} (the Basic
username works the same way for the catalog client).
Deliberately not modelled
- Real fulfilment timing and the pharmacy's internal queue: nothing moves on its own unless
autoAdvanceis set. - EPCS signing itself (two-factor, DEA checks): an admin transition to
signedstands for it. - Sigs, demographics on orders, documents and reason-for-compounding are validated, never stored
or echoed; only the roster keeps the demographics
patient_listreturns. - The live catalog: the default templates are synthesised in the fields the live client parses
(no sandbox recording exists); pass
templatesorPUT /__admin/templatesto load recorded rows. - Refills, reauthorizations and multi-order shipments.
API
| Export | Kind | Description |
|---|---|---|
PharmetikaAPI |
class | The in-process mock: fetch(request), reset(), transition(uuid, {to, tracking_id?}), tick(), orders(), webhookBody(order). Options: sqlite, now, namespace, templates, clinics, patients, settings, onWebhook. |
createRuntime |
function | The mock with the full service contract (health, admin, namespaces, credentials, presets, webhooks). Options: webhooks: {url, secret, retryDelaysMs?, fetch?}, settings, templates, clinics, patients, tickMs, clock, seed, adminKey, onLog. |
PHARMETIKA_PRESETS |
object | Every named fault preset. |
PHARMETIKA_NAMESPACE |
string | The service name, "pharmetika". |
TOKEN_HEADER |
string | "x-pmk-authentication-token". |
WEBHOOK_SECRET_HEADER |
string | "x-pharmetika-webhook-secret". |
tokenCredential |
function | The credential a request carries (portal token, else Basic username), for namespace mapping. |
DEFAULT_TEMPLATES, DEFAULT_CLINICS, SANDBOX_PATIENT |
values | The seed catalog, clinics and roster patient. |
KNOWN_STATUSES, SUBMITTED_STATUS, PENDING_APPROVAL_STATUS |
values | Documented workflow statuses. |
document, operationIds, supportedOperationIds |
values | The vendored OpenAPI contract and its operation ids. |
createServer, serveTarget, DEFAULT_PORT (./server) |
Node | Serve over node:http (auto-advance ticks every 100 ms); the serve CLI target; port 8801. |
Part of mockingbird.