## The division of labour

| Stripe does | Your app does |
|---|---|
| Retry schedule and dunning email | Move a row to `past_due` and back |
| Refund mechanics and the money movement | Revoke access if the refund means they should lose it |
| Chargeback representment workflow | Know that a dispute happened |
| Card updates via the billing portal | Nothing |

Everything in the right column happens in one controller,
`app/controllers/webhooks/stripe_controller.rb`, and writes to one model,
`app/models/subscription.rb`.

## Refunds are issued in the dashboard

There is no refund button to build. You refund in Stripe, `charge.refunded` arrives, and your handler
decides what that means. For a subscription, usually nothing: the subscription continues or was
already canceled separately. For a one-time purchase it usually means revoking, or you have refunded
someone who keeps the product.

## The part that is genuinely yours

Idempotency. Stripe retries deliveries, so the same event can arrive twice, and a handler that
increments or appends rather than setting will double up. The activation path is written as an
idempotent upsert for this reason:

```ruby
Subscription.activate!(account, plan: :pro)
```

Running it twice leaves the same row. Any handler you add should have that property, and it is worth
checking by replaying an event from the Stripe dashboard rather than assuming.

## Verify the four events end to end

```bash
bin/dev
stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger invoice.payment_failed
stripe trigger charge.refunded
```

With `stripe listen` forwarding to your local app, those two exercise the branches that are hardest
to reach by clicking, and they are the ones most likely to be wrong because nobody tests them.

## What you should not build

A reconciliation job that polls Stripe for subscription state. It is tempting after a missed webhook,
but it hides the real problem, which is that the endpoint is failing. Fix the endpoint; Stripe
retries for three days and backfills on its own.
