# `Accrue.Jobs.DunningSweeper`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v0.3.0/lib/accrue/jobs/dunning_sweeper.ex#L1)

Oban cron worker for BILL-15 / D4-02 dunning grace-period overlay.

Stripe Smart Retries owns the retry cadence. Accrue owns a thin
grace-period overlay on top: once `past_due_since` is older than the
configured `grace_days`, this worker asks the processor facade to
move the subscription to the terminal action (`:unpaid` or
`:canceled`) and stamps `dunning_sweep_attempted_at` on success.

## Canonicality (D2-29)

This worker NEVER flips the local `subscription.status`. It only:

  * Calls `Accrue.Processor.update_subscription/3` to ask the
    processor facade to transition the row.
  * Stamps `dunning_sweep_attempted_at` AFTER a successful processor
    call so the same row is not retried on the next tick.
  * Records a `dunning.terminal_action_requested` audit event in
    `accrue_events`.

The actual local status flip happens when Stripe echoes the change
back via `customer.subscription.updated`, which the
`Accrue.Webhook.DefaultHandler` picks up and projects.

## Host wiring

Accrue does not start its own Oban instance. The host app must wire
the cron themselves:

    config :my_app, Oban,
      queues: [accrue_dunning: 2],
      plugins: [
        {Oban.Plugins.Cron,
         crontab: [{"*/15 * * * *", Accrue.Jobs.DunningSweeper}]}
      ]

## Failure handling

A processor error on any row logs a warning and returns `false`
WITHOUT stamping `dunning_sweep_attempted_at`, so the next cron tick
picks the same row up again. `max_attempts: 3` on the worker bounds
per-tick retries.

# `sweep`

```elixir
@spec sweep() :: {:ok, non_neg_integer()}
```

Runs one sweep tick. Returns `{:ok, count}` where `count` is the
number of subscriptions successfully transitioned this tick.

---

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