Hide failed-payment order attempts from the customer's own order history

A cancelled order with no invoiceNumber is a Stripe payment that never
succeeded (failed or timed out before ever reaching received/invoiced),
not a real cancellation of something that actually happened — from the
customer's point of view it was never really an order. Filtered out of
getCustomerOrders/getCustomerOrderDetail by default; the row stays in
Payload for admin/audit purposes (shown there as "Zahlung
fehlgeschlagen", see backend).

getCustomerOrderDetail's filter is opt-in via a new optional parameter,
not the default — /api/checkout/status/route.ts's post-payment polling
needs to keep seeing exactly this order to show the "Zahlung
fehlgeschlagen, bitte erneut versuchen" retry state. The GDPR export
route also opts out for the same reason a legal completeness export
can't silently drop rows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-25 16:05:26 +00:00
parent 0e12ab1f1e
commit bae23775f2
4 changed files with 67 additions and 7 deletions
+24 -1
View File
@@ -374,7 +374,12 @@ exactly as before: no gateway involved, order goes straight to `received`.
(`app/lib/payments/confirmPaymentEmail.ts`, only when the response isn't
`alreadyProcessed: true` — a repeat webhook delivery must never resend
it), mirroring exactly what the checkout route already does inline for
a manual/Überweisung order.
a manual/Überweisung order. Product photos in that snapshot's
`items[].imageUrl` need no frontend change to work —
`ConfirmPaymentOrderSnapshot`/`OrderConfirmationItem` already typed the
field; the backend just wasn't populating it (fixed there, see its own
README — needed `depth: 2` so `item.product.image` resolves to a real
`Media` doc).
- **`/checkout/verarbeitung`** (`VerarbeitungContent.tsx`) is the
`return_url` target. Neither a client-side `confirmPayment()` success nor
landing back from a PayPal redirect is trusted as proof of payment on its
@@ -1172,6 +1177,24 @@ ref isn't attached to anything yet at that exact synchronous point.
`cancelled`/`return_requested`/`returned`) is maintained by hand in the
Payload admin for the shipping states — no shipping-carrier API
integration.
- **A `cancelled` Stripe order with no `invoiceNumber` never shows up
here** — that's a `pending_payment` order whose payment failed or timed
out (see the backend's `expirePendingPayments`/`confirmPayment.ts`), not
a real Storno (which is always of an already-`received`, already-
invoiced order, so it always has an `invoiceNumber`). From the
customer's point of view a payment that never went through was never
really an order, so `getCustomerOrders`/`getCustomerOrderDetail`
(`app/lib/customerAuth.ts`) filter these out by default — the row still
exists in Payload for admin/audit purposes (shown there as "Zahlung
fehlgeschlagen", see the backend's own README), just not surfaced to
the customer. `getCustomerOrderDetail` takes this as an **optional**
4th param, defaulting `false``/api/checkout/status/route.ts`'s
post-payment polling deliberately calls it unfiltered, since that flow
needs to keep seeing exactly this order (to show "Zahlung
fehlgeschlagen, bitte erneut versuchen") for the one case this filter
would otherwise hide. `/api/account/export/route.ts`'s GDPR export also
opts out (`false`) — a legal completeness export can't silently drop
rows.
- **`Navbar.tsx`'s `AccountLink`** (account icon, always visible in the
header itself — not duplicated inside the mobile fullscreen menu, see
"Mobile navigation" below) is the only *always*-reachable way into