@crvouga/mockingbird-service-vpi

中文文档
3.11.0发布于 2026/11/11MITTypeScriptES Module构建来源已验证近 30 天下载 2319

Stateful emulator of the VPI compounding pharmacy API: JWT authentication, products, clinic, patients, providers, saveNewPrescription drafts and the three paged prescription status lists.

原始使用文档
文档目录9 节
  1. Install
  2. Usage
  3. Seed data
  4. Routes
  5. Lifecycle and status lists
  6. Admin (beyond the standard contract)
  7. Namespaces
  8. Deliberately not modelled
  9. API

@crvouga/mockingbird-service-vpi

Local emulators. Real API contracts. Part of Mockingbird.

[!WARNING] Our app's VPI_API_URL defaults to PRODUCTION (https://api.vpicompounding.net, see apps/backend/src/modules/erx/clients/vpi-api.client.ts). An unset variable sends real prescriptions to the real pharmacy. The stack must set VPI_API_URL to this emulator (e.g. http://127.0.0.1:8802) whenever the VPI rail is reachable.

Stateful emulator of the VPI compounding-pharmacy clinic API that our backend drives as a draft-only eRx rail: JWT authentication, the product taxonomy/details/discounts, day supply, shipping states and rates, the clinic location, providers, the patient roster and details, the provider-signature duplicate check, saveNewPrescription, and the three paged prescription status lists our poller reads. Prescriptions move only when a test says so (an admin transition). VPI sends no webhooks: our backend polls page 1 (limit 5) of each list.

  • Operation coverage: SUPPORT.md
  • The vendor publishes no spec: the contract (openapi.yaml) is hand-derived from our client's zod schemas (vpi-api.contracts.ts plus the client-local schemas in vpi-api.client.ts). The acceptance tests parse every emulator response with a verbatim port of those schemas.

Install

npm install -D @crvouga/mockingbird-service-vpi

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

Usage

Point the app at the emulator and give it any credentials (any pair logs in unless accounts is set):

npx mockingbird-vpi serve --port 8802
# app env:
#   VPI_API_URL=http://127.0.0.1:8802           (REQUIRED: the default is production)
#   [email protected]  VPI_API_PASSWORD=anything
#   VPI_CLINIC_LOCATION_ID=65a1c0de00000000000000d1
import { createServer } from "@crvouga/mockingbird-service-vpi/server"

// Test setup: a local VPI.
const server = await createServer()
const baseUrl = server.url // the app's VPI_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 VPI.
const { jwtToken } = (await (
  await post("/accounts/authenticate", { email: "[email protected]", password: "x" })
).json()) as { jwtToken: string }
// …then POST /clinic/rxOrdering/saveNewPrescription (Bearer jwtToken) saves a draft…

// Test control: move it the way the pharmacy would; our poller maps each status.
await post("/__admin/prescriptions/66b200000000000000000001/transition", {
  to: "Order Completed",
  trackingNumber: "1Z999",
})

await server.close()

Seed data

Every namespace starts with: user 65a1c0de00000000000000a1 (every login resolves to it unless accounts is set), clinic 65a1c0de00000000000000c1, clinic location 65a1c0de00000000000000d1 ("Acme Main"), providers Grace Hopper (NPI 1234567893, id 65a1c0de00000000000000e1) and Alan Turing (NPI 1987654320), patient Ada Lovelace (65a1c0de00000000000000f1, DOB 1985-02-14, 1 Main St, Phoenix AZ 85004), and four products: Testosterone Cypionate (64f1c2a9e4b0a1b2c3d4e5f6 / 2185_INJ, sterile, 10% clinic discount), Semaglutide / B6 Troche (3097_POW, cold-shipped), Enclomiphene (4410_CAP, no compounding reason needed) and Nandrolone (5120_INJ, controlled: refused by saveNewPrescription). Replace any of it with createRuntime({ data: { products, providers, clinicLocations, patients } }).

Routes

RouteBehaviour
POST /accounts/authenticate{email, password, isPatientLogin: false} → {id, jwtToken, refreshToken}. The JWT payload carries sub (user id), email, iat and exp = emulator-clock now + tokenTtlSeconds (default 3600). Our client caches it until exp − 30 s and re-authenticates once on a 401. isPatientLogin: true is refused.
every other routeRequires Authorization: Bearer <jwtToken>: missing or tampered → 401 Unauthorized; expired on the emulator clock → 401 jwt expired.
GET /products/getAllFamiliesAndCategories[{family, categories: [subCategory1…]}].
POST /products/getProductsByCategory{category, subCategory1} → [{subCategory2_item, commonNames: [{commonName, products: [...]}]}].
GET /products/getProductDetailsByProductId/{id}By Mongo id: full details incl. sigOptions, reasonForCompoundedMedication, patientPayAmount, ndc (a number). 404 if unknown.
POST /products/getProductDiscountByProductIds{clinicId, productIds} → [{id, productId, discountedPrice, unitPrice, discountedPercentage, controlledSubstance}] for known products.
POST /products/calculateDaySupply{productId, quantity, sig} → {daySupply, daySupplyReason} (per-each products: 1 per day; otherwise 30).
GET /admin/rxOrdering/getShippingStates{data: [{states: [{name: "Arizona", booleanCheck, nonSterile, sterile}]}]} (full names; no sterile shipping to Alabama or DC).
POST /portal/getShippingRate{clinicId, clinicLocationId, patientId, productIds, shippingState: "AZ", isRushOrder} → {shippingMethod, rushOrderCost, rushOrderMethod, isSignatureRequired}; 400 for a state VPI does not ship (sterile) to.
POST /clinic/rxOrdering/checkProviderSignatureNeededDuplicateisDuplicate is true when an active (not archived) prescription exists for the same patient and product; isProviderSignatureNeeded from settings (default true).
POST /clinic/rxOrdering/saveNewPrescriptionValidates the whole payload against the contract (the shipping state must be the canonical full name, controlledSubstance must be "0", …) → 400 {message, errors: [{path, message}]}; unknown clinic/location/provider/patient/product → 404; a controlled or code-mismatched product → 400. Success: a draft Provider Signature Needed in the incomplete list → {message, prescriptionId, isRefillRequest: false, refillFromPrescriptionId: null}.
POST /patients/getPatientByPatientId, …/getPatientAddressesByPatientId{patientId, userId} → the patient (dateOfBirth, phoneNumber, cellPhone), or {addresses: [...]}.
POST /patients/getPatientsInClinic{clinicId, userId, limit, currentPage} → {pagination: {hasNextPage, currentPage, limit, totalCount}, patients}.
POST /staffs/getAllProvidersByClinicLocationId{clinicLocationId, clinicId} → providers with npi.
POST /clinicLocations/getClinicLocationByClinicLocationId{clinicLocationId} → {id, clinicId, locationName, …}.
POST /clinic/rxOrdering/getIncompleteSavedPrescriptionsInClinicLocation, …/getSubmittedPrescriptionsInClinicLocation, …/getArchivedPrescriptionsInClinic{clinicLocationId, userId, limit, currentPage} → one page, newest first, of {prescriptionId, prescriptionStatus, trackingNumber, patientId, createdAt}. Envelope per statusEnvelope (default vendor: submitted {message: {prescriptions}}, archived {message: [...]} with rows keyed id, incomplete a bare array).

Lifecycle and status lists

Transition toListOur consumer's mapping
Provider Signature Needed (draft), Signature Needed, New Formula Pendingincompleteprocessing
Received, Order Receivedsubmittedsubmitted
In Process, Order In Process, Prescriptions In Process, On Hold, Order On Holdsubmittedprocessing
Completed, Order Complete, Order Completed (adds a 1Z… tracking number when none is given)submittedshipped
Cancelled, Order Cancelledarchivedcancelled
Archived, or any other string (used verbatim)archived / submittedunmapped (no refresh)

Our consumer has no delivered status for VPI and the draft rail starts at processing, so a payment on this rail moves processing → shipped (or cancelled).

Admin (beyond the standard contract)

RouteEffect
POST /__admin/prescriptions/:id/transition{to, trackingNumber?, list?: "incomplete" | "submitted" | "archived"}.
GET /__admin/prescriptionsThe namespace's prescriptions (ids and status only).
POST /__admin/patientsSeed a clinic patient {firstName, lastName, dateOfBirth, email?, phoneNumber?, cellPhone?, id?, clinicId?, addresses?: [{addressLine1, addressLine2?, city, state, zipcode}]} (VPI patient creation is not part of our client). GET lists them.
GET /__admin/catalogProducts, providers and clinic locations.
PUT /__admin/settings{tokenTtlSeconds?, accounts?: [{email, password, id}], statusEnvelope?, isProviderSignatureNeeded?}.

Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}; GET /__admin/faults/presets): token_expired (401 on authorized calls; with count: 1 our client re-authenticates exactly once and the retry succeeds), unauthorized_twice (the retry fails too), auth_rejected, server_error, duplicate_prescription, save_ambiguous_409 (saves the draft, then 409: our classifier says needs_review), save_rate_limited (429, not saved: needs_review), save_400 (retry via the browser agent), response_drift (taxonomy and product-details fields change type: our zod parse fails closed).

Namespaces

x-mockingbird-namespace, a /__admin/ns/<name> prefix on VPI_API_URL, or by login email: PUT /__admin/credentials {"credentials": {"<VPI_API_EMAIL>": "<namespace>"}} (the JWT carries the email). Authentication itself lands in the default namespace (or the /__admin/ns/ one); tokens verify in every namespace.

Deliberately not modelled

  • Webhooks: VPI has none; our backend polls.
  • Patient creation, clinic default-card billing (encryptedBillingInfo) and provider signing: our client has not captured those contracts; seed patients through the admin API.
  • The refresh-token exchange (our client re-authenticates with email/password instead).
  • Real pricing, tax and shipping-rate tables: rates are fixed per cold/sterile/rush.
  • The live catalog: the seed is synthesised in our client's field names (no sandbox recording).

API

ExportKindDescription
VpiAPIclassThe in-process emulator: fetch(request), reset(), transition(id, {to, trackingNumber?, list?}), addPatient(input), prescriptions(). Options: sqlite, now, namespace, seed, settings.
createRuntimefunctionThe emulator with the full service contract (health, admin, namespaces, credentials, presets, journal). Options: data, settings, clock, seed, adminKey, onLog, sqlite.
VPI_PRESETSobjectEvery named fault preset.
VPI_NAMESPACEstringThe service name, "vpi".
tokenCredentialfunctionThe login email a bearer JWT carries (how credentials map to namespaces).
issueJwtfunctionMint a JWT the emulator accepts, from {sub, email, iat, exp}.
DEFAULT_USER_ID, DEFAULT_CLINIC_ID, DEFAULT_CLINIC_LOCATION_ID, DEFAULT_PROVIDER_ID, DEFAULT_PATIENT_IDstringsThe seeded ids.
DEFAULT_PRODUCTS, DEFAULT_PROVIDERS, DEFAULT_PATIENTS, DEFAULT_CLINIC_LOCATION, SHIPPING_STATESvaluesThe seed data.
document, operationIds, supportedOperationIdsvaluesThe vendored OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT (./server)NodeServe over node:http; the serve CLI target (--token-ttl); port 8802.

Part of mockingbird.

版本与发布记录

@crvouga/mockingbird-service-vpi 共发布 38 个版本,首次发布于 2026/09/23;过去 12 个月发布了 38 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
3.11.0latest2026/11/11今天
3.10.52026/11/038 天前
3.10.42026/11/038 天前
3.10.32026/11/038 天前
3.10.22026/11/038 天前
3.10.12026/11/038 天前

关于 @crvouga/mockingbird-service-vpi 的常见问题

@crvouga/mockingbird-service-vpi 是什么?

Stateful emulator of the VPI compounding pharmacy API: JWT authentication, products, clinic, patients, providers, saveNewPrescription drafts and the three paged prescription status lists.

如何安装 @crvouga/mockingbird-service-vpi?

在项目目录运行 npm install @crvouga/mockingbird-service-vpi,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 mockingbird-vpi,可以用 npx mockingbird-vpi 直接运行。

@crvouga/mockingbird-service-vpi 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@crvouga/mockingbird-service-vpi 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@crvouga/mockingbird-service-vpi 可以商用吗?

@crvouga/mockingbird-service-vpi 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@crvouga/mockingbird-service-vpi 的下载量有多少?

根据 npm 官方下载统计,@crvouga/mockingbird-service-vpi 在 2026/10/11 – 2026/11/09 这 30 天内被下载 2,319 次,日均约 77 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@crvouga/mockingbird-service-vpi 还在维护吗?

最新版本 3.11.0 发布于 2026/11/11(今天);至今共发布 38 个版本,其中过去 12 个月发布了 38 个;首次发布于 2026/09/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。