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

Webhook handler for events arriving on the `:connect` endpoint.

Stripe Connect platforms receive two distinct webhook streams:

1. **Platform events** (`/webhooks/stripe`) — account-scoped events
   targeting the platform account itself; dispatched to
   `Accrue.Webhook.DefaultHandler`.
2. **Connect events** (`/webhooks/stripe/connect`) — events relayed by
   Stripe from a connected account (`account.updated`,
   `account.application.{authorized,deauthorized}`, `capability.updated`,
   `payout.*`, `person.*`); dispatched to this module.

The `Accrue.Webhook.DispatchWorker` reads `row.endpoint` from the
persisted `accrue_webhook_events` row and selects the handler module at
dispatch time. This module implements the Connect-specific reducers.

## Reducer shape

Every reducer runs inside `Accrue.Repo.transact/1` with an
`Accrue.Events.record/1` call appended so the state mutation and audit
row commit atomically. For events whose payload is needed
beyond the `%Accrue.Webhook.Event{}` struct's `object_id`, the raw
`accrue_webhook_events.data` jsonb row is refetched via
`ctx.webhook_event_id` — the handler never trusts the lean Event
struct alone for anything beyond routing.

## Out-of-order delivery (Pitfall 3)

Stripe does not guarantee webhook delivery order. An `account.updated`
for a just-created account can arrive before the local
`accrue_connect_accounts` row has been inserted. The reducer handles
this by calling `Accrue.Connect.retrieve_account/2`, which runs the
canonical processor round-trip and upserts the local row via
`Accrue.Connect.Account.force_status_changeset/2` — so a missing row
is seeded from the latest Stripe state in the same transaction that
would otherwise have failed. Later, in-order events arriving on a
now-settled row update it the same way (idempotent).

The reducer uses "refetch canonical" rather than "compare
timestamps" for stale-event detection: because the Stripe round-trip
always returns the current account state, a stale webhook replay
overwrites the row with the same values it already has — a
functional no-op — rather than clobbering later events with older
snapshot data (Pitfall 3).

## Deauthorization tombstoning

`account.application.deauthorized` NEVER hard-deletes the local row.
It stamps `deauthorized_at` via `force_status_changeset/2` so the row
survives for audit, and emits an ops telemetry event
`[:accrue, :ops, :connect_account_deauthorized]` via
`Accrue.Telemetry.Ops`.

## Scope

Reducers for `person.*` log a debug line and no-op — Custom-account
person KYC is deferred to v1.x.

---

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