Pharmetika

@crvouga/mockingbird-service-pharmetika

Stateful emulator of the Pharmetika compounding-pharmacy provider portal: clinics, patients, medication-order validate / EPCS prepare / submit / lookup, the v7 cancel, the medication-template catalog, and status webhooks.

Parity: Medication orders and webhooksPharmacy9 of 9 operationsIn-process fetchNode servermockingbird-pharmetika
$
npm install -D @crvouga/mockingbird-service-pharmetika

Playground

Requests go to the real emulator running in this tab. State persists across requests, and the journal shows what it received. A green dot marks operations whose sample succeeds as-is; others need ids from earlier responses.

Loads the real @crvouga/mockingbird-service-pharmetika into this tab on first send
https://testreviverx.pharmetika.com

Send a request to see the emulator's response.

Operations

9 of 9 operations in the vendored contract are emulated. Hand-authored from the wire shapes our consumer reads and writes (pharmetika-fulfillment.adapter.ts, pharmetika-live.client.ts, and the adapter spec's recorded bodies).

MethodPathOperationStatus
GET/api/v5/provider_portal/clinic/clinic_listListClinics
GET/api/v5/provider_portal/provider/patient_listListPatients
POST/api/v5/provider_portal/patient/create_newCreatePatient
PUT/api/v5/provider_portal/medication_order/id/{orderId}/validateValidateMedicationOrder
GET/api/v5/provider_portal/medication_order/id/{orderId}GetMedicationOrder
PUT/api/v5/provider_portal/medication_order/id/{orderId}PrepareMedicationOrderEPCS prepare for controlled substances. The order parks as `pending_prescriber_approval` and is never submitted: the prescriber signs it …
PUT/api/v5/provider_portal/medication_order/id/{orderId}/submitSubmitMedicationOrder
PUT/api/v7/provider_portal/medication_order/entry/cancelCancelMedicationOrder
GET/api/pharmetika/provider_access/profile/medication_templatesListMedicationTemplatesOur catalog client sends `x-pmk-authentication-token`, else Basic `PHARMETIKA_USERNAME:PASSWORD`, else nothing; the emulator accepts all …

Documentation

The package README, the same file that ships in the npm tarball.Edit on GitHub

Local emulators. Real API contracts. Part of Mockingbird.

Stateful emulator 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 emulator 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 success flag. Only true or 1 is success: the emulator answers business failures (unknown patient, controlled substance on submit, a reused order id, …) as HTTP 200 with success: 0 and messages: [{message, type}], the case our adapter guards against.

Install

Shell
npm install -D @crvouga/mockingbird-service-pharmetika

ESM 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 emulator 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=Acme picks the seeded clinic.

Shell
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"
TypeScript
import { createServer } from "@crvouga/mockingbird-service-pharmetika/server"

// Test setup: a local Pharmetika that posts its status webhooks to the app.
const server = await createServer({
  webhooks: { url: "http://127.0.0.1:3000/prescriptions/webhooks/pharmetika", secret: "whsec" },
})
const baseUrl = server.url // the app's PHARMETIKA_API_URL

// Application code: the order it submits to Pharmetika.
const id = "0192f0c8-7a3e-7cc1-9f5d-3b2a1c4d5e6f"
await fetch(`${baseUrl}/api/v5/provider_portal/medication_order/id/${id}/submit`, {
  method: "PUT",
  headers: { "x-pmk-authentication-token": "pmk_test", "content-type": "application/json" },
  body: JSON.stringify({
    clinic_identifier: "clinic-acme-0001",
    medication_order_identifier: id,
    patient: { identification: { patient_id: 1 } },
    medication_requests: [
      { product_identification: { product_identifier: "PMK-ENCLO-25" }, quantity_authorized: 1, sig: "daily" },
    ],
  }),
})

// Test control: move the order the way the pharmacy would; each step posts the status webhook.
const transition = (body: unknown) =>
  fetch(`${baseUrl}/__admin/orders/${id}/transition`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  })
await transition({ to: "data_entry" })
await transition({ to: "shipped", tracking_id: "1Z999" })
await transition({ to: "completed" })

await server.close()

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 Acme and Acme 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 emulator 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 /__admin/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 autoAdvance is set.
  • EPCS signing itself (two-factor, DEA checks): an admin transition to signed stands for it.
  • Sigs, demographics on orders, documents and reason-for-compounding are validated, never stored or echoed; only the roster keeps the demographics patient_list returns.
  • The live catalog: the default templates are synthesised in the fields the live client parses (no sandbox recording exists); pass templates or PUT /__admin/templates to load recorded rows.
  • Refills, reauthorizations and multi-order shipments.

API

Export Kind Description
PharmetikaAPI class The in-process emulator: fetch(request), reset(), transition(uuid, {to, tracking_id?}), tick(), orders(), webhookBody(order). Options: sqlite, now, namespace, templates, clinics, patients, settings, onWebhook.
createRuntime function The emulator 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.

↑↓ to navigate↵ to open