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 reconciliationattempt_link/2— look up the matching delivery (byproviderprovider_message_idin:metadata/:normalized_payload; Phase 4 V02 migration droppedraw_payloadfrom the ledger per D-15)
Summary
Functions
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 events awaiting reconciliation (delivery_id is NULL + needs_reconciliation = true), ordered oldest first.
Functions
@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).
@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.