RxVortex

@crvouga/mockingbird-service-rxvortex

Stateful emulator of the RxVortex (Strive) pharmacy API: OAuth token, order submit, status, cancel, recovery by sender order id, preset catalog, and signed status webhooks.

Parity: Orders and status webhooksPharmacy5 of 5 operationsIn-process fetchNode servermockingbird-rxvortex
$
npm install -D @crvouga/mockingbird-service-rxvortex

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-rxvortex into this tab on first send
https://sandbox.rxvortex.com

Send a request to see the emulator's response.

Operations

5 of 5 operations in the vendored contract are emulated. Hand-authored from the wire shapes our consumer reads and writes (rxvortex-auth.service.ts, rxvortex-fulfillment.adapter.ts, rxvortex-live.client.ts).

MethodPathOperationStatus
POST/api/v1/generate-access-tokenGenerateAccessToken
POST/api/v1/ordersCreateOrder
GET/api/v1/orders/{orderId}GetOrder
DELETE/api/v1/orders/{orderId}CancelOrder
GET/api/v1/preset-catalog-itemsListPresetCatalogItems

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 RxVortex (Strive) compounding-pharmacy API for test suites: the client-credentials token, order submit, status, cancel, the recovery lookup by sender order id, the preset catalog, and the signed 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), so an eRx suite that waited up to 60 s on the real sandbox resolves in milliseconds.

  • 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, and every field fallback it relies on is served.

Install

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

ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with npx mockingbird-rxvortex serve, createServer from ./server (Node), or createRuntime with any Fetch server.

Usage

Point RXVORTEX_API_URL at the emulator. Set RXVORTEX_WEBHOOK_SECRET in the app and pass the same value as --webhook-secret.

Shell
npx mockingbird-rxvortex serve --port 8791 \
  --webhook-url http://127.0.0.1:3000/prescriptions/webhooks/rxvortex \
  --webhook-secret "$RXVORTEX_WEBHOOK_SECRET" \
  --auto-advance "2000:Fill,Shipping,Delivered" \
  --catalog ./preset-catalog.json

--catalog loads your own preset catalog into every namespace: a JSON array of rows, or the {data: [...]} body GET /api/v1/preset-catalog-items answers with, so a recorded sandbox response loads as-is. Only catalog_id and medication_name are required. POST /__admin/reset returns to these rows. Pass --unknown-presets accept to take any well-formed preset id instead.

TypeScript
import { createServer } from "@crvouga/mockingbird-service-rxvortex/server"

// Test setup: a local RxVortex that posts its status webhooks to the app.
const server = await createServer({
  webhooks: { url: "http://127.0.0.1:3000/prescriptions/webhooks/rxvortex", secret: "whsec-test" },
})
const baseUrl = server.url // the app's RXVORTEX_API_URL
const post = (path: string, body: unknown, headers: Record<string, string> = {}) =>
  fetch(`${baseUrl}${path}`, {
    method: "POST",
    headers: { "content-type": "application/json", ...headers },
    body: JSON.stringify(body),
  })

// Application code: the requests it sends to RxVortex.
const { access_token } = (await (
  await post("/api/v1/generate-access-token", { client_id: "acme", client_secret: "s" })
).json()) as { access_token: string }
// …then POST /api/v1/orders with Authorization: Bearer <access_token> and sender_order_id "pay_123"…

// Test control: move that order the way the pharmacy would; each step emits the signed status webhook.
await post("/__admin/orders/pay_123/transition", { to: "Shipping", trackingnumber: "1Z999" })
await post("/__admin/orders/pay_123/transition", { to: "Delivered" })

await server.close()

Routes

Route Behaviour
POST /api/v1/generate-access-token JSON {client_id, client_secret} → {access_token, token_type: "Bearer", expires_in: 86400}. Any pair works unless clients is set (PUT /__admin/settings). Tokens stay valid 24 h on the emulator clock; our client caches for 24 h and never refreshes on 401.
POST /api/v1/orders Validates the submit payload against the contract; a violation is 422 {message, errors: {"patient.phone": ["…"]}}. An inactive or unknown preset_catalog_id is 422 (under unknownPresets: "accept" an unknown UUID becomes an active catalog row named after the request's medication_name). A repeated order.sender_order_id is 409. Success: {success: true, order_tracking_id: "RXV-…", sender_order_id, status: "Created"} (the tracking id is always a string).
GET /api/v1/orders/{id} id is the tracking id or the sender order id (our payment id, the recovery lookup). Returns rxstatus, orderstatus, shipping_status, delivered_date, trackingnumber, shippingservice, shippingcarrier, shipmenttrackingurl, cancellable, and the id under order_tracking_id, tracking_id and orderReferenceID.
DELETE /api/v1/orders/{id} Cancels while cancellable (until shipped), emitting the webhook; otherwise 409.
GET /api/v1/preset-catalog-items {data: [...]} rows with catalog_id, medication_name, medication_strength, package_size, quantity, quantity_units, medication_form, route, states, status. Includes the custom-cream anchor preset e404ad76-0f82-4b04-8f25-841650e2e819 and one inactive row.

Webhooks

Every status change posts {event: "order.status_updated", orderReferenceID, order_tracking_id, tracking_id, sender_order_id, rxstatus, orderstatus, shipping_status, delivered_date, trackingnumber, shippingcarrier, shippingservice, shipmenttrackingurl, updated_at} with header x-rxvortex-webhook-secret: <secret> (plain equality, as our receiver checks). 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 now, and PUT /__admin/webhook-endpoints sets per-namespace receivers.

Admin (beyond the standard contract)

Route Effect
POST /__admin/orders/:id/transition {to, trackingnumber?, shippingcarrier?, shippingservice?, delivered_date?}. to is a vendor status: Fill, PV1 Complete, Compound, Out of Stock, On Hold, Shipping, Delivered, Cancelled, Rejected, Error, or any string (used verbatim). The three status fields move together; shipping generates tracking when none is given.
PUT /__admin/catalog {items: [{catalog_id, medication_name, medication_strength?, package_size?, quantity?, quantity_units?, medication_form?, route?, states?, status?}], mode?: "replace" | "merge"} for the calling namespace (replace is the default; merge upserts by catalog_id). Answers {count}, the namespace's row count. items may also be sent as data.
GET /__admin/catalog The namespace's rows, as {data: [...]}.
PUT /__admin/settings {tokenTtlSeconds?, staticTokens?, clients?, autoAdvance?: {afterMs, path} | null, unknownPresets?: "reject" | "accept"} for the calling namespace. unknownPresets defaults to reject, as the sandbox does. staticTokens admits RXVORTEX_API_TOKEN (the catalog client's static bearer).
POST /__admin/tick Apply every auto-advance step that is due on the emulator clock (the served emulator also ticks every 100 ms).
GET /__admin/orders The namespace's orders.

Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}; GET /__admin/faults/presets): duplicate_sender_order_id, created_but_500 (creates, then 500; recovery succeeds), numeric_tracking_id, token_expired, stale_error_with_delivered_date, validation_errors_array, validation_errors_object, validation_errors_empty, server_error, webhook_duplicate, webhook_reorder, webhook_drop.

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 RXVORTEX_API_URL, or by client id: PUT /__admin/credentials {"credentials": {"<RXVORTEX_CLIENT_ID>": "<namespace>"}} (tokens carry the client id they were issued to).

Deliberately not modelled

  • Real fulfilment timing: nothing moves on its own unless autoAdvance is set.
  • Patient and prescriber details are validated, never stored or echoed back.
  • The live catalog: the default rows are synthesised in the live client's field names (no sandbox recording exists). Load recorded rows with serve --catalog, PUT /__admin/catalog, or the catalog option.
  • Refills and multi-order shipments.

API

Export Kind Description
RxVortexAPI class The in-process emulator: fetch(request), reset(), transition(id, {to, …}), tick(), orders(). Options: sqlite, now, namespace, catalog, settings, onWebhook.
createRuntime function The emulator with the full service contract (health, admin, namespaces, credentials, presets, webhooks). Options: webhooks: {url, secret, retryDelaysMs?, fetch?}, settings, catalog, tickMs, clock, seed, adminKey, onLog.
RXVORTEX_PRESETS object Every named fault preset.
RXVORTEX_NAMESPACE string The service name, "rxvortex".
tokenCredential function The client id a bearer token was issued to (how credentials map to namespaces).
CUSTOM_CREAM_ANCHOR_PRESET_ID string The sandbox custom-cream anchor preset id.
DEFAULT_CATALOG array The default preset catalog rows.
parseCatalog, parseCatalogItem functions Catalog rows from a JSON array or a recorded {data: [...]} body (missing optional fields default to null, states to [], status to "active"); throw on a malformed row.
document, operationIds, supportedOperationIds values The vendored OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT, loadCatalogFile (./server) Node Serve over node:http (auto-advance ticks every 100 ms); the serve CLI target; port 8791; read a --catalog file.

Part of mockingbird.

↑↓ to navigate↵ to open