# `Accrue.Webhook.Ingest`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v0.3.1/lib/accrue/webhook/ingest.ex#L1)

Transactional webhook event persistence and Oban job dispatch (D2-24).

All three writes (webhook_event row, Oban job, accrue_events ledger entry)
succeed atomically or none do. Duplicate POSTs are idempotent via the
`UNIQUE(processor, processor_event_id)` constraint with `on_conflict: :nothing`.

## Transaction shape

    Ecto.Multi
    |> run(:persist, ...)           # check-then-insert with on_conflict guard
    |> run(:maybe_enqueue, ...)     # Oban job only for new events
    |> run(:maybe_ledger, ...)      # accrue_events only for new events

Duplicate detection uses an explicit SELECT-then-INSERT pattern inside a
single `Multi.run` step. The `on_conflict: :nothing` guard on the INSERT
handles the race condition where a concurrent request inserts between
SELECT and INSERT. With binary_id autogenerate, Ecto generates UUIDs
client-side, so the Pitfall 2 approach (`id: nil` on conflict) does not
work -- the struct always has an id regardless of conflict.

# `run`

```elixir
@spec run(Plug.Conn.t(), atom(), LatticeStripe.Event.t(), binary(), atom() | nil) ::
  Plug.Conn.t()
```

Runs the transactional ingest pipeline for a verified webhook event.

Called by `Accrue.Webhook.Plug` after signature verification succeeds.
Returns the `Plug.Conn` with a 200 (success or duplicate) or 500 (failure).

## Endpoint

Phase 5 (05-01, D5-01): the optional `endpoint` argument is persisted on
the `accrue_webhook_events` row so `Accrue.Webhook.DispatchWorker` can
branch on it and route `:connect`-scoped events to
`Accrue.Webhook.ConnectHandler`. Accepts either the atom `:connect` (which
persists as `:connect`) or any other value (persists as `:default`) so
existing Phase 2-4 multi-endpoint configs using names like `:primary` or
`:unconfigured` continue to land in the default lane without needing a
schema migration per endpoint name.

---

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