# `Accrue.Webhooks.DLQ`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v0.3.0/lib/accrue/webhooks/dlq.ex#L1)

Dead-letter queue replay and retention for webhook events (WH-08 / D4-04).

Replay inserts a fresh Oban dispatch job. Oban's own `retry_job/2`
refuses jobs in `:discarded`/`:cancelled` states, and dead-lettered
webhook events correspond to exactly those — so the only correct path
is to insert a brand-new job whose args reference the existing
`WebhookEvent` row by id.

## Public API

  * `requeue/1` — single dead-lettered row → fresh dispatch job
  * `requeue_where/2` — bulk replay with batch + stagger + dry-run + max-rows cap
  * `list/2` — paginated browse for ops tooling
  * `count/1` — accurate count for confirm prompts
  * `prune/1` — delete `:dead` rows older than N days
  * `prune_succeeded/1` — delete `:succeeded` rows older than N days

Each public function ships in dual bang/tuple form per the D-05
convention.

## Replay-death-loop prevention

When a replayed event re-enters the dispatch worker and the processor
fetch returns `{:error, :not_found}` (e.g., the underlying upstream
resource has been deleted since the original failure), the worker
treats it as terminal-skip — status becomes `:replayed`, no
re-dead-letter — so a single bad row cannot loop forever.

# `filter`

```elixir
@type filter() :: [
  type: String.t() | [String.t()],
  since: DateTime.t(),
  until: DateTime.t(),
  livemode: boolean()
]
```

# `replay_error`

```elixir
@type replay_error() ::
  :not_found
  | :already_replayed
  | :not_dead_lettered
  | :replay_too_large
  | term()
```

# `replay_opts`

```elixir
@type replay_opts() :: [
  batch_size: pos_integer(),
  stagger_ms: non_neg_integer(),
  dry_run: boolean(),
  force: boolean()
]
```

# `count`

```elixir
@spec count(filter()) :: non_neg_integer()
```

# `list`

```elixir
@spec list(
  filter(),
  keyword()
) :: [Accrue.Webhook.WebhookEvent.t()]
```

# `prune`

```elixir
@spec prune(pos_integer() | :infinity) :: {:ok, non_neg_integer()}
```

# `prune_succeeded`

```elixir
@spec prune_succeeded(pos_integer() | :infinity) :: {:ok, non_neg_integer()}
```

# `requeue`

```elixir
@spec requeue(Ecto.UUID.t()) ::
  {:ok, Accrue.Webhook.WebhookEvent.t()} | {:error, replay_error()}
```

# `requeue!`

```elixir
@spec requeue!(Ecto.UUID.t()) :: Accrue.Webhook.WebhookEvent.t()
```

# `requeue_where`

```elixir
@spec requeue_where(filter(), replay_opts()) ::
  {:ok, map()} | {:error, :replay_too_large | term()}
```

# `requeue_where!`

```elixir
@spec requeue_where!(filter(), replay_opts()) :: %{
  requeued: non_neg_integer(),
  skipped: non_neg_integer()
}
```

---

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