Skip to content

Move to stripe 23, and add a reconcile that covers every owner - #97

Merged
nedtwigg merged 6 commits into
mainfrom
stripe-23
Nov 10, 2026
Merged

nedtwigg merged 6 commits into
mainfrom
stripe-23

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

Moves @pgstencil/stripe from stripe 22 to 23 (API 2026-08-26.dahlia → 2026-10-28.endive) and adds a reconcile that covers every owner, so a consumer can prove an API version move lost nothing.

Draft until the sandbox rehearsal is done. Everything here ran against StripeDev; nothing has reached api.stripe.com. This PR raises no package version, so merging it releases nothing.

What changes for a consumer

  • Peer range is stripe ^23.1.0. Every stripe minor moves the pinned API version (23.0 pins 2026-09-30.endive, 23.1 pins 2026-10-28.endive), so the docs now tell consumers to pin stripe with ~ and treat any move as the endpoint procedure in BILLING.md → "Moving to a new Stripe API version".
  • Checkout sends allowed_payment_method_types: ['card']. v23 removes payment_method_types. The new parameter filters the methods Stripe would offer instead of listing them. This also changes Checkout's idempotent parameters, so a creation retried across the deploy is refused.
  • paused is a reachable status, through the pause endpoint or the portal's opt-in pause flow. It denies access and blocks another checkout. reconcileStale does not pick up a paused owner.
  • reconcileAll({ dryRun?, pageSize? }) pages every account with a customer, resyncs each under its owner lock, and returns the owners whose subscription rows changed, with the rows before and after, plus each failure, without stopping. A dry run rolls each owner's transaction back. reconcileStale finds only owners holding a current subscription without access, so it never repairs a cancellation or refund missed during a cut-over.
  • Webhook tolerance is unchanged. The SDK's default moved, but pgstencil already passed 300 seconds explicitly; a test now pins it.

No migration is added or changed. No field pgstencil reads changed type between the two API versions.

Cut-over behavior, from the code

  • Applying one event id twice is harmless: the id is recorded with ON CONFLICT DO NOTHING and processing stops if it was already handled. A delivery that fails verification records nothing, so it cannot block the other endpoint's copy.
  • Two endpoints at different versions are told apart only by signing secret and the exact api_version check. Each release handles its own endpoint's deliveries and answers 400 to the other's.

Verification

  • pnpm check passes on the branch (format, typecheck, script tests, 209 Vitest tests).
  • reconcileAll's tests fail with the resync removed and with the dry-run rollback removed.
  • pnpm packages:pack produces clean archives; Dormouse Hosted is being built and tested against them on a rehearsal branch.

Not verified, and how to verify it

All of these need a Stripe sandbox:

  • Checkout with allowed_payment_method_types succeeds, alone and with automatic tax.
  • A webhook endpoint created at 2026-10-28.endive is accepted and its events verify.
  • Pause and resume events arrive with the shapes StripeDev assumes.
  • One full walk of the version-move steps with two endpoints, ending in a reconcileAll that reports no changed owner.

StripeDev remains partial: it has no invoices, payments, or refunds, and does not serve the pause endpoint.

🤖 Generated with Claude Code

nedtwigg and others added 6 commits November 10, 2026 08:40
The peer and dev ranges in packages/stripe and the login example's
dependency go from ^22.6.1 to ^23.1.0, the newest stable release
(published 2026-10-29, outside the one-day minimum release age). The
peer range admits v23 only: STRIPE_API_VERSION is the installed SDK's
pinned version, so a range spanning both majors would let a consumer's
install choose between two API versions and two type surfaces.

Stripe 23 removes `payment_method_types` from Checkout session creation;
Checkout now sends `allowed_payment_method_types: ['card']`, which
filters the account's dynamically eligible methods down to cards. The
request snapshot and the trial test's expected parameters follow.

Validated: pnpm typecheck, pnpm test:unit, and the billing, billing-http
and billing-workers integration tests (54 passing), all against StripeDev.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Subscriptions StripeDev creates now carry the fields the endive API
versions define and that were missing or new: each item's
`current_period_start` (advanced with `current_period_end` on renewal
and failed payment) and `current_trial: null`, `trial_settings` with
`end_behavior.billing_cycle_anchor: null` and the requested
`missing_payment_method`, and `pending_update: null`.

`transition(id, 'pause')` and `'resume'` model the new pause endpoint
and the portal's `subscription_pause` flow: pausing sets
`status: 'paused'` with `status_details.paused`, resuming returns to
`active` with a new period starting now; each emits
`customer.subscription.updated` and then `.paused` or `.resumed`.

Checkout session creation refuses `payment_method_types`, which the
endive versions removed, and records `allowed_payment_method_types` on
the session.

A new billing test pins what a paused subscription means to pgstencil:
no access, another checkout refused, not chosen by reconcileStale, and
access back after resuming. Removing 'paused' from SUBSCRIBED fails it.

Validated: pnpm typecheck, pnpm format:check, pnpm test:unit and the
three billing integration suites against StripeDev.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stripe 23 makes `webhooks.signature.verifyHeader[Async]` apply the
300-second default when no tolerance is passed (it used to skip the
check) and lets 0 really skip it. `verifyWebhook` passes 300 to
`constructEventAsync` explicitly, with the injected clock as
`receivedAt`, so its behavior is unchanged: v22 and v23 both check the
`t=` of the Stripe-Signature header, the time Stripe signed that
delivery attempt, against the clock, rejecting only an age over 300 s.

A new test pins that: a delivery is accepted exactly 300 s after it was
signed and refused one second later; the same event re-signed five hours
later, as a Stripe retry is, verifies and is a ledger no-op (one row,
one attempt). The test fails with the tolerance set to 299 or to 0.

Validated: pnpm typecheck, pnpm format:check, the billing integration
suite (28 passing) against StripeDev.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
BILLING.md names the pinned version (2026-10-28.endive for stripe 23.1)
and how to create a webhook endpoint at it, explains that the 300-second
tolerance reads each delivery's signed timestamp, says what `paused`
means for access now that Stripe's pause endpoint and the portal's
subscription_pause flow reach it, states which stale owners
reconcileStale never chooses, documents the StripeDev pause and resume
transitions and what StripeDev does not model, and adds "Moving to a
new Stripe API version", marked as not yet exercised end to end.
Checkout's card restriction is now `allowed_payment_method_types`.

packages/stripe/README.md and PACKAGES.md tell applications that every
`stripe` minor release moves the API version webhooks must match, so
they pin it with `~`.

Validated: pnpm check (format, typecheck, 31 script tests, 208 Vitest
tests) passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…rts what changed

reconcileStale finds only owners holding a current subscription without
access, so an event missed during a Stripe API version move (a
cancellation, a portal-scheduled cancellation, a refund) is never
repaired by it. reconcileAll keyset-pages every account with a
customer, resyncs each under its owner lock, and returns the owners
whose subscription rows changed with the rows before and after, plus
each failure, without stopping. A dry run rolls each owner's
transaction back, so it reports what would change and writes nothing.

Its report is the measurement a cut-over needs: each changed owner is
one event that was lost. BILLING.md's migration step 5 now names it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@nedtwigg
nedtwigg marked this pull request as ready for review November 10, 2026 21:02
@nedtwigg
nedtwigg merged commit a911fd7 into main Nov 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant