Wholescripts

@crvouga/mockingbird-service-wholescripts

Stateful emulator of the Wholescripts supplement fulfilment API: product and private-label catalogs, order submit, status polling and cancel, with admin status transitions.

Parity: Catalogs, orders, and cancelPharmacy5 of 5 operationsIn-process fetchNode servermockingbird-wholescripts
$
npm install -D @crvouga/mockingbird-service-wholescripts

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-wholescripts into this tab on first send
https://api.wholescripts.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 consumers read and write (backend and EMR wholescripts.types.ts zod schemas, the supplement scheduler's Python client, adapter and mappers).

MethodPathOperationStatus
GET/api/Orders/PrivateLabelProductListGetPrivateLabelProductList
GET/api/Orders/ProductListGetProductList
POST/api/Orders/SubmitSubmitOrder
GET/api/Orders/StatusGetOrderStatus
POST/api/Orders/CancelCancelOrder

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 Wholescripts supplement fulfilment API for test suites: the product catalog, the private-label (MedPax) catalog, order submit, status polling and cancel. Orders move only when a test says so (an admin transition or an auto-advance path on the emulator clock). Wholescripts sends no webhooks, so the app sees each change on its next status poll.

  • Operation coverage: SUPPORT.md
  • The vendor publishes no spec. The contract (openapi.yaml) is hand-authored from our consumers' zod schemas (backend and EMR wholescripts.types.ts) and the supplement scheduler's Python client.

Install

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

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

Usage

Point WHOLESCRIPTS_API_URL at the emulator. It is read by the backend (global-services/services/wholescripts), the EMR (services/wholescripts) and the supplement scheduler (supplement_management). Any non-empty WHOLESCRIPTS_USERNAME / WHOLESCRIPTS_PASSWORD pair is accepted unless you pin one with --username/--password.

Shell
npx mockingbird-wholescripts serve --port 8803 --auto-advance "2000:Processing,Complete"
TypeScript
import { createServer } from "@crvouga/mockingbird-service-wholescripts/server"

// Test setup: a local Wholescripts.
const server = await createServer()
const baseUrl = server.url // the app's WHOLESCRIPTS_API_URL

// Application code: the order it submits to Wholescripts.
const headers = { authorization: `Basic ${btoa("acme:secret")}`, "content-type": "application/json" }
const response = await fetch(`${baseUrl}/api/Orders/Submit`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    ShippingAddress: { FName: "Ada", LName: "L", Address1: "1 Main St", City: "Phoenix", State: "AZ", Zip: "85004" },
    Items: [{ Sku: "SKU001", Quantity: 1 }],
    ShippingMethod: "Ground",
  }),
})
const submitted = (await response.json()) as { orderNumber: string }

// Test control: ship it the way the vendor would.
await fetch(`${baseUrl}/__admin/orders/${submitted.orderNumber}/transition`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ to: "Complete", trackingNumber: "1Z999", carrier: "UPS" }),
})

// Application code: its next status poll sees the shipment.
await fetch(`${baseUrl}/api/Orders/Status?ordernum=${submitted.orderNumber}`, { headers })

await server.close()

Routes

Route Behaviour
GET /api/Orders/PrivateLabelProductList {privateLabelProducts: [], medPaxPills: [{sku, genericName, privateLabelName, quantity}], privateLabelCartons: [{sku, name, cartonImage, quantity}]}.
GET /api/Orders/ProductList Product rows (productName, sku, medPaxSku, categories, prices, quantity, defaultDosing, optional medPaxDetails). instockonly=true keeps quantity > 0 (the EMR), search matches name/SKU/brand/categories (the supplement scheduler), limit caps.
POST /api/Orders/Submit {ShippingAddress, Notes?, Items: [{Sku, Quantity, MedPaxName?, MedPaxPills?: [{Sku, Quantity, ItemTime}]}], ShippingMethod} → {orderNumber, success: true, msg}. A contract violation or an unknown SKU (product, MedPax, pill or carton SKU) is still 200 with success: false and the reason in msg, which is what both consumers read.
GET /api/Orders/Status?ordernum= [{orderNumber, orderDate, salesOrder, status, tracking: [{trackingNumber, carrier, trackingUrl}], message, subTotal, shipMethod, shipCharge, discount, tax, serviceFee, orderTotal}]; [] for an unknown order. Totals are priced from the catalog (MedPax SKUs at their medPaxDetails price) plus 9.95 shipping (0 for "free" methods).
POST /api/Orders/Cancel {OrderNumber} → {success, msg}. Succeeds while Pending/Processing with no tracking; otherwise success: false. Unknown order: 404.

Missing or wrong Basic credentials answer 401 {"Message": "Authorization has been denied for this request."}.

Statuses are Pending (new), Processing, Complete, Cancelled, Error, or any string a test sets. How our consumers read them: the scheduler maps Pending/Processing to placed (or shipped once tracking exists), Complete to shipped (carrier hand-off, not delivery), anything containing cancel/error to cancelled/failed, and everything else to unknown. The backend and EMR do not map statuses.

Admin (beyond the standard contract)

Route Effect
POST /__admin/orders/:orderNumber/transition {to, trackingNumber?, carrier?, trackingUrl?, message?}. A tracking number is appended to tracking; Complete without one generates a UPS number. Processing/Complete set salesOrder.
GET /__admin/orders The namespace's orders (SKUs and quantities only).
GET/PUT /__admin/catalog Read or replace {products, medPaxPills, privateLabelCartons} for the namespace.
GET/PUT /__admin/settings {accounts?: [{username, password}], autoAdvance?: {afterMs, path} | null}.
POST /__admin/tick Apply every auto-advance step that is due on the emulator clock (the served emulator ticks every 100 ms).

Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}; GET /__admin/faults/presets): submit_rejected (200 success: false), submit_timeout (the order is placed, then the connection drops: the scheduler's "order may have been placed" branch), status_empty ([]), status_schema_drift (rows the backend's zod rejects, so it returns null), server_error (500, which the scheduler retries), unauthorized (401).

Namespaces

x-mockingbird-namespace, a /__admin/ns/<name> prefix on WHOLESCRIPTS_API_URL, or by Basic username: PUT /__admin/credentials {"credentials": {"<WHOLESCRIPTS_USERNAME>": "<namespace>"}}.

Corpus and seed data

The default catalog is the rows our consumers' own tests use (the consumer app's wholescripts-service.test.ts), with their repeated medPaxSkus made unique, plus the scheduler's MedPax box 000000000200095263 and medPaxDetails rows the scheduler's catalog sync reads. Protein Powder (PP001) is out of stock. No sandbox recording exists; load recorded rows with serve --catalog <file>, the catalog option, or PUT /__admin/catalog.

serve --catalog rows.json lays a JSON file onto the default catalog for every namespace, and a reset returns to it (unlike PUT /__admin/catalog, which replaces the catalog until the next reset). The file is {products?, medPaxPills?, privateLabelCartons?}, or a bare array of products. A row needs its sku (and a product its productName); every other string defaults to "" and every number to 0, so give quantity for a product that should survive instockonly=true. By default (--catalog-mode merge) a row whose SKU the default already has replaces it in place, any other is appended, and a list the file leaves out is kept. --catalog-mode replace makes the file the whole catalog (a list it leaves out is empty). In a fleet, set options: {"catalog": "rows.json", "catalog-mode": "replace"}. The catalog option of createRuntime still takes a whole Catalog.

Deliberately not modelled

  • Webhooks: Wholescripts has none; status is polled.
  • Real fulfilment timing, partial shipments and returns: nothing moves unless a test or autoAdvance moves it.
  • Shipping address and recipient are validated but never stored or echoed back (PHI).
  • Real tax, discounts, service fees and carrier rates: tax/discount/fee are 0, shipping is flat.
  • Payment authorization: a failed payment is only reachable through submit_rejected or an Error transition.

API

Export Kind Description
WholescriptsAPI class The in-process emulator: fetch(request), reset(), transition(orderNumber, {to, …}), tick(), orders(). Options: sqlite, now, namespace, catalog, settings.
createRuntime function The emulator with the full service contract (health, admin, namespaces, credentials, presets). Options: catalog, settings, tickMs, clock, seed, adminKey, onLog.
WHOLESCRIPTS_PRESETS object Every named fault preset.
WHOLESCRIPTS_NAMESPACE string The service name, "wholescripts".
basicUsername function The Basic username a request carries (how credentials map to namespaces).
canonicalStatus function The canonical casing of a transition target (complete → Complete).
DEFAULT_CATALOG object The default {products, medPaxPills, privateLabelCartons}.
parseCatalogDocument, mergeCatalog functions Check a --catalog document (filling omitted fields) and lay it onto a catalog by SKU.
MEDPAX_BOX_SKU string The supplement scheduler's MedPax box SKU, 000000000200095263.
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 8803.

Part of mockingbird.

↑↓ to navigate↵ to open