@crvouga/mockingbird-service-flex 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog — @crvouga/mockingbird-service-flex
2
2
 
3
+ ## 0.3.0 (2026-09-24)
4
+
5
+ ### Features
6
+
7
+ - add subscription-mode hosted checkout and promote the service to ready ([dd42cc2](https://github.com/crvouga/mockingbird/commit/dd42cc247a83588cb2b84b89eed4f870494b084f))
8
+
9
+ ## 0.2.0 (2026-09-23)
10
+
11
+ ### Features
12
+
13
+ - replace self-hosted Vault and Turborepo cache with GitHub ([963ecd8](https://github.com/crvouga/mockingbird/commit/963ecd862e50f2782acf16aaef0d25c518f87afc))
14
+
3
15
  ## 0.1.0 (2026-09-22)
4
16
 
5
17
  Initial release.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @crvouga/mockingbird-service-flex
2
2
 
3
3
  Stateful mock of the **Flex** (withflex.com) HSA/FSA payments API for test suites: products
4
- (answered from a recorded catalog corpus), checkout sessions in `payment`, `off_session` and
5
- `setup` modes, customers, setup intents, refunds, the **hosted checkout page**, and the
6
- Svix-signed webhooks Flex posts back. A UI checkout that drove the real
4
+ (answered from a recorded catalog corpus), checkout sessions in `payment` (one-time),
5
+ `subscription`, `off_session` and `setup` modes, subscriptions, customers, setup intents,
6
+ refunds, the **hosted checkout page**, and the Svix-signed webhooks Flex posts back. A UI checkout that drove the real
7
7
  `checkout.withflex.com` page and then waited on a 5-minute reconciler settles here in
8
8
  milliseconds: the page is local, and the signed webhook reaches the app as soon as the card is
9
9
  accepted.
@@ -11,7 +11,9 @@ accepted.
11
11
  - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/flex/SUPPORT.md)
12
12
  - Flex publishes no machine-readable spec: the contract (`openapi.yaml`) is hand-authored from
13
13
  the wire shapes our consumer reads and writes (`B/billing/flex/`), and every field its zod
14
- schemas require is served.
14
+ schemas require is served. Subscription mode, `price_data.recurring` and the subscription
15
+ object follow the [Flex API reference](https://docs.withflex.com/api-reference) (there is
16
+ no sandbox recording).
15
17
 
16
18
  ## Install
17
19
 
@@ -73,6 +75,16 @@ const { checkout_session } = (await (
73
75
  await post(`/__admin/sessions/${checkout_session.checkout_session_id}/complete`, {})
74
76
  ```
75
77
 
78
+ ### Browser E2E suites
79
+
80
+ Run `npx mockingbird-flex serve` on a port, point `FLEX_API_BASE_URL` at it, and the browser
81
+ lands on the mock's page instead of `checkout.withflex.com`: no network, no shared Flex
82
+ account, and the signed webhook reaches the app as soon as Pay is clicked. The page URL takes
83
+ the mock's origin, or `--public-url` when given. A suite that finds the Flex tab by host
84
+ (`/\bcheckout\.withflex\.com\b/`) keeps working with
85
+ `--public-url http://checkout.withflex.com.localhost:8792`: Chromium resolves every
86
+ `*.localhost` name to loopback. Other browsers may need a hosts entry.
87
+
76
88
  ### Routes
77
89
 
78
90
  All API bodies are wrapped: `{product: {…}}`, `{checkout_session: {…}}`, `{customer: {…}}`,
@@ -85,12 +97,13 @@ Errors are `{error: {type, message, param?}}`.
85
97
  | `POST /v1/products` | `{product: {name, description?, url?, client_reference_id?, metadata?}}`. New products are active, `hsa_fsa_eligibility: null` until classified (`PUT /__admin/products/:id`). |
86
98
  | `GET /v1/products/{id}` | `product_id, name, description, url, client_reference_id, hsa_fsa_eligibility, visit_type, active, test_mode, metadata, created_at`. |
87
99
  | `PATCH /v1/products/{id}` | `{product: {active?, name?, description?, url?, metadata?}}`; emits `product.updated`. |
88
- | `POST /v1/checkout/sessions` | `Idempotency-Key` honoured. `mode` `payment` (≥1 line item), `setup` (a customer and no line items, else 400), `off_session` (customer + a saved `payment_method`, charged before answering: the response is already `complete`, or its expanded payment intent is `requires_payment_method` / `requires_action` per `offSessionOutcome`). Unknown or inactive products, customers and payment methods are 400. `redirect_url` and `url` are the hosted page. |
100
+ | `POST /v1/checkout/sessions` | `Idempotency-Key` honoured. `mode` `payment` (≥1 line item), `subscription` (≥1 line item whose `price_data.recurring` is `{interval: day\|week\|month\|year, interval_count?}`, else 400; optional `subscription_data: {cancel_at_period_end?, metadata?}`), `setup` (a customer and no line items, else 400), `off_session` (customer + a saved `payment_method`, charged before answering: the response is already `complete`, or its expanded payment intent is `requires_payment_method` / `requires_action` per `offSessionOutcome`). Unknown or inactive products, customers and payment methods are 400. `redirect_url` and `url` are the hosted page. |
89
101
  | `GET /v1/checkout/sessions/{id}?expand_customer=true&expand_payment_intent=true` | The session; expansions return `customer` / `payment_intent` objects instead of ids. |
90
102
  | `GET /v1/checkout/sessions?client_reference_id=&limit=&starting_after=` | Newest first (our ambiguous-create recovery). |
91
103
  | `POST /v1/checkout/sessions/{id}/refund` | `Idempotency-Key` honoured. `{checkout_session: {}}` (full) or `{checkout_session: {amount}}`; 400 when unpaid or over-refunded. |
92
104
  | `POST /v1/customers` | `Idempotency-Key` honoured. `{customer: {first_name, last_name, email, phone}}` (all required). |
93
105
  | `GET /v1/setup_intents/{id}?expand=customer,payment_method` | `setup_intent_id, status, customer, payment_method`. |
106
+ | `GET /v1/subscriptions/{id}` | `{subscription: {subscription_id, status, items, customer, default_payment_method, cancel_at_period_end, current_period_start, current_period_end, canceled_at, metadata, test_mode, created_at}}`. A paid subscription-mode session starts one: `active`, `items` = its recurring line items, the period one interval of the first recurring item (month/year steps clamp to the month's last day), charged to the card just used; the session's `subscription` holds its id. |
94
107
 
95
108
  Auth: `Authorization: Bearer fsk_test_…` or `fsk_…`; a missing key, or any other format
96
109
  (`sk_test_…`, `whsec_…`), is 401 `authentication_error`. `test_mode` on created objects follows
@@ -129,6 +142,7 @@ intent plus `checkout_session_id`, refund events `checkout_session` and `payment
129
142
  | Event | When |
130
143
  | --- | --- |
131
144
  | `payment_intent.succeeded`, then `checkout.session.completed` | the page (or `…/complete`, or an off-session charge) settles a session |
145
+ | `customer.subscription.created` (the subscription), before those two | a subscription-mode session settles |
132
146
  | `checkout.session.async_payment_succeeded` | settling a session whose intent was `processing` |
133
147
  | `checkout.session.async_payment_failed` | a decline (page, admin, off-session) |
134
148
  | `checkout.session.expired` | `…/expire`, or `expires_at` passing on the mock clock (default 24 h) |
@@ -188,8 +202,12 @@ so product names are synthesised.
188
202
  split payment is reachable only as `next_action: provide_second_payment_method`.
189
203
  - The letter-of-medical-necessity questionnaire: one submit button stands in for it.
190
204
  - Test/live data separation: a live key sees the same objects (only `test_mode` differs).
191
- - Subscriptions, coupons, promotion codes (`allow_promotion_codes` is echoed only), partial
192
- captures, disputes.
205
+ - Subscription lifecycle after checkout: renewals, invoices and the `invoice.*` events,
206
+ trials, `customer.subscription.updated` / `.deleted`, and the subscription update/cancel
207
+ routes. A subscription stays `active` for its first period. The Prices API (`price` ids in
208
+ line items) is not modelled either: use inline `price_data`.
209
+ - Coupons, promotion codes (`allow_promotion_codes` is echoed only), partial captures,
210
+ disputes.
193
211
  - Flex's exact error texts and ids: shapes follow what our consumer reads; ids look like
194
212
  `fprod_01z…`, `fcs_01z…`, `fcus_…`, `fpi_…`, `fseti_…`, `fpm_…`, `fevt_…`.
195
213
 
@@ -197,7 +215,7 @@ so product names are synthesised.
197
215
 
198
216
  | Export | Kind | Description |
199
217
  | --- | --- | --- |
200
- | `FlexAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `settle(id)`, `decline(id)`, `expire(id)`, `requireAction(id, type)`, `setPaymentIntent(id, patch)`, `applyRefund(id, amount)`, `putProduct(product)`, `emitFor(type, target)`, `tick()`, `sessions()`, `present(session, view)`. Options: `sqlite`, `now`, `namespace`, `publicNamespace`, `products`, `settings`, `onEvent`. |
218
+ | `FlexAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `settle(id)`, `decline(id)`, `expire(id)`, `requireAction(id, type)`, `setPaymentIntent(id, patch)`, `applyRefund(id, amount)`, `putProduct(product)`, `emitFor(type, target)`, `tick()`, `sessions()`, `subscriptions()`, `present(session, view)`. Options: `sqlite`, `now`, `namespace`, `publicNamespace`, `products`, `settings`, `onEvent`. |
201
219
  | `createRuntime` | function | The mock with the full service contract. Options: `webhooks: {url, secret, retryDelaysMs?, fetch?}`, `products`, `settings`, `tickMs`, `clock`, `seed`, `adminKey`, `onLog`. |
202
220
  | `FLEX_PRESETS` | object | Every named fault preset. |
203
221
  | `FLEX_NAMESPACE` | string | The service name, `"flex"`. |
@@ -206,7 +224,8 @@ so product names are synthesised.
206
224
  | `isNextActionType` | function | Whether a string is a next-action type. |
207
225
  | `CARDS`, `classifyCard`, `substituteSessionId` | values | The hosted page's test cards, its card classifier, and the `{CHECKOUT_SESSION_ID}` substitution. |
208
226
  | `CORPUS_ROWS`, `corpusProduct` | values | The recorded product corpus and its row → product mapping. |
209
- | `DEFAULT_SETTINGS`, `ELIGIBILITIES`, `NEXT_ACTION_TYPES`, `PAYMENT_INTENT_STATUSES` | values | Defaults and enums. |
227
+ | `DEFAULT_SETTINGS`, `ELIGIBILITIES`, `NEXT_ACTION_TYPES`, `PAYMENT_INTENT_STATUSES`, `SUBSCRIPTION_STATUSES` | values | Defaults and enums. |
228
+ | `periodEnd` | function | `periodEnd(startMs, {interval, interval_count?})`: the end of a billing period (epoch ms). |
210
229
  | `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
211
230
  | `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http` (expiry ticks every 100 ms); the `serve` CLI target (`--webhook-url`, `--webhook-secret`, `--public-url`, `--event-naming`); port 8792. |
212
231
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createRuntime
3
- } from "./chunk-RB3RIE5G.js";
3
+ } from "./chunk-CTPIFEBJ.js";
4
4
 
5
5
  // ../../adapters/node/dist/cli.js
6
6
  import { readFile } from "node:fs/promises";
@@ -377,4 +377,4 @@ export {
377
377
  createServer2 as createServer,
378
378
  serveTarget
379
379
  };
380
- //# sourceMappingURL=chunk-Q6AKODYZ.js.map
380
+ //# sourceMappingURL=chunk-7ZR3HG2Y.js.map
@@ -1550,7 +1550,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
1550
1550
  };
1551
1551
 
1552
1552
  // ../core/dist/version.js
1553
- var PACKAGE_VERSION = true ? "0.1.0" : UNRELEASED_VERSION;
1553
+ var PACKAGE_VERSION = true ? "0.3.0" : UNRELEASED_VERSION;
1554
1554
 
1555
1555
  // ../core/dist/signing.js
1556
1556
  var encoder = new TextEncoder();
@@ -2550,9 +2550,9 @@ var bodyIssues = (context, contentType = "application/json") => {
2550
2550
  };
2551
2551
 
2552
2552
  // src/generated/openapi.ts
2553
- var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Flex HSA/FSA payments API (Mockingbird subset)","description":"Stateful mock subset of the Flex (withflex.com) payments API: products, checkout sessions\\n(payment, off-session and setup modes), customers, setup intents, refunds, and the hosted\\ncheckout page. Request and response bodies are wrapped in an envelope named after the\\nobject (\`{product: {...}}\`, \`{checkout_session: {...}}\`, \`{products: [...]}\`).\\nHand-authored from the consumer's wire shapes (Flex publishes no machine-readable spec).\\n","version":"1","x-mockingbird-upstream":{"note":"Hand-authored from the wire shapes our consumer reads and writes (flex-api.client.ts, flex-payment.types.ts, flex-product.types.ts, flex-webhook.service.ts, do-flex-hosted-checkout.ts)."}},"servers":[{"url":"https://api.withflex.com"}],"security":[{"bearerAuth":[]}],"paths":{"/v1/products":{"get":{"operationId":"ListProducts","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"starting_after","in":"query","required":false,"description":"The last product_id of the previous page.","schema":{"type":"string","x-mockingbird-resource-ref":{"type":"product","missing":"fprod_00000000000000000000000000"}}}],"responses":{"200":{"description":"A page of products, oldest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"CreateProduct","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductBody"}}}},"responses":{"200":{"description":"The created product","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"}}}},"/v1/products/{productId}":{"parameters":[{"name":"productId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"product","missing":"fprod_00000000000000000000000000"}}}],"get":{"operationId":"GetProduct","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The product","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductEnvelope"}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"UpdateProduct","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductBody"}}}},"responses":{"200":{"description":"The updated product (emits product.updated)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/v1/checkout/sessions":{"get":{"operationId":"ListCheckoutSessions","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"client_reference_id","in":"query","required":false,"schema":{"type":"string","maxLength":255}},{"name":"expand_customer","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"expand_payment_intent","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"starting_after","in":"query","required":false,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"checkout_session","missing":"fcs_00000000000000000000000000"}}}],"responses":{"200":{"description":"Sessions, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"CreateCheckoutSession","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9:_.-]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCheckoutSessionBody"}}}},"responses":{"200":{"description":"The created session. Off-session mode charges the saved payment method before answering, so the session is already complete (or its payment intent failed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/checkout/sessions/{sessionId}":{"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"checkout_session","missing":"fcs_00000000000000000000000000"}}}],"get":{"operationId":"GetCheckoutSession","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"expand_customer","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"expand_payment_intent","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"The session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionEnvelope"}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/v1/checkout/sessions/{sessionId}/refund":{"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"checkout_session","missing":"fcs_00000000000000000000000000"}}}],"post":{"operationId":"RefundCheckoutSession","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9:_.-]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundCheckoutSessionBody"}}}},"responses":{"200":{"description":"The session with amount_refunded updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"}}}},"/v1/customers":{"post":{"operationId":"CreateCustomer","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9:_.-]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerBody"}}}},"responses":{"200":{"description":"The created customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"}}}},"/v1/setup_intents/{setupIntentId}":{"parameters":[{"name":"setupIntentId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"setup_intent","missing":"fseti_00000000000000000000000000"}}}],"get":{"operationId":"GetSetupIntent","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"expand","in":"query","required":false,"description":"Comma-separated fields to expand (customer, payment_method).","schema":{"type":"string","enum":["customer","payment_method","customer,payment_method"]}}],"responses":{"200":{"description":"The setup intent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupIntentEnvelope"}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/pay/{sessionId}":{"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"HostedCheckoutPage","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":true,"reason":"A browser-facing HTML page; covered by the acceptance suite, not by JSON walks."}},"responses":{"200":{"description":"The card form (or the letter-of-medical-necessity step)","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"Unknown session","content":{"text/html":{"schema":{"type":"string"}}}},"409":{"description":"The session is no longer payable","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"operationId":"SubmitHostedCheckout","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":false,"reason":"A browser form post; covered by the acceptance suite."}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"step":{"type":"string","enum":["card","lmn"]},"cardNumber":{"type":"string"},"expiry":{"type":"string"},"cvc":{"type":"string"},"postalCode":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"phone":{"type":"string"}}}}}},"responses":{"302":{"description":"Paid; redirects to success_url (or to the next action's page)"},"400":{"description":"The card form is incomplete or the card number is invalid","content":{"text/html":{"schema":{"type":"string"}}}},"402":{"description":"The card was declined","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"Unknown session","content":{"text/html":{"schema":{"type":"string"}}}},"409":{"description":"The session is no longer payable","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/pay/{sessionId}/cancel":{"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"CancelHostedCheckout","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":true,"reason":"A browser navigation to cancel_url; covered by the acceptance suite."}},"responses":{"302":{"description":"Redirects to cancel_url; the session stays open"},"404":{"description":"Unknown session","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A secret key, \`fsk_test_\u2026\` (test mode) or \`fsk_\u2026\` (live mode)."}},"responses":{"Error":{"description":"An error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"schemas":{"ErrorBody":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["type","message"],"properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","idempotency_error","api_error","card_error"]},"code":{"type":"string"},"message":{"type":"string"},"param":{"type":"string"}}}}},"StringMap":{"type":"object","maxProperties":50,"additionalProperties":{"type":"string","maxLength":500}},"Product":{"type":"object","required":["product_id","name","active","test_mode","created_at"],"properties":{"product_id":{"type":"string","x-mockingbird-resource":{"type":"product","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"name":{"type":"string"},"description":{"type":["string","null"]},"url":{"type":["string","null"]},"client_reference_id":{"type":["string","null"]},"hsa_fsa_eligibility":{"description":"not_eligible, auto_substantiation, private_label, letter_of_medical_necessity, prescription, vision or service; null until Flex classifies a new product.","type":["string","null"]},"visit_type":{"type":["string","null"]},"active":{"type":"boolean"},"test_mode":{"type":"boolean"},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"ProductEnvelope":{"type":"object","required":["product"],"properties":{"product":{"$ref":"#/components/schemas/Product"}}},"ProductList":{"type":"object","required":["products","has_more"],"properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"has_more":{"type":"boolean"}}},"CreateProductBody":{"type":"object","required":["product"],"properties":{"product":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":250},"description":{"type":"string","maxLength":1000},"url":{"type":"string","maxLength":2048},"client_reference_id":{"type":"string","minLength":1,"maxLength":255},"metadata":{"$ref":"#/components/schemas/StringMap"}}}}},"UpdateProductBody":{"type":"object","required":["product"],"properties":{"product":{"type":"object","properties":{"active":{"type":"boolean"},"name":{"type":"string","minLength":1,"maxLength":250},"description":{"type":"string","maxLength":1000},"url":{"type":"string","maxLength":2048},"metadata":{"$ref":"#/components/schemas/StringMap"}}}}},"LineItem":{"type":"object","required":["price_data","quantity"],"properties":{"price_data":{"type":"object","required":["product","unit_amount"],"properties":{"product":{"type":"string","x-mockingbird-resource-ref":{"type":"product","missing":"fprod_00000000000000000000000000"}},"unit_amount":{"type":"integer","minimum":0,"maximum":10000000}}},"quantity":{"type":"integer","minimum":1,"maximum":100}}},"CreateCheckoutSessionBody":{"type":"object","required":["checkout_session"],"properties":{"checkout_session":{"type":"object","required":["success_url","line_items"],"properties":{"allow_promotion_codes":{"type":"boolean"},"capture_method":{"type":"string","enum":["automatic"]},"cancel_url":{"type":"string","minLength":1,"maxLength":2048},"success_url":{"type":"string","minLength":1,"maxLength":2048},"client_reference_id":{"type":"string","minLength":1,"maxLength":255},"line_items":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/LineItem"}},"metadata":{"$ref":"#/components/schemas/StringMap"},"mode":{"type":"string","enum":["payment","off_session","setup"]},"payment_method":{"type":"string","maxLength":255},"customer":{"type":"string","x-mockingbird-resource-ref":{"type":"customer","missing":"fcus_00000000000000000000000000"}},"setup_future_use":{"type":"string","enum":["off_session"]}}}}},"RefundCheckoutSessionBody":{"type":"object","required":["checkout_session"],"properties":{"checkout_session":{"type":"object","properties":{"amount":{"type":"integer","minimum":1,"maximum":10000000}}}}},"CreateCustomerBody":{"type":"object","required":["customer"],"properties":{"customer":{"type":"object","required":["first_name","last_name","email","phone"],"properties":{"first_name":{"type":"string","minLength":1,"maxLength":100},"last_name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email","maxLength":254},"phone":{"type":"string","minLength":1,"maxLength":32}}}}},"Customer":{"type":"object","required":["customer_id","test_mode","created_at"],"properties":{"customer_id":{"type":"string","x-mockingbird-resource":{"type":"customer","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"test_mode":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"CustomerEnvelope":{"type":"object","required":["customer"],"properties":{"customer":{"$ref":"#/components/schemas/Customer"}}},"PaymentMethod":{"type":"object","required":["payment_method_id","type"],"properties":{"payment_method_id":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"type":{"type":"string","enum":["card"]},"hsa_fsa":{"type":"boolean"},"customer":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}}}},"PaymentIntent":{"type":"object","required":["payment_intent_id","amount","status"],"properties":{"payment_intent_id":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"amount":{"type":"integer"},"amount_received":{"type":["integer","null"]},"customer":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}},"payment_method":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}},"status":{"type":"string","enum":["requires_payment_method","requires_action","processing","succeeded","canceled"]},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"SetupIntent":{"type":"object","required":["setup_intent_id","status"],"properties":{"setup_intent_id":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"status":{"type":"string","enum":["requires_payment_method","requires_action","processing","succeeded","canceled"]},"customer":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/Customer"},{"type":"null"}]},"payment_method":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/PaymentMethod"},{"type":"null"}]},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"SetupIntentEnvelope":{"type":"object","required":["setup_intent"],"properties":{"setup_intent":{"$ref":"#/components/schemas/SetupIntent"}}},"NextAction":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["collect_letter_of_medical_necessity","provide_second_payment_method","provide_alternative_payment_method","payment_failed"]},"collect_letter_of_medical_necessity":{"type":"object","required":["url"],"properties":{"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}},"provide_second_payment_method":{"type":"object","required":["url"],"properties":{"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}},"provide_alternative_payment_method":{"type":"object","required":["url"],"properties":{"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}},"payment_failed":{"type":"object"}}},"ResponseLineItem":{"type":"object","required":["price_data","quantity","amount_total"],"properties":{"price_data":{"type":"object","required":["product","unit_amount"],"properties":{"product":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"unit_amount":{"type":"integer"}}},"quantity":{"type":"integer"},"amount_total":{"type":"integer"}}},"CheckoutSession":{"type":"object","required":["checkout_session_id","amount_total","status","mode","redirect_url","url","test_mode","created_at"],"properties":{"checkout_session_id":{"type":"string","x-mockingbird-resource":{"type":"checkout_session","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"client_reference_id":{"type":["string","null"]},"amount_total":{"type":"integer","minimum":0},"amount_received":{"type":["integer","null"]},"amount_refunded":{"type":"integer","minimum":0},"customer":{"anyOf":[{"type":"string","x-mockingbird-volatile":{"kind":"id"}},{"$ref":"#/components/schemas/Customer"},{"type":"null"}]},"payment_intent":{"anyOf":[{"type":"string","x-mockingbird-volatile":{"kind":"id"}},{"$ref":"#/components/schemas/PaymentIntent"},{"type":"null"}]},"setup_intent":{"type":["string","null"],"x-mockingbird-resource":{"type":"setup_intent","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"mode":{"type":"string","enum":["payment","off_session","setup"]},"status":{"type":"string","enum":["open","paid","complete","canceled","expired"]},"redirect_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"success_url":{"type":"string"},"cancel_url":{"type":["string","null"]},"next_action":{"anyOf":[{"$ref":"#/components/schemas/NextAction"},{"type":"null"}]},"visit_type":{"type":["string","null"]},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/ResponseLineItem"}},"allow_promotion_codes":{"type":"boolean"},"capture_method":{"type":"string"},"setup_future_use":{"type":["string","null"]},"test_mode":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"expires_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"CheckoutSessionEnvelope":{"type":"object","required":["checkout_session"],"properties":{"checkout_session":{"$ref":"#/components/schemas/CheckoutSession"}}},"CheckoutSessionList":{"type":"object","required":["checkout_sessions","has_more"],"properties":{"checkout_sessions":{"type":"array","items":{"$ref":"#/components/schemas/CheckoutSession"}},"has_more":{"type":"boolean"}}}}}}`);
2554
- var operationIds = ["ListProducts", "CreateProduct", "GetProduct", "UpdateProduct", "ListCheckoutSessions", "CreateCheckoutSession", "GetCheckoutSession", "RefundCheckoutSession", "CreateCustomer", "GetSetupIntent", "HostedCheckoutPage", "SubmitHostedCheckout", "CancelHostedCheckout"];
2555
- var supportedOperationIds = ["ListProducts", "CreateProduct", "GetProduct", "UpdateProduct", "ListCheckoutSessions", "CreateCheckoutSession", "GetCheckoutSession", "RefundCheckoutSession", "CreateCustomer", "GetSetupIntent", "HostedCheckoutPage", "SubmitHostedCheckout", "CancelHostedCheckout"];
2553
+ var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Flex HSA/FSA payments API (Mockingbird subset)","description":"Stateful mock subset of the Flex (withflex.com) payments API: products, checkout sessions\\n(payment, subscription, off-session and setup modes), subscriptions, customers, setup\\nintents, refunds, and the hosted checkout page. Request and response bodies are wrapped in an envelope named after the\\nobject (\`{product: {...}}\`, \`{checkout_session: {...}}\`, \`{products: [...]}\`).\\nHand-authored from the consumer's wire shapes and the Flex API reference\\n(https://docs.withflex.com/api-reference); Flex publishes no machine-readable spec.\\n","version":"1","x-mockingbird-upstream":{"note":"Hand-authored from the wire shapes our consumer reads and writes (flex-api.client.ts, flex-payment.types.ts, flex-product.types.ts, flex-webhook.service.ts, do-flex-hosted-checkout.ts). Subscription mode, recurring price_data and the Subscription object follow https://docs.withflex.com/api-reference/checkout-sessions/create-checkout-session, https://docs.withflex.com/api-reference/subscriptions/get-subscription and https://docs.withflex.com/api-reference/prices/create-price."}},"servers":[{"url":"https://api.withflex.com"}],"security":[{"bearerAuth":[]}],"paths":{"/v1/products":{"get":{"operationId":"ListProducts","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"starting_after","in":"query","required":false,"description":"The last product_id of the previous page.","schema":{"type":"string","x-mockingbird-resource-ref":{"type":"product","missing":"fprod_00000000000000000000000000"}}}],"responses":{"200":{"description":"A page of products, oldest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"CreateProduct","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductBody"}}}},"responses":{"200":{"description":"The created product","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"}}}},"/v1/products/{productId}":{"parameters":[{"name":"productId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"product","missing":"fprod_00000000000000000000000000"}}}],"get":{"operationId":"GetProduct","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The product","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductEnvelope"}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"UpdateProduct","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductBody"}}}},"responses":{"200":{"description":"The updated product (emits product.updated)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/v1/checkout/sessions":{"get":{"operationId":"ListCheckoutSessions","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"client_reference_id","in":"query","required":false,"schema":{"type":"string","maxLength":255}},{"name":"expand_customer","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"expand_payment_intent","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"starting_after","in":"query","required":false,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"checkout_session","missing":"fcs_00000000000000000000000000"}}}],"responses":{"200":{"description":"Sessions, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"CreateCheckoutSession","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9:_.-]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCheckoutSessionBody"}}}},"responses":{"200":{"description":"The created session. Off-session mode charges the saved payment method before answering, so the session is already complete (or its payment intent failed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/checkout/sessions/{sessionId}":{"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"checkout_session","missing":"fcs_00000000000000000000000000"}}}],"get":{"operationId":"GetCheckoutSession","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"expand_customer","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"expand_payment_intent","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"The session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionEnvelope"}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/v1/checkout/sessions/{sessionId}/refund":{"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"checkout_session","missing":"fcs_00000000000000000000000000"}}}],"post":{"operationId":"RefundCheckoutSession","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9:_.-]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundCheckoutSessionBody"}}}},"responses":{"200":{"description":"The session with amount_refunded updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"}}}},"/v1/customers":{"post":{"operationId":"CreateCustomer","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9:_.-]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerBody"}}}},"responses":{"200":{"description":"The created customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"}}}},"/v1/setup_intents/{setupIntentId}":{"parameters":[{"name":"setupIntentId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"setup_intent","missing":"fseti_00000000000000000000000000"}}}],"get":{"operationId":"GetSetupIntent","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"expand","in":"query","required":false,"description":"Comma-separated fields to expand (customer, payment_method).","schema":{"type":"string","enum":["customer","payment_method","customer,payment_method"]}}],"responses":{"200":{"description":"The setup intent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupIntentEnvelope"}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/v1/subscriptions/{subscriptionId}":{"parameters":[{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"subscription","missing":"fsub_00000000000000000000000000"}}}],"get":{"operationId":"GetSubscription","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The subscription a completed subscription-mode checkout session created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionEnvelope"}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/pay/{sessionId}":{"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"HostedCheckoutPage","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":true,"reason":"A browser-facing HTML page; covered by the acceptance suite, not by JSON walks."}},"responses":{"200":{"description":"The card form (or the letter-of-medical-necessity step)","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"Unknown session","content":{"text/html":{"schema":{"type":"string"}}}},"409":{"description":"The session is no longer payable","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"operationId":"SubmitHostedCheckout","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":false,"reason":"A browser form post; covered by the acceptance suite."}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"step":{"type":"string","enum":["card","lmn"]},"cardNumber":{"type":"string"},"expiry":{"type":"string"},"cvc":{"type":"string"},"postalCode":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"phone":{"type":"string"}}}}}},"responses":{"302":{"description":"Paid; redirects to success_url (or to the next action's page)"},"400":{"description":"The card form is incomplete or the card number is invalid","content":{"text/html":{"schema":{"type":"string"}}}},"402":{"description":"The card was declined","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"Unknown session","content":{"text/html":{"schema":{"type":"string"}}}},"409":{"description":"The session is no longer payable","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/pay/{sessionId}/cancel":{"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"CancelHostedCheckout","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"safe":true,"reason":"A browser navigation to cancel_url; covered by the acceptance suite."}},"responses":{"302":{"description":"Redirects to cancel_url; the session stays open"},"404":{"description":"Unknown session","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A secret key, \`fsk_test_\u2026\` (test mode) or \`fsk_\u2026\` (live mode)."}},"responses":{"Error":{"description":"An error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"schemas":{"ErrorBody":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["type","message"],"properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","idempotency_error","api_error","card_error"]},"code":{"type":"string"},"message":{"type":"string"},"param":{"type":"string"}}}}},"StringMap":{"type":"object","maxProperties":50,"additionalProperties":{"type":"string","maxLength":500}},"Product":{"type":"object","required":["product_id","name","active","test_mode","created_at"],"properties":{"product_id":{"type":"string","x-mockingbird-resource":{"type":"product","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"name":{"type":"string"},"description":{"type":["string","null"]},"url":{"type":["string","null"]},"client_reference_id":{"type":["string","null"]},"hsa_fsa_eligibility":{"description":"not_eligible, auto_substantiation, private_label, letter_of_medical_necessity, prescription, vision or service; null until Flex classifies a new product.","type":["string","null"]},"visit_type":{"type":["string","null"]},"active":{"type":"boolean"},"test_mode":{"type":"boolean"},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"ProductEnvelope":{"type":"object","required":["product"],"properties":{"product":{"$ref":"#/components/schemas/Product"}}},"ProductList":{"type":"object","required":["products","has_more"],"properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"has_more":{"type":"boolean"}}},"CreateProductBody":{"type":"object","required":["product"],"properties":{"product":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":250},"description":{"type":"string","maxLength":1000},"url":{"type":"string","maxLength":2048},"client_reference_id":{"type":"string","minLength":1,"maxLength":255},"metadata":{"$ref":"#/components/schemas/StringMap"}}}}},"UpdateProductBody":{"type":"object","required":["product"],"properties":{"product":{"type":"object","properties":{"active":{"type":"boolean"},"name":{"type":"string","minLength":1,"maxLength":250},"description":{"type":"string","maxLength":1000},"url":{"type":"string","maxLength":2048},"metadata":{"$ref":"#/components/schemas/StringMap"}}}}},"LineItem":{"type":"object","required":["price_data","quantity"],"properties":{"price_data":{"type":"object","required":["product","unit_amount"],"properties":{"product":{"type":"string","x-mockingbird-resource-ref":{"type":"product","missing":"fprod_00000000000000000000000000"}},"unit_amount":{"type":"integer","minimum":0,"maximum":10000000},"recurring":{"$ref":"#/components/schemas/Recurring"}}},"quantity":{"type":"integer","minimum":1,"maximum":100}}},"Recurring":{"type":"object","description":"Makes the line item's price recurring; a subscription-mode session needs at least one.","required":["interval"],"properties":{"interval":{"type":"string","enum":["day","week","month","year"]},"interval_count":{"type":"integer","minimum":1,"maximum":12}}},"SubscriptionData":{"type":"object","properties":{"cancel_at_period_end":{"type":"boolean"},"metadata":{"$ref":"#/components/schemas/StringMap"}}},"CreateCheckoutSessionBody":{"type":"object","required":["checkout_session"],"properties":{"checkout_session":{"type":"object","required":["success_url","line_items"],"properties":{"allow_promotion_codes":{"type":"boolean"},"capture_method":{"type":"string","enum":["automatic"]},"cancel_url":{"type":"string","minLength":1,"maxLength":2048},"success_url":{"type":"string","minLength":1,"maxLength":2048},"client_reference_id":{"type":"string","minLength":1,"maxLength":255},"line_items":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/LineItem"}},"metadata":{"$ref":"#/components/schemas/StringMap"},"mode":{"type":"string","enum":["payment","subscription","off_session","setup"]},"subscription_data":{"$ref":"#/components/schemas/SubscriptionData"},"payment_method":{"type":"string","maxLength":255},"customer":{"type":"string","x-mockingbird-resource-ref":{"type":"customer","missing":"fcus_00000000000000000000000000"}},"setup_future_use":{"type":"string","enum":["off_session"]}}}}},"RefundCheckoutSessionBody":{"type":"object","required":["checkout_session"],"properties":{"checkout_session":{"type":"object","properties":{"amount":{"type":"integer","minimum":1,"maximum":10000000}}}}},"CreateCustomerBody":{"type":"object","required":["customer"],"properties":{"customer":{"type":"object","required":["first_name","last_name","email","phone"],"properties":{"first_name":{"type":"string","minLength":1,"maxLength":100},"last_name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email","maxLength":254},"phone":{"type":"string","minLength":1,"maxLength":32}}}}},"Customer":{"type":"object","required":["customer_id","test_mode","created_at"],"properties":{"customer_id":{"type":"string","x-mockingbird-resource":{"type":"customer","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"test_mode":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"CustomerEnvelope":{"type":"object","required":["customer"],"properties":{"customer":{"$ref":"#/components/schemas/Customer"}}},"PaymentMethod":{"type":"object","required":["payment_method_id","type"],"properties":{"payment_method_id":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"type":{"type":"string","enum":["card"]},"hsa_fsa":{"type":"boolean"},"customer":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}}}},"PaymentIntent":{"type":"object","required":["payment_intent_id","amount","status"],"properties":{"payment_intent_id":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"amount":{"type":"integer"},"amount_received":{"type":["integer","null"]},"customer":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}},"payment_method":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}},"status":{"type":"string","enum":["requires_payment_method","requires_action","processing","succeeded","canceled"]},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"SetupIntent":{"type":"object","required":["setup_intent_id","status"],"properties":{"setup_intent_id":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"status":{"type":"string","enum":["requires_payment_method","requires_action","processing","succeeded","canceled"]},"customer":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/Customer"},{"type":"null"}]},"payment_method":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/PaymentMethod"},{"type":"null"}]},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"SetupIntentEnvelope":{"type":"object","required":["setup_intent"],"properties":{"setup_intent":{"$ref":"#/components/schemas/SetupIntent"}}},"NextAction":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["collect_letter_of_medical_necessity","provide_second_payment_method","provide_alternative_payment_method","payment_failed"]},"collect_letter_of_medical_necessity":{"type":"object","required":["url"],"properties":{"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}},"provide_second_payment_method":{"type":"object","required":["url"],"properties":{"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}},"provide_alternative_payment_method":{"type":"object","required":["url"],"properties":{"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}},"payment_failed":{"type":"object"}}},"ResponseLineItem":{"type":"object","required":["price_data","quantity","amount_total"],"properties":{"price_data":{"type":"object","required":["product","unit_amount"],"properties":{"product":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"unit_amount":{"type":"integer"},"recurring":{"anyOf":[{"$ref":"#/components/schemas/Recurring"},{"type":"null"}]}}},"quantity":{"type":"integer"},"amount_total":{"type":"integer"}}},"CheckoutSession":{"type":"object","required":["checkout_session_id","amount_total","status","mode","redirect_url","url","test_mode","created_at"],"properties":{"checkout_session_id":{"type":"string","x-mockingbird-resource":{"type":"checkout_session","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"client_reference_id":{"type":["string","null"]},"amount_total":{"type":"integer","minimum":0},"amount_received":{"type":["integer","null"]},"amount_refunded":{"type":"integer","minimum":0},"customer":{"anyOf":[{"type":"string","x-mockingbird-volatile":{"kind":"id"}},{"$ref":"#/components/schemas/Customer"},{"type":"null"}]},"payment_intent":{"anyOf":[{"type":"string","x-mockingbird-volatile":{"kind":"id"}},{"$ref":"#/components/schemas/PaymentIntent"},{"type":"null"}]},"setup_intent":{"type":["string","null"],"x-mockingbird-resource":{"type":"setup_intent","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"mode":{"type":"string","enum":["payment","subscription","off_session","setup"]},"subscription":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}},"subscription_data":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionData"},{"type":"null"}]},"status":{"type":"string","enum":["open","paid","complete","canceled","expired"]},"redirect_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"success_url":{"type":"string"},"cancel_url":{"type":["string","null"]},"next_action":{"anyOf":[{"$ref":"#/components/schemas/NextAction"},{"type":"null"}]},"visit_type":{"type":["string","null"]},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/ResponseLineItem"}},"allow_promotion_codes":{"type":"boolean"},"capture_method":{"type":"string"},"setup_future_use":{"type":["string","null"]},"test_mode":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"expires_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"CheckoutSessionEnvelope":{"type":"object","required":["checkout_session"],"properties":{"checkout_session":{"$ref":"#/components/schemas/CheckoutSession"}}},"CheckoutSessionList":{"type":"object","required":["checkout_sessions","has_more"],"properties":{"checkout_sessions":{"type":"array","items":{"$ref":"#/components/schemas/CheckoutSession"}},"has_more":{"type":"boolean"}}},"SubscriptionItem":{"type":"object","required":["price_data","quantity"],"properties":{"price_data":{"type":"object","required":["product","unit_amount","recurring"],"properties":{"product":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"unit_amount":{"type":"integer"},"recurring":{"$ref":"#/components/schemas/Recurring"}}},"quantity":{"type":"integer"}}},"Subscription":{"type":"object","required":["subscription_id","created_at","items","status","cancel_at_period_end","test_mode"],"properties":{"subscription_id":{"type":"string","x-mockingbird-resource":{"type":"subscription","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"status":{"type":"string","enum":["incomplete","incomplete_expired","trialing","active","past_due","canceled","unpaid","paused"]},"items":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionItem"}},"customer":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}},"default_payment_method":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"id"}},"cancel_at_period_end":{"type":"boolean"},"current_period_start":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"current_period_end":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"canceled_at":{"type":["string","null"]},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"}},"test_mode":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"SubscriptionEnvelope":{"type":"object","required":["subscription"],"properties":{"subscription":{"$ref":"#/components/schemas/Subscription"}}}}}}`);
2554
+ var operationIds = ["ListProducts", "CreateProduct", "GetProduct", "UpdateProduct", "ListCheckoutSessions", "CreateCheckoutSession", "GetCheckoutSession", "RefundCheckoutSession", "CreateCustomer", "GetSetupIntent", "GetSubscription", "HostedCheckoutPage", "SubmitHostedCheckout", "CancelHostedCheckout"];
2555
+ var supportedOperationIds = ["ListProducts", "CreateProduct", "GetProduct", "UpdateProduct", "ListCheckoutSessions", "CreateCheckoutSession", "GetCheckoutSession", "RefundCheckoutSession", "CreateCustomer", "GetSetupIntent", "GetSubscription", "HostedCheckoutPage", "SubmitHostedCheckout", "CancelHostedCheckout"];
2556
2556
 
2557
2557
  // src/hosted.ts
2558
2558
  var CARDS = {
@@ -2615,12 +2615,19 @@ ${body}
2615
2615
  `;
2616
2616
  var alert = (error2) => error2 ? `<div role="alert" data-testid="flex-mock-error">${escapeHtml(error2)}</div>` : "";
2617
2617
  var cancelLink = (session) => session.cancel_url ? `<p><a data-testid="flex-mock-cancel" href="./${encodeURIComponent(session.checkout_session_id)}/cancel">Cancel and return</a></p>` : "";
2618
+ var cadence = (session) => {
2619
+ const recurring = session.line_items.find((item) => item.price_data.recurring)?.price_data.recurring;
2620
+ if (!recurring) return "";
2621
+ const count = recurring.interval_count ?? 1;
2622
+ const every = count === 1 ? recurring.interval : `${count} ${recurring.interval}s`;
2623
+ return ` <span data-testid="flex-mock-interval">per ${escapeHtml(every)}</span>`;
2624
+ };
2618
2625
  var summary = (input) => {
2619
2626
  const { session } = input;
2620
2627
  const items = input.productNames.map((name) => `<li data-testid="flex-mock-line-item">${escapeHtml(name)}</li>`).join("");
2621
- return `<h1>Pay with HSA/FSA</h1>
2622
- <p data-testid="flex-mock-session" data-session-id="${escapeHtml(session.checkout_session_id)}">
2623
- ${session.mode === "setup" ? "Save a card for future payments" : `Total <strong data-testid="flex-mock-amount">${dollars(session.amount_total)}</strong>`}
2628
+ return `<h1>${session.mode === "subscription" ? "Subscribe with HSA/FSA" : "Pay with HSA/FSA"}</h1>
2629
+ <p data-testid="flex-mock-session" data-session-id="${escapeHtml(session.checkout_session_id)}" data-mode="${escapeHtml(session.mode)}">
2630
+ ${session.mode === "setup" ? "Save a card for future payments" : `Total <strong data-testid="flex-mock-amount">${dollars(session.amount_total)}</strong>${session.mode === "subscription" ? cadence(session) : ""}`}
2624
2631
  </p>
2625
2632
  ${items ? `<ul>${items}</ul>` : ""}`;
2626
2633
  };
@@ -3374,6 +3381,16 @@ var PAYMENT_INTENT_STATUSES = [
3374
3381
  "succeeded",
3375
3382
  "canceled"
3376
3383
  ];
3384
+ var SUBSCRIPTION_STATUSES = [
3385
+ "incomplete",
3386
+ "incomplete_expired",
3387
+ "trialing",
3388
+ "active",
3389
+ "past_due",
3390
+ "canceled",
3391
+ "unpaid",
3392
+ "paused"
3393
+ ];
3377
3394
  var DEFAULT_SETTINGS = {
3378
3395
  eventNaming: "dotted",
3379
3396
  offSessionOutcome: "succeeded",
@@ -3441,6 +3458,7 @@ var FlexState = class {
3441
3458
  this.paymentIntents = new Collection(sqlite, namespace, "payment_intents");
3442
3459
  this.setupIntents = new Collection(sqlite, namespace, "setup_intents");
3443
3460
  this.refunds = new Collection(sqlite, namespace, "refunds");
3461
+ this.subscriptions = new Collection(sqlite, namespace, "subscriptions");
3444
3462
  this.settings = new Collection(sqlite, namespace, "settings");
3445
3463
  this.ids = new IdSequence(sqlite, namespace, "flex");
3446
3464
  }
@@ -3452,6 +3470,7 @@ var FlexState = class {
3452
3470
  paymentIntents;
3453
3471
  setupIntents;
3454
3472
  refunds;
3473
+ subscriptions;
3455
3474
  settings;
3456
3475
  ids;
3457
3476
  current() {
@@ -3752,6 +3771,7 @@ var FLEX_EVENT_TYPES = [
3752
3771
  "checkout.session.expired",
3753
3772
  "checkout_session.expired",
3754
3773
  "payment_intent.succeeded",
3774
+ "customer.subscription.created",
3755
3775
  "refund.created",
3756
3776
  "refund.updated",
3757
3777
  "charge.refunded",
@@ -3780,6 +3800,27 @@ var sleep = (ms, signal) => new Promise((resolve) => {
3780
3800
  resolve();
3781
3801
  });
3782
3802
  });
3803
+ var daysInMonth = (year, month) => new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
3804
+ var periodEnd = (startMs, recurring) => {
3805
+ const count = recurring.interval_count ?? 1;
3806
+ const start = new Date(startMs);
3807
+ if (recurring.interval === "day") return startMs + count * 864e5;
3808
+ if (recurring.interval === "week") return startMs + count * 7 * 864e5;
3809
+ const months = recurring.interval === "month" ? count : count * 12;
3810
+ const total = start.getUTCMonth() + months;
3811
+ const year = start.getUTCFullYear() + Math.floor(total / 12);
3812
+ const month = total % 12;
3813
+ const day = Math.min(start.getUTCDate(), daysInMonth(year, month));
3814
+ return Date.UTC(
3815
+ year,
3816
+ month,
3817
+ day,
3818
+ start.getUTCHours(),
3819
+ start.getUTCMinutes(),
3820
+ start.getUTCSeconds(),
3821
+ start.getUTCMilliseconds()
3822
+ );
3823
+ };
3783
3824
  var FlexAPI = class {
3784
3825
  app;
3785
3826
  sqlite;
@@ -3811,6 +3852,7 @@ var FlexAPI = class {
3811
3852
  RefundCheckoutSession: (context) => this.idempotent(context, () => this.refund(context)),
3812
3853
  CreateCustomer: (context) => this.idempotent(context, () => this.createCustomer(context)),
3813
3854
  GetSetupIntent: (context) => this.getSetupIntent(context),
3855
+ GetSubscription: (context) => this.getSubscription(context),
3814
3856
  HostedCheckoutPage: (context) => this.hostedPage(context),
3815
3857
  SubmitHostedCheckout: (context) => this.submitHosted(context),
3816
3858
  CancelHostedCheckout: (context) => this.cancelHosted(context)
@@ -3977,6 +4019,12 @@ var FlexAPI = class {
3977
4019
  if (mode !== "setup" && lines.length === 0) {
3978
4020
  return invalid("At least one line item is required.", "line_items");
3979
4021
  }
4022
+ if (mode === "subscription" && !lines.some((line) => line.price_data.recurring)) {
4023
+ return invalid(
4024
+ "Subscription mode requires at least one line item with a recurring price.",
4025
+ "line_items"
4026
+ );
4027
+ }
3980
4028
  if (mode === "off_session" && (!customerId || !paymentMethodId)) {
3981
4029
  return invalid("Off-session mode requires a customer and a payment method.", "mode");
3982
4030
  }
@@ -4008,7 +4056,14 @@ var FlexAPI = class {
4008
4056
  products.push(product);
4009
4057
  }
4010
4058
  const items = lines.map((line) => ({
4011
- price_data: { product: line.price_data.product, unit_amount: line.price_data.unit_amount },
4059
+ price_data: {
4060
+ product: line.price_data.product,
4061
+ unit_amount: line.price_data.unit_amount,
4062
+ recurring: line.price_data.recurring ? {
4063
+ interval: line.price_data.recurring.interval,
4064
+ interval_count: line.price_data.recurring.interval_count ?? 1
4065
+ } : null
4066
+ },
4012
4067
  quantity: line.quantity,
4013
4068
  amount_total: line.price_data.unit_amount * line.quantity
4014
4069
  }));
@@ -4061,6 +4116,8 @@ var FlexAPI = class {
4061
4116
  customer: customerId,
4062
4117
  payment_intent: null,
4063
4118
  setup_intent: setupIntent,
4119
+ subscription: null,
4120
+ subscription_data: mode === "subscription" ? isRecord5(input.subscription_data) ? input.subscription_data : {} : null,
4064
4121
  mode,
4065
4122
  status: "open",
4066
4123
  url: `${this.pageBase(context)}/pay/${encodeURIComponent(id)}`,
@@ -4249,6 +4306,49 @@ var FlexAPI = class {
4249
4306
  { ids: { setupIntentId: id } }
4250
4307
  );
4251
4308
  }
4309
+ getSubscription(context) {
4310
+ const id = context.params.subscriptionId ?? "";
4311
+ const subscription = this.state.subscriptions.get(id);
4312
+ if (!subscription) return error(404, "invalid_request_error", `No such subscription: '${id}'`);
4313
+ return annotateResponse(jsonRes(200, { subscription }), { ids: { subscriptionId: id } });
4314
+ }
4315
+ /**
4316
+ * Start the subscription a paid subscription-mode session buys: active, billed from now
4317
+ * for one period of its first recurring line item, charged to the card just used.
4318
+ */
4319
+ startSubscription(session, paymentMethod) {
4320
+ const recurring = session.line_items.flatMap(
4321
+ (item) => item.price_data.recurring ? [
4322
+ {
4323
+ price_data: { ...item.price_data, recurring: item.price_data.recurring },
4324
+ quantity: item.quantity
4325
+ }
4326
+ ] : []
4327
+ );
4328
+ const first = recurring[0]?.price_data.recurring ?? { interval: "month" };
4329
+ const data = session.subscription_data ?? {};
4330
+ const subscription = {
4331
+ subscription_id: this.state.nextId("fsub_"),
4332
+ status: "active",
4333
+ items: recurring,
4334
+ customer: session.customer,
4335
+ default_payment_method: paymentMethod,
4336
+ cancel_at_period_end: data.cancel_at_period_end === true,
4337
+ current_period_start: this.iso(),
4338
+ current_period_end: new Date(periodEnd(this.now(), first)).toISOString(),
4339
+ canceled_at: null,
4340
+ metadata: data.metadata ?? null,
4341
+ test_mode: session.test_mode,
4342
+ created_at: this.iso()
4343
+ };
4344
+ this.state.subscriptions.insert(subscription.subscription_id, subscription);
4345
+ this.emit("customer.subscription.created", { ...subscription }, session.test_mode);
4346
+ return subscription;
4347
+ }
4348
+ /** The subscriptions paid subscription-mode sessions created in this namespace. */
4349
+ subscriptions() {
4350
+ return this.state.subscriptions.list().map((row) => row.value);
4351
+ }
4252
4352
  // --- lifecycle (hosted page, admin, off-session) ----------------------------------------
4253
4353
  paymentIntentFor(session) {
4254
4354
  const existing = session.payment_intent ? this.state.paymentIntents.get(session.payment_intent) : void 0;
@@ -4328,11 +4428,13 @@ var FlexAPI = class {
4328
4428
  const before = this.paymentIntentFor(session);
4329
4429
  const wasProcessing = before.status === "processing";
4330
4430
  session = this.state.sessions.get(id);
4431
+ const subscription = session.mode === "subscription" ? this.startSubscription(session, method.payment_method_id) : void 0;
4331
4432
  this.state.sessions.update(id, {
4332
4433
  ...session,
4333
4434
  status: "complete",
4334
4435
  amount_received: session.amount_total,
4335
- next_action: null
4436
+ next_action: null,
4437
+ ...subscription ? { subscription: subscription.subscription_id } : {}
4336
4438
  });
4337
4439
  this.setPaymentIntent(id, {
4338
4440
  status: "succeeded",
@@ -4533,6 +4635,7 @@ export {
4533
4635
  ELIGIBILITIES,
4534
4636
  NEXT_ACTION_TYPES,
4535
4637
  PAYMENT_INTENT_STATUSES,
4638
+ SUBSCRIPTION_STATUSES,
4536
4639
  DEFAULT_SETTINGS,
4537
4640
  corpusProduct,
4538
4641
  FLEX_PRESETS,
@@ -4540,7 +4643,8 @@ export {
4540
4643
  FLEX_NAMESPACE,
4541
4644
  FLEX_EVENT_TYPES,
4542
4645
  keyMode,
4646
+ periodEnd,
4543
4647
  FlexAPI,
4544
4648
  isNextActionType
4545
4649
  };
4546
- //# sourceMappingURL=chunk-RB3RIE5G.js.map
4650
+ //# sourceMappingURL=chunk-CTPIFEBJ.js.map