Allow switching an unpaid Überweisung order to Stripe payment

New "Zahlungsart ändern" button on the account order detail page,
shown for a still-'received', still-manual (Überweisung) order when an
active Stripe payment method exists. Reuses PaymentStep (the same Stripe
Payment Element checkout uses) and /checkout/verarbeitung's polling logic
(both now take a returnContext prop/param to land back on the order page
instead of clearing the cart and redirecting to /bestellbestaetigung).

Also:
- Payment status badge (Offen/Bezahlt/...) next to the existing Zahlungsart
  display on the order detail page.
- A one-line mention of the switch option in the Vorkasse unpaid notice
  in the order-confirmation email, shown only when a Stripe option is
  actually active (hasOnlinePaymentOption).
- CustomerOrderDetail gained paymentProvider (was missing from the type
  entirely, even though the field already existed on the order).

Backend counterpart: docker/payload's switchPaymentToStripeEndpoint.
This commit is contained in:
Marco
2026-07-30 09:48:44 +00:00
parent 8c843c0ac1
commit 0e995884a7
9 changed files with 269 additions and 17 deletions
@@ -24,6 +24,11 @@ export function VerarbeitungContent() {
const router = useRouter();
const searchParams = useSearchParams();
const orderNumber = searchParams.get("orderNumber");
// "account" — the payment-method-switch flow on an existing order's
// account page (see PaymentStep.tsx's own returnContext comment).
// Defaults to "checkout" for a bare/missing param, same as before this
// branch existed.
const isAccountContext = searchParams.get("context") === "account";
const [state, setState] = useState<"polling" | "timeout" | "failed" | "error">(orderNumber ? "polling" : "error");
const startedAt = useRef<number | null>(null);
@@ -42,6 +47,15 @@ export function VerarbeitungContent() {
return;
}
if (data.paymentStatus === "paid") {
if (isAccountContext) {
// Nothing to clear — this order was already placed (as
// Überweisung) and confirmed long before this switch, there's
// no cart/discount/draft snapshot involved. Land back on the
// same order instead of /bestellbestaetigung, which would
// read as a brand-new purchase.
router.push(`/konto/bestellungen/${encodeURIComponent(orderNumber!)}`);
return;
}
try {
const pending = window.sessionStorage.getItem(PENDING_ORDER_KEY);
if (pending) {
@@ -114,13 +128,15 @@ export function VerarbeitungContent() {
Zahlung fehlgeschlagen
</p>
<p className="text-body text-text-muted max-w-md">
Deine Zahlung konnte nicht abgeschlossen werden. Dein Warenkorb ist noch vorhanden du kannst es gerne erneut versuchen.
{isAccountContext
? "Die Zahlung konnte nicht abgeschlossen werden. Deine Bestellung bleibt unverändert — du kannst es jederzeit erneut versuchen."
: "Deine Zahlung konnte nicht abgeschlossen werden. Dein Warenkorb ist noch vorhanden — du kannst es gerne erneut versuchen."}
</p>
<Link
href="/checkout"
href={isAccountContext ? `/konto/bestellungen/${encodeURIComponent(orderNumber ?? "")}` : "/checkout"}
className="flex items-center gap-2 px-7 py-4 rounded-sm bg-brand text-h4 font-bold text-text-primary hover:bg-brand-hover active:scale-[0.97] transition-all"
>
Zurück zum Checkout
{isAccountContext ? "Zurück zur Bestellung" : "Zurück zum Checkout"}
</Link>
</>
)}
@@ -133,10 +149,10 @@ export function VerarbeitungContent() {
Falls die Zahlung erfolgreich war, erhältst du in Kürze eine Bestätigungs-E-Mail. Andernfalls kannst du es erneut versuchen.
</p>
<Link
href="/checkout"
href={isAccountContext ? `/konto/bestellungen/${encodeURIComponent(orderNumber ?? "")}` : "/checkout"}
className="flex items-center gap-2 px-7 py-4 rounded-sm bg-brand text-h4 font-bold text-text-primary hover:bg-brand-hover active:scale-[0.97] transition-all"
>
Zurück zum Checkout
{isAccountContext ? "Zurück zur Bestellung" : "Zurück zum Checkout"}
</Link>
</>
)}