Consolidate Kreditkarte/PayPal into one "Online-Zahlung" checkout option

Both already route through the same Stripe PaymentIntent
(automatic_payment_methods: enabled — Stripe's own recommended Payment
Element pattern, letting Stripe itself decide which eligible method to
show). Pre-selecting one of two identical-behind-the-scenes rows before
the payment step was redundant friction, not a real choice. Collapses
them into one option with a hint text explaining the actual instrument
is picked on the next screen; Überweisung is unaffected.

Also refines paymentMethodTitle from a neutral "Online-Zahlung"
placeholder (snapshotted at order-creation time, before the customer has
picked an instrument) to the real one Stripe reports, once payment
confirms — carried through to both the stored order and the
sessionStorage snapshot shown on /bestellbestaetigung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-25 12:17:38 +00:00
parent 4e22942031
commit bab2c916be
8 changed files with 152 additions and 27 deletions
+34 -4
View File
@@ -308,6 +308,30 @@ exactly as before: no gateway involved, order goes straight to `received`.
branch — `'manual'` (Überweisung) or `'stripe'` (Kreditkarte/PayPal).
`app/lib/payload.ts`'s `getPaymentMethods()` exposes it; the checkout
route re-resolves it server-side, never trusts a client-submitted value.
- **Checkout UI collapses Kreditkarte + PayPal into one "Online-Zahlung"
option** (`groupPaymentMethodsForCheckout()` in `app/lib/payload.ts`,
used by `CheckoutContent.tsx`). Both admin rows still exist and both
still need `provider: 'stripe'` — this is a display-layer grouping, not
a data change. Reasoning: the PaymentIntent is created with
`automatic_payment_methods: { enabled: true }` (Stripe's own recommended
Payment Element pattern — Stripe itself decides which eligible method to
show), so pre-selecting "Kreditkarte" vs. "PayPal" before that never
actually restricted anything; it was redundant friction, not a real
choice. The combined option shows a hint text ("die genaue Zahlungsart
wählst du im nächsten Schritt") so the consolidation reads as intentional,
not a missing option. Überweisung stays a separate, real option.
- **`paymentMethodTitle` is snapshotted as a neutral `"Online-Zahlung"`**
at order-creation time for the `stripe` branch (the customer hasn't
picked an instrument yet at that point) and **refined to the real one**
(`"Kreditkarte"`/`"PayPal"`) once Stripe reports it —
`resolveStripePaymentMethodLabel()` in `stripeProvider.ts` reads the
confirmed PaymentIntent's `payment_method.type` in the webhook route and
passes it to `confirm-payment` as an optional field. Best-effort: an
unresolved label just leaves the neutral title in place. The
`/checkout/verarbeitung` polling page also patches this into the
provisional `sessionStorage` snapshot before promoting it, so
`/bestellbestaetigung` shows the real instrument too, not the neutral
placeholder.
- **`app/api/checkout/route.ts`, `provider === 'stripe'` branch**: creates
a Stripe PaymentIntent *before* the order (`app/lib/payments/
stripeProvider.ts`) — its id is known immediately and gets persisted as
@@ -337,10 +361,16 @@ exactly as before: no gateway involved, order goes straight to `received`.
`{paymentStatus, providerReference, paidAt}`. Returns a non-2xx status on
any internal failure so Stripe's own retry schedule (~3 days) provides
resilience for free, rather than this app building its own retry queue.
That backend endpoint is what actually flips the order to `received`,
assigns the (until-then-deferred) invoice number, and sends the
confirmation email/invoice + the internal admin new-order notification —
see the backend repo's own README for that half.
That backend endpoint flips the order to `received`, assigns the
(until-then-deferred) invoice number, and queues the internal admin
new-order notification — see the backend repo's own README for that
half. It has no SMTP sender of its own, though: it returns a full order
snapshot in its response instead, and **this webhook route is what
actually sends the confirmation email + invoice PDF**
(`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.
- **`/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