You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Issue #1571 handles the happy path and clean duplicates. Real payment traffic also includes declines, timeouts, abandoned checkouts, partial refunds, and webhooks that simply never show up. Without a reconciliation layer, these become silent bugs — a booking stuck PENDING forever, or money captured but never confirmed in our own records.
This is issue 3 of 7 in the payment track.
Objective
Define a complete failure taxonomy as first-class data, implement a scheduled reconciliation job that treats provider truth as authoritative and self-heals drift, and give admins a bounded manual-review path for anything that can't be resolved automatically.
Technical approach
Failure taxonomy as enum values, not free text: DECLINED, EXPIRED (no confirmation within TTL), PROVIDER_ERROR (5xx/timeout talking to the provider), ABANDONED (user never returned), PARTIALLY_REFUNDED, DISPUTED — each with a defined allowed next-state set inside Payments 1/7: Payment Domain Model, Initiation Flow & Idempotent Transaction Lifecycle #1570's transition guard.
Reconciliation cron: every few minutes, pull Payments in AWAITING_CONFIRMATION past a short threshold and re-verify directly against the provider (reusing Payments 2/7: Real-Time Confirmation Pipeline & the App/Provider State Boundary #1571's fast-path verify call); resolve to CONFIRMED/FAILED from provider truth, independent of whether a webhook ever arrived.
Escalation tier: payments still unresolved after a longer threshold (e.g. 24h) are flagged MANUAL_REVIEW and surfaced to admins with a reason — never silently retried forever.
Retry/backoff utility for our own outbound provider calls (verify, refund): exponential backoff with jitter, capped attempts, and a hard distinction between retryable errors (timeout/5xx) and terminal ones (4xx).
Partial-outcome support: a refund sub-ledger allowing multiple partial refunds against one Payment, tracking "amount captured" vs "amount refunded" rather than a single boolean.
Shared retry/backoff utility (also used by the on-chain rail issue).
Metrics: payments resolved by webhook vs. by reconciliation vs. stuck in manual review (feeds the observability issue).
Important edge cases and failure scenarios
Provider reports "still processing" indefinitely (e.g. a bank-side hold) — must not flip-flop state on every pass; back off polling frequency the longer it stays pending.
Reconciliation job crashes mid-batch — must be safe to re-run: idempotent, resumable, no duplicate side effects (e.g. a second confirmation email).
Refund exceeds the remaining captured amount (double-refund race between two admins) — rejected atomically.
A provider-side outage makes every verify call fail — reconciliation must distinguish "provider unreachable" from "provider says failed," never mass-flagging everything MANUAL_REVIEW after one bad run.
A failed payment must release its associated booking hold — must coordinate with booking cancellation without leaking a held slot forever.
Dependencies
Depends on #1570 and #1571 (reuses the transition guard and verify-on-return call).
Acceptance criteria
A simulated "webhook never arrives" scenario is resolved correctly by reconciliation within its polling window (test).
Double-refund race test rejects the second refund atomically.
The manual-review queue correctly excludes payments still within a normal provider-outage retry window (no false positives from a single simulated outage).
Reconciliation is safe to run twice against the same data with no duplicate side effects (idempotency test).
Definition of done
Cron job running in staging with dashboarded metrics; admin manual-review and recovery actions available and role-restricted; failure taxonomy documented in the payments module README; alert wired for manual-review queue depth exceeding a threshold.
Problem / Context
Issue #1571 handles the happy path and clean duplicates. Real payment traffic also includes declines, timeouts, abandoned checkouts, partial refunds, and webhooks that simply never show up. Without a reconciliation layer, these become silent bugs — a booking stuck
PENDINGforever, or money captured but never confirmed in our own records.This is issue 3 of 7 in the payment track.
Objective
Define a complete failure taxonomy as first-class data, implement a scheduled reconciliation job that treats provider truth as authoritative and self-heals drift, and give admins a bounded manual-review path for anything that can't be resolved automatically.
Technical approach
DECLINED,EXPIRED(no confirmation within TTL),PROVIDER_ERROR(5xx/timeout talking to the provider),ABANDONED(user never returned),PARTIALLY_REFUNDED,DISPUTED— each with a defined allowed next-state set inside Payments 1/7: Payment Domain Model, Initiation Flow & Idempotent Transaction Lifecycle #1570's transition guard.Payments inAWAITING_CONFIRMATIONpast a short threshold and re-verify directly against the provider (reusing Payments 2/7: Real-Time Confirmation Pipeline & the App/Provider State Boundary #1571's fast-path verify call); resolve toCONFIRMED/FAILEDfrom provider truth, independent of whether a webhook ever arrived.MANUAL_REVIEWand surfaced to admins with a reason — never silently retried forever.Payment, tracking "amount captured" vs "amount refunded" rather than a single boolean.Detailed scope
Important edge cases and failure scenarios
MANUAL_REVIEWafter one bad run.Dependencies
Depends on #1570 and #1571 (reuses the transition guard and verify-on-return call).
Acceptance criteria
Definition of done
Cron job running in staging with dashboarded metrics; admin manual-review and recovery actions available and role-restricted; failure taxonomy documented in the payments module README; alert wired for manual-review queue depth exceeding a threshold.