## The sequence

1. The renewal charge fails. Stripe fires `invoice.payment_failed`.
2. Stripe retries over the following days and emails the customer each time. That schedule is
   configurable in the dashboard and is called dunning.
3. Either a retry succeeds, and `invoice.payment_succeeded` arrives, or Stripe gives up and cancels,
   which fires `customer.subscription.deleted`.

The status enum in `app/models/subscription.rb` has `past_due` for exactly the middle of that, and
`entitled?` does not include it.

## Decide what past due means for you

This is a product decision, not a technical one, and the code makes it a one-line change:

```ruby
def entitled? = active? || trialing?
```

As written, a past due account loses access immediately. That is the strict reading, and it is
defensible for a high-cost service. For most products it is too harsh: the common cause is a card
that expired, the customer intends to pay, and locking them out while Stripe is still retrying turns
a billing hiccup into a cancellation.

Adding `past_due?` to that method gives them the retry window. Whichever you choose, choose it on
purpose.

## Do not build your own dunning emails

Stripe already sends them, with the card update link, on a schedule you can configure. A second set
of emails from your app arrives alongside Stripe's and says the same thing less well. Enable Stripe's
and spend the effort on the billing portal link instead, which is where they fix it.

## Check who is currently in that state

```bash
bin/kamal app exec --interactive --reuse 'bin/rails console'
```

```ruby
Subscription.where(status: "past_due").count
```

A number that only grows means `customer.subscription.deleted` is not enabled on your webhook
endpoint, so nothing ever resolves the state in either direction. The handler that would move it is
in `app/controllers/webhooks/stripe_controller.rb`.

## The related failure

A customer who updates their card in the billing portal produces
`customer.subscription.updated`. If that event is not enabled, the payment succeeds and your app
still shows them as past due.
