Pharmetika

@crvouga/mockingbird-service-pharmetika

Stateful mock 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.

Work in progress. Usable, but incomplete: operations, response shapes and options can still change between releases. Pin an exact version. Check the README's "Deliberately not modelled" notes before relying on it.
Work in progressPharmacy Runs in the browser9 of 9 operationsIn-process fetchNode servermockingbird-pharmetika
$npm install -D @crvouga/mockingbird-service-pharmetika

Playground

Requests go to the real mock 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 mock's response.

Operations

9 of 9 operations in the vendored contract are mocked. 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 mock accepts all thre…

Documentation

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

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 success flag. Only true or 1 is success: the mock 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 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.

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 { 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 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 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.

to navigate to open