Stripe

Stripe billing for an Askr app: a customer per user, Checkout for subscriptions and one-off payments, the customer portal, and webhooks that are verified, recorded once and applied in one transaction.

Billing is not something every app needs, and Stripe moves on Stripe's
schedule rather than Askr's. So it is not in the framework: it is the first
plugin, and the one the plugin mechanism was designed against.

## What an app gets

- **A customer per user.** The framework does not own your user model, so
  the Stripe customer is keyed by your user id as text — the id `Login`
  takes — the way API tokens are.
- **Checkout, not a payment form.** Stripe's hosted Checkout takes the card,
  3-D Secure and SCA, so none of that is Pascal an app has to trust.
  Subscriptions and one-off payments both start there.
- **The customer portal** for changing a card, switching a plan or
  cancelling, on Stripe's own page.
- **Webhooks that arrive once.** The signature is checked against the raw
  body with a time window; each event id is recorded, so a retried delivery
  has no second effect; and the event is applied in one transaction. If
  anything fails, it all rolls back and the answer is a 500 — and Stripe
  sends it again, for up to three days. Stripe's retries are the queue, so
  a new app needs none of its own.
- **Order that does not matter.** Stripe does not promise it. An older
  event is still recorded and still dispatched, but it does not overwrite
  a newer subscription state.
- **Questions an app asks:** is this user subscribed to that price, on a
  trial, in a grace period after cancelling.

## Money is minor units

Stripe counts in øre and cents, as integers. They go into `Currency` by
assignment, never by a cast: a cast reinterprets the bits instead of
converting the value, and gives a different answer on x86_64 than on
arm64.

## What the real account found

The first run against a test-mode account found a bug no fake could: the
customer was made with an idempotency key built from the user id. Stripe
keeps a key for a day across everything that shares the account, so a
reset database got a deleted customer back — and two apps on one account,
a dev and a staging, would have shared customers. The key is random now,
and a race between two requests is decided by the row.

## What it does not do

Invoices as PDF, metered billing, Stripe Tax, Connect and card forms in
your own pages are not in it. Each is a Stripe product with its own
surface, and none of them is needed to take a subscription. Changing a
plan is the portal's, or Stripe's API — the documentation shows both.