Skip to content

[Gap-Audit] 040 Payment Retry UI: idempotency-key reuse, retry counter + cooling, error categorization, offline banner, audit log #43

@TortoiseWolfe

Description

@TortoiseWolfe

Status correction (2026-04-27)

The original framing of this issue ("build /payment/result page + retry surface + offline error banner") was wrong. The route already exists.

src/app/payment-result/page.tsx shipped in commit ffb33a1 on 2026-04-16 — a 290-LOC, 6-state page with auth gating, suspense, real-time updates via usePaymentRealtime, and a working retry button. The 2026-04-25 audit cross-referenced spec text against documented status rather than the filesystem and concluded "route missing"; that error then propagated through STATUS.md, PRP-STATUS.md, the spec, the truth-table, the stability tracker, and the session handoff.

Doc-correction PR: #62. Real B1 code work: see "Real gaps" below — follow-up PR.

What's shipped

  • src/lib/payments/payment-service.tsretryFailedPayment(intentId) retry logic
  • Stripe + PayPal webhook handlers (742 LOC Deno)
  • src/app/payment-result/page.tsx (commit ffb33a1, 2026-04-16) — 6-state route with auth + Suspense + retry button
  • src/components/payment/PaymentStatusDisplay/PaymentStatusDisplay.tsx — status badge + details + real-time updates + retry CTA
  • 4 live E2E tests in tests/e2e/payment/03-failed-payment-retry.spec.ts (missing-session, malformed-ID, payment demo loads, consent flow)

Real gaps (this is what B1 actually closes)

In this issue's scope (recommended next PR — gaps 1-5)

  1. Retry button regenerates idempotency_key. retryFailedPayment() (src/lib/payments/payment-service.ts:237) calls createPaymentIntent() for a fresh INSERT, which loses the queued idempotency_key and bypasses the partial unique index that landed in PR fix(offline-queue): watchdog reclaim + idempotent payment INSERTs (#52) #59. The retry button on /payment-result should reuse the queued key.
  2. No retry attempt counter or cooling period (FR-008, FR-009, FR-010). Currently: unlimited rapid retries permitted.
  3. No error categorization (FR-002 — 8 error types defined, none mapped). Current UI shows status === 'failed' with no reason context.
  4. No offline error banner.
  5. No audit log on retry attempts (NFR-007).

Deferred to a follow-up PR (Stories 3 and 4 — large, P1 in spec but separate scope)

  1. User Story 3 — Update Payment Method (FR-011-FR-015). Has stripe.js + PCI surface considerations that warrant their own design pass.
  2. User Story 4 — Guided Recovery Wizard (FR-016-FR-019). Largest piece by far.

Reference

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestgap-auditIdentified during 2026-04-25 planned-vs-shipped auditpriority:p2Medium — schedule (feature gaps, partial implementations)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions