Accrue.Webhook.Ingest (accrue v0.3.0)

Copy Markdown View Source

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.

Summary

Functions

Runs the transactional ingest pipeline for a verified webhook event.

Functions

run(conn, processor, stripe_event, raw_body, endpoint \\ nil)

@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.