# `Accrue.Billing.Dunning`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v0.3.0/lib/accrue/billing/dunning.ex#L1)

Pure policy module for BILL-15 dunning (D4-02 hybrid).

No side effects, no DB, no Stripe calls — `Accrue.Jobs.DunningSweeper`
owns those. This module's job is to answer one question for each
candidate subscription: "given the configured policy, what should we
ask the processor facade to do next?"

The sweeper is a thin grace-period overlay on top of Stripe Smart
Retries. Stripe still owns the retry cadence. Accrue only asks the
processor to move the subscription to the terminal action once the
grace window has elapsed and we have not already asked (tracked via
`dunning_sweep_attempted_at`).

## Policy shape

    [
      mode: :stripe_smart_retries | :disabled,
      grace_days: pos_integer(),
      terminal_action: :unpaid | :canceled,
      telemetry_prefix: [atom()]
    ]

## Decisions

  * `:skip` — do nothing (not past_due, already swept, or disabled).
  * `:hold` — past_due but still inside the grace window.
  * `{:sweep, terminal_action}` — grace elapsed; sweeper should ask
    the processor facade to move the subscription to `terminal_action`.

Local subscription status is NEVER touched by the sweeper (D2-29 —
Stripe is canonical; the webhook flips the row).

# `decision`

```elixir
@type decision() :: {:sweep, :unpaid | :canceled} | :hold | :skip
```

# `policy`

```elixir
@type policy() :: keyword()
```

# `compute_terminal_action`

```elixir
@spec compute_terminal_action(Accrue.Billing.Subscription.t(), policy()) :: decision()
```

Pure decision function. Given a subscription row and a dunning policy,
returns whether the sweeper should `:skip`, `:hold`, or
`{:sweep, terminal_action}`.

# `grace_elapsed?`

```elixir
@spec grace_elapsed?(DateTime.t() | nil, pos_integer(), DateTime.t()) :: boolean()
```

Returns `true` when `now` is more than `grace_days` past `past_due_since`.

A `nil` `past_due_since` returns `false` — with no recorded start of
the past_due window, there is no grace to elapse.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
