@crvouga/mockingbird-service-stripe 1.2.0 → 1.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,6 +1,23 @@
1
1
  # Changelog — @crvouga/mockingbird-service-stripe
2
2
 
3
- ## 1.2.0 (2026-09-26)
3
+ ## 1.3.0 (2026-09-29)
4
+
5
+ ### Features
6
+
7
+ - hosted customer portal, trial_end=now and subscription lifecycle refinements ([f8d0567](https://github.com/crvouga/mockingbird/commit/f8d0567928a7d5fdc0463cc691f5350f9cb4d1c6))
8
+ - let a scalar naming a later branch's enum value pick that branch ([30ade3a](https://github.com/crvouga/mockingbird/commit/30ade3a28a5b6b273ffbf261216b821acac8e961))
9
+ - promotion codes, inline prices and trials in Checkout ([f60923c](https://github.com/crvouga/mockingbird/commit/f60923c915009ad0d264237f36338a13fb546874))
10
+ - bill prorations, pause collection and uncollectible invoices ([089fc7e](https://github.com/crvouga/mockingbird/commit/089fc7eb86700ef678b1e2a509674565b48f410f))
11
+ - vendor billing portal, subscription resume and trial settings ([09f7280](https://github.com/crvouga/mockingbird/commit/09f7280db744117fce16a6e0606b41f90d67fa54))
12
+
13
+ ### Fixes and improvements
14
+
15
+ - stop cleared webhook deliveries from rescheduling forever ([424b78f](https://github.com/crvouga/mockingbird/commit/424b78f29317b162e3bf7d0049cc19be5b977026))
16
+ - serialize immediate webhook attempts to fix reorder flake ([625c82b](https://github.com/crvouga/mockingbird/commit/625c82b5a8a7992c1cf93e9df68e77af08b353e3))
17
+ - omit null advice and network decline codes from card errors ([34f074a](https://github.com/crvouga/mockingbird/commit/34f074a143c3d4d15f6fd472c51f6f4b341d1baf))
18
+ - flush drains webhook retries scheduled while it runs ([c58cede](https://github.com/crvouga/mockingbird/commit/c58cede1b6ba7ae5ed3f8721ad9b7cd7bc0b1ca7))
19
+
20
+ ## 1.2.0 (2026-09-25)
4
21
 
5
22
  ### Features
6
23
 
package/README.md CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  Stateful, in-process mock of the [Stripe API](https://docs.stripe.com/api) for test suites: accounts
4
4
  chosen by API key, customers and balances, payment methods, payment and setup intents, charges,
5
- refunds, disputes, checkout (with a hosted page and a Stripe.js stand-in), invoices, subscriptions
6
- that renew when the clock moves, subscription schedules, coupons, promotion codes, products,
5
+ refunds, disputes, checkout (with a hosted page and a Stripe.js stand-in), the customer portal
6
+ (configurations, sessions and the hosted portal page), invoices, subscriptions that renew, pause,
7
+ resume and cancel as the clock moves, subscription schedules, coupons, promotion codes, products,
7
8
  prices, test clocks, webhook endpoints, the balance ledger and the event log — with signed webhooks
8
9
  fanned out to every matching endpoint. Responses are rendered at the caller's `Stripe-Version`
9
10
  (`2024-06-20`, `2025-02-24.acacia`, or the vendored latest), and the whole surface is verified by
10
11
  differential property tests against Stripe test mode.
11
12
 
12
- - Operation coverage (111 of 115 operations in the vendored spec, with reasons for each gap):
13
+ - Operation coverage (119 of 123 operations in the vendored spec, with reasons for each gap):
13
14
  [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/stripe/SUPPORT.md)
14
15
  - Stripe API reference: https://docs.stripe.com/api · Upstream OpenAPI: https://github.com/stripe/openapi
15
16
 
@@ -152,7 +153,19 @@ clock-driven lifecycle, so their webhooks fire at once; the served mock also tic
152
153
  finalized and charged off-session to the default payment method, then `invoice.paid` +
153
154
  `customer.subscription.updated` (`previous_attributes.current_period_end`), or
154
155
  `invoice.payment_failed` and `past_due`. Trials end into a cycle; `cancel_at_period_end` cancels
155
- (`customer.subscription.deleted`). `invoice.upcoming` fires 3 days before renewal.
156
+ (`customer.subscription.deleted`). `invoice.upcoming` fires 3 days before renewal and
157
+ `customer.subscription.trial_will_end` 3 days before a trial ends (once per trial).
158
+ - **Trials without a card**: `trial_settings[end_behavior][missing_payment_method]` decides what a
159
+ trial ending with no default payment method does — `create_invoice` (the default) invoices and
160
+ goes `past_due`, `cancel` cancels at the trial's end, `pause` sets status `paused`
161
+ (`customer.subscription.paused`) until `POST /v1/subscriptions/:id/resume`:
162
+ `billing_cycle_anchor=now` (default) starts a new period with a `subscription_update` invoice
163
+ (paid → `active`, failed → `past_due`); `unchanged` keeps the anchor and prorates the rest of the
164
+ period per `proration_behavior` (and `proration_date`). Both emit `customer.subscription.resumed`.
165
+ - **Paused collection**: `pause_collection[behavior]` keeps periods moving while renewal invoices are
166
+ voided (`void`), marked `uncollectible` (`mark_uncollectible`, `invoice.marked_uncollectible`) or
167
+ left as drafts (`keep_as_draft`), never charged; the status holds. `pause_collection[resumes_at]`
168
+ lifts the pause on its own before the renewal it precedes; `pause_collection=""` lifts it now.
156
169
  - `incomplete` subscriptions become `incomplete_expired` after 23 h (their invoice is voided).
157
170
  - Checkout Sessions expire at `expires_at` (`checkout.session.expired`).
158
171
  - Schedule phases advance; the last one releases or cancels per `end_behavior`.
@@ -167,7 +180,11 @@ now and returns `incomplete` on a decline; `error_if_incomplete` fails the call
167
180
  customer, and paying it through Stripe.js activates the subscription. `trial_end`,
168
181
  `backdate_start_date` + `billing_cycle_anchor` + `proration_behavior=none` (a $0 first invoice),
169
182
  item updates with `always_invoice` (billed now) or `create_prorations` (next invoice), and
170
- discounts with stable `di_` ids (`discounts=""` clears) are modelled. A $0 invoice is `paid` on
183
+ discounts with stable `di_` ids (`discounts=""` clears) are modelled. Items must be active recurring
184
+ prices sharing one currency and interval. `DELETE /v1/subscriptions/:id` takes
185
+ `cancellation_details[comment|feedback]`, `prorate` (a credit for the unused time as a pending
186
+ proration) and `invoice_now` (a final invoice; a net credit lands on the customer balance), in the
187
+ query string (as stripe-node sends them) or the body. A $0 invoice is `paid` on
171
188
  finalize; a customer credit balance is applied at finalize; `void` works only on open invoices
172
189
  (`You can only pass in open invoices. This invoice isn't open.`).
173
190
 
@@ -182,9 +199,27 @@ finalize; a customer credit balance is applied at finalize; `void` works only on
182
199
  succeeding brands, declines (generic, insufficient funds, expired, attach-then-fail), 3D Secure
183
200
  and dispute. Each button fills every field, and with “Pay immediately after filling” ticked it
184
201
  submits the form too. Pay completes the session (creating the customer, the PaymentIntent with
185
- `payment_intent_data.metadata`, the Subscription with `subscription_data.metadata`, or the
186
- SetupIntent), emits `checkout.session.completed` and 302s to `success_url` with
187
- `{CHECKOUT_SESSION_ID}` substituted raw and `%7B…%7D`-encoded; Cancel 302s to `cancel_url`.
202
+ `payment_intent_data.metadata`, the Subscription with `subscription_data` — metadata,
203
+ description, trial and `trial_settings` — or the SetupIntent), emits `checkout.session.completed`
204
+ and 302s to `success_url` with `{CHECKOUT_SESSION_ID}` substituted raw and `%7B…%7D`-encoded;
205
+ Cancel 302s to `cancel_url`. The page authenticates 3-D Secure cards in place in every mode, and
206
+ checks what was typed the way the Payment Element does (expiry in the past, incomplete CVC,
207
+ invalid email); a decline leaves the session open (in subscription and setup mode it creates no
208
+ customer; in payment mode it records the failed PaymentIntent and charge, as Stripe does). The email and name
209
+ typed become the new customer's and `customer_details`; `customer_email` pre-fills the email.
210
+ - Subscription mode bills one-time lines once, on the first invoice, and `price_data` lines (with
211
+ or without `recurring`) create inline prices (and `product_data` products), so every line — and
212
+ the subscription — references a real price id. With `payment_method_collection=if_required` a
213
+ free trial shows no card fields (`stripe-mock-no-card`) and starts without a payment method.
214
+ - `allow_promotion_codes=true` adds a promotion code field to the summary
215
+ (`stripe-mock-promotion-code`, `-apply`, `-remove`; a refusal shows
216
+ `stripe-mock-promotion-error` with Checkout's "This code is invalid."). Codes are
217
+ case-insensitive and checked for being active, unexpired, under their limits, for this customer,
218
+ first-time and minimum-amount restrictions, and applicable products; the session re-prices.
219
+ - Creation is validated like Stripe: `customer` with `customer_email`, a recurring price in
220
+ `payment` mode, `allow_promotion_codes` with `discounts`, `subscription_data` outside
221
+ subscription mode, `payment_intent_data` outside payment mode, `customer_creation` outside
222
+ payment mode, inactive prices, mixed currencies, quantities below 1, `trial_end` under 48 h.
188
223
  - `POST /__admin/checkout/sessions/:id/complete {"card": "4242…"}` does the same without a browser;
189
224
  `…/expire` and `…/async_payment_succeeded` too.
190
225
  - `GET /v3` — the Stripe.js stand-in: `Stripe(pk)`, `elements()` → `create("payment"|"card")`,
@@ -196,6 +231,38 @@ finalize; a customer credit balance is applied at finalize; `void` works only on
196
231
  A publishable key may only confirm or read an intent whose `client_secret` it presents, and create
197
232
  payment methods; anything else is Stripe's 401.
198
233
 
234
+ ### Customer portal
235
+
236
+ `POST /v1/billing_portal/sessions` returns a `url` on the mock (`/p/session/:id`, in place of
237
+ billing.stripe.com) where the customer manages their billing, as the session's configuration allows:
238
+
239
+ - **Configurations**: `POST|GET /v1/billing_portal/configurations[/:id]`
240
+ (`billing_portal.configuration.created|updated`). Each account has a default configuration, as if
241
+ saved in the dashboard: every feature on, cancellation at period end with a reason, plan and
242
+ quantity changes with `create_prorations` across every active product with active recurring
243
+ prices. API-created configurations start with every feature off; `products[].prices` must be
244
+ recurring prices of that product; the default cannot be deactivated.
245
+ - **Sessions** check the customer, an active configuration, and `flow_data`: the subscription must
246
+ be the customer's and manageable, the feature enabled, `subscription_update_confirm` prices
247
+ offered, a retention coupon real (`billing_portal.session.created`). `return_url` falls back to
248
+ `default_return_url`.
249
+ - **The page** (`data-testid="stripe-mock-portal"`, `data-view`): current subscriptions with their
250
+ status (trial end, cancels on, past due, paused, payments paused); **Cancel plan** (reason and
251
+ comment → `cancellation_details`; at period end, or immediately with the configured proration);
252
+ **Renew plan** for a subscription set to cancel; **Update plan** (price and quantity within the
253
+ configured bounds, a confirmation step showing the proration, then the configured
254
+ `proration_behavior`; `trial_update_behavior=end_trial` ends a trial); payment methods (**Add**
255
+ through a SetupIntent, which becomes the customer's and each subscription's default; **Make
256
+ default**; **Delete** a non-default card); billing information (the configured `allowed_updates`,
257
+ `customer.updated` with `previous_attributes`); invoice history with **Pay** for open invoices.
258
+ - **Deep links** open on the flow's page — cancel (with a retention offer to accept), update, update
259
+ confirmation, payment method, billing details — and on completion honour `after_completion`:
260
+ `redirect` (302), `hosted_confirmation` (its `custom_message`) or the homepage. Every action
261
+ runs the same code as the API call it stands for, so the webhooks match.
262
+
263
+ Like Stripe's portal, it has no pause control: pausing is `pause_collection` or
264
+ `trial_settings` + `/resume` through the API, and the portal shows a paused subscription as such.
265
+
199
266
  ### Test values
200
267
 
201
268
  - Payment methods: `pm_card_visa`, `pm_card_mastercard`, `pm_card_amex`, `pm_card_discover`,
@@ -299,6 +366,9 @@ plus `port`, `host`; resolves `{url, port, runtime, close}`), `serveTarget` (the
299
366
  `unpaid` and dunning emails are not run.
300
367
  - **Proration arithmetic** is day-fraction approximate (Stripe prorates to the second);
301
368
  `auto_advance` drafts are not finalized an hour later.
369
+ - **Customer portal extras**: the login page (`login_page.url` is not served), `schedule_at_period_end`
370
+ downgrades, `billing_cycle_anchor` resets on plan changes, multi-item subscription updates, locales,
371
+ and payment method configurations. Portal sessions do not expire.
302
372
  - **Webhook endpoint `api_version`**: payloads render at the account's version, not per endpoint.
303
373
  - **Live keys** (`sk_live_…`) are refused with Stripe's 401: the mock is test mode only.
304
374
  - Operations marked unsupported in SUPPORT.md (charge create/update, checkout session update,