# `Mailglass.Events.Reconciler`
[🔗](https://github.com/szTheory/mailglass/blob/v0.1.0/lib/mailglass/events/reconciler.ex#L1)

Pure Ecto query functions for orphan-webhook reconciliation (D-19).

Phase 2 scope: query functions only. Phase 4 wraps these in
`Mailglass.Oban.Reconciler` at `{:cron, "*/15 * * * *"}` cadence
(D-20). Phase 2 has no Oban dep.

## What "orphan" means

A webhook event arrives for a `provider_message_id` before the
delivery row has committed its `provider_message_id` field
(empirical: SendGrid + Postmark p99 webhook latency is 5-30s;
dispatch commits are ms-scale but the window is real). Phase 4's
webhook plug inserts the event with `delivery_id = nil` and
`needs_reconciliation = true` rather than failing.

## Reconciliation semantics (research §6.1 note)

`needs_reconciliation` lives on the event row, not the delivery,
and the immutability trigger prevents UPDATE on events. The Phase 4
worker's exact mechanic (emit a `:reconciled` event + update
delivery projection vs. only update projection) is Phase 4's call.
Phase 2 exposes the primitives either approach needs:

- `find_orphans/1` — query events awaiting reconciliation
- `attempt_link/2` — look up the matching delivery (by `provider`
  + `provider_message_id` in `:metadata` / `:normalized_payload`;
  Phase 4 V02 migration dropped `raw_payload` from the ledger per D-15)

# `attempt_link`
*since 0.1.0* 

```elixir
@spec attempt_link(Mailglass.Events.Event.t()) ::
  {:ok, {Mailglass.Outbound.Delivery.t(), Mailglass.Events.Event.t()}}
  | {:error, :delivery_not_found | :malformed_payload}
```

Attempts to locate the matching `%Delivery{}` for an orphan event
via `(provider, provider_message_id)`. The provider + message id are
extracted from the event's `:metadata` first, then `:normalized_payload`
(Phase 4 V02 migration dropped `raw_payload` from the ledger per D-15;
provider bytes now live in `mailglass_webhook_events.raw_payload`, and
Plan 06 Ingest writes identifying fields into the ledger's `:metadata`
at insert time).

Returns `{:ok, {delivery, event}}` when matched;
`{:error, :delivery_not_found}` when no delivery with the
(provider, provider_message_id) pair exists.

Pure query — does NOT mutate anything. Phase 4's Oban worker
decides whether to emit a `:reconciled` event and/or update the
delivery projection after this returns success.

Emits `[:mailglass, :persist, :reconcile, :link, :*]` with
`tenant_id` metadata (PII-free per D-31 whitelist).

# `find_orphans`
*since 0.1.0* 

```elixir
@spec find_orphans(keyword()) :: [Mailglass.Events.Event.t()]
```

Returns events awaiting reconciliation (delivery_id is NULL +
needs_reconciliation = true), ordered oldest first.

## Options

- `:tenant_id` — scope to a specific tenant. Default: all tenants.
- `:limit` — max rows returned. Default: 100.
- `:max_age_minutes` — ignore orphans older than this (integer).
  Default: 10_080 (7 days).

Uses the partial index `mailglass_events_needs_reconcile_idx`
(Plan 02) for efficient scans.

---

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