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:
@@ -0,0 +1,73 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSessionCustomer, getCustomerOrderDetail } from "../../../../../lib/customerAuth";
|
||||
import { getPaymentMethods, groupPaymentMethodsForCheckout } from "../../../../../lib/payload";
|
||||
import { paymentProvider, isPaymentTestMode } from "../../../../../lib/payments";
|
||||
|
||||
const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de";
|
||||
const PAYMENT_WEBHOOK_SECRET = process.env.PAYMENT_WEBHOOK_SECRET || "";
|
||||
|
||||
// Lets a logged-in customer move an existing, still-unpaid Überweisung
|
||||
// order onto a Stripe PaymentIntent instead of waiting on their own bank
|
||||
// transfer — see the backend's switchPaymentToStripe.ts for the matching
|
||||
// endpoint and why this needs a dedicated backend route rather than the
|
||||
// generic customer-JWT order-PATCH path (paymentProvider/paymentStatus
|
||||
// are system fields a customer JWT can never touch).
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ orderNumber: string }> }) {
|
||||
const session = await getSessionCustomer();
|
||||
if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 });
|
||||
|
||||
const { orderNumber } = await params;
|
||||
const order = await getCustomerOrderDetail(session.token, session.customer.id, decodeURIComponent(orderNumber));
|
||||
if (!order) return NextResponse.json({ ok: false, reason: "Bestellung nicht gefunden." }, { status: 404 });
|
||||
|
||||
// Same eligibility the backend endpoint re-checks authoritatively —
|
||||
// checked here too for a friendly error instead of a bare 409.
|
||||
if (order.paymentProvider !== "manual" || order.status !== "received") {
|
||||
return NextResponse.json({ ok: false, reason: "Die Zahlungsart kann für diese Bestellung gerade nicht geändert werden." }, { status: 400 });
|
||||
}
|
||||
|
||||
// Only offer this when a real Stripe payment method is actually active
|
||||
// — same "Online-Zahlung" grouping the checkout itself uses, so this
|
||||
// never presents an option that checkout wouldn't currently accept either.
|
||||
const methods = groupPaymentMethodsForCheckout(await getPaymentMethods());
|
||||
const hasStripeOption = methods.some((m) => m.provider === "stripe");
|
||||
if (!hasStripeOption) {
|
||||
return NextResponse.json({ ok: false, reason: "Aktuell steht keine Online-Zahlung zur Verfügung." }, { status: 400 });
|
||||
}
|
||||
|
||||
let intent: { clientSecret: string; providerReference: string };
|
||||
try {
|
||||
intent = await paymentProvider.createPaymentIntent({
|
||||
amountCents: Math.round(order.total * 100),
|
||||
currency: "eur",
|
||||
customerEmail: order.customerEmail,
|
||||
description: `Bestellung ${order.orderNumber}`,
|
||||
});
|
||||
} catch (err) {
|
||||
return NextResponse.json({ ok: false, reason: "Zahlung konnte nicht vorbereitet werden." }, { status: 500 });
|
||||
}
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/orders/${order.id}/switch-payment-to-stripe`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "x-payment-webhook-secret": PAYMENT_WEBHOOK_SECRET },
|
||||
body: JSON.stringify({ providerReference: intent.providerReference }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
return NextResponse.json({ ok: false, reason: data?.reason ?? "Umstellung fehlgeschlagen." }, { status: 400 });
|
||||
}
|
||||
|
||||
// Best-effort, same as checkout's own call — a failure here doesn't
|
||||
// block the payment itself, only the confirm-payment webhook's metadata
|
||||
// lookup, which the frontend's own webhook route already alerts on.
|
||||
await paymentProvider.attachOrderMetadata(intent.providerReference, { orderId: String(order.id), orderNumber: order.orderNumber }).catch(() => {});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
clientSecret: intent.clientSecret,
|
||||
orderId: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
testMode: isPaymentTestMode,
|
||||
...(isPaymentTestMode ? { providerReference: intent.providerReference } : {}),
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { CartItem } from "../../lib/cart";
|
||||
import { getShippingMethods, getPaymentMethods, getCompanySettings } from "../../lib/payload";
|
||||
import { getShippingMethods, getPaymentMethods, getCompanySettings, groupPaymentMethodsForCheckout } from "../../lib/payload";
|
||||
import { validateDiscountCode, redeemDiscountCode } from "../../lib/discountServer";
|
||||
import { createOrder } from "../../lib/orderServer";
|
||||
import { getSessionCustomer, registerCustomer, setSessionCookie, type CustomerSummary } from "../../lib/customerAuth";
|
||||
@@ -467,6 +467,9 @@ export async function POST(request: Request) {
|
||||
discountCode: body.discountCode || null,
|
||||
total,
|
||||
isManualPayment: true,
|
||||
// Only meaningful for the Vorkasse notice above — whether a
|
||||
// switch to Kreditkarte/PayPal is even worth mentioning right now.
|
||||
hasOnlinePaymentOption: groupPaymentMethodsForCheckout(paymentMethods).some((m) => m.provider === "stripe"),
|
||||
},
|
||||
body.email,
|
||||
).catch((err) => {
|
||||
|
||||
@@ -24,25 +24,43 @@ type Props = {
|
||||
testMode: boolean;
|
||||
/** Only present in test mode — see api/checkout/route.ts's own comment. */
|
||||
providerReference?: string;
|
||||
/** Where /checkout/verarbeitung sends the customer once payment is
|
||||
* confirmed — "checkout" (default) clears the cart/draft and lands on
|
||||
* /bestellbestaetigung, exactly like today. "account" is used by the
|
||||
* account order detail page's "Zahlungsart ändern" flow (an existing,
|
||||
* already-confirmed order — nothing to clear, and /bestellbestaetigung
|
||||
* would be the wrong destination): lands back on that same order's
|
||||
* page instead. See VerarbeitungContent.tsx's own branching on this. */
|
||||
returnContext?: "checkout" | "account";
|
||||
};
|
||||
|
||||
// Rendered by CheckoutContent once /api/checkout returns
|
||||
// `requiresPayment: true` (Kreditkarte/PayPal) — see
|
||||
// spicy-leaping-pizza.md §3/§7. The order already exists in Payload at
|
||||
// this point (status 'pending_payment'); this step only collects/confirms
|
||||
// the actual payment, it doesn't create anything.
|
||||
export function PaymentStep({ clientSecret, orderNumber, orderId, testMode, providerReference }: Props) {
|
||||
// the actual payment, it doesn't create anything. Also reused as-is by
|
||||
// the account order detail page's payment-method-switch flow (see
|
||||
// returnContext above) — the Stripe collection UI itself is identical
|
||||
// either way, only the post-payment destination differs.
|
||||
export function PaymentStep({ clientSecret, orderNumber, orderId, testMode, providerReference, returnContext = "checkout" }: Props) {
|
||||
if (testMode) {
|
||||
return <TestPaymentButtons orderNumber={orderNumber} orderId={orderId} providerReference={providerReference ?? ""} />;
|
||||
return (
|
||||
<TestPaymentButtons
|
||||
orderNumber={orderNumber}
|
||||
orderId={orderId}
|
||||
providerReference={providerReference ?? ""}
|
||||
returnContext={returnContext}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Elements stripe={getStripe()} options={{ clientSecret }}>
|
||||
<StripePaymentForm orderNumber={orderNumber} />
|
||||
<StripePaymentForm orderNumber={orderNumber} returnContext={returnContext} />
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
|
||||
function StripePaymentForm({ orderNumber }: { orderNumber: string }) {
|
||||
function StripePaymentForm({ orderNumber, returnContext }: { orderNumber: string; returnContext: "checkout" | "account" }) {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -62,7 +80,7 @@ function StripePaymentForm({ orderNumber }: { orderNumber: string }) {
|
||||
const { error: confirmError } = await stripe.confirmPayment({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `${window.location.origin}/checkout/verarbeitung?orderNumber=${encodeURIComponent(orderNumber)}`,
|
||||
return_url: `${window.location.origin}/checkout/verarbeitung?orderNumber=${encodeURIComponent(orderNumber)}&context=${returnContext}`,
|
||||
},
|
||||
});
|
||||
// Only reached for immediate client-side failures (e.g. invalid card
|
||||
@@ -88,7 +106,17 @@ function StripePaymentForm({ orderNumber }: { orderNumber: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TestPaymentButtons({ orderNumber, orderId, providerReference }: { orderNumber: string; orderId: number; providerReference: string }) {
|
||||
function TestPaymentButtons({
|
||||
orderNumber,
|
||||
orderId,
|
||||
providerReference,
|
||||
returnContext,
|
||||
}: {
|
||||
orderNumber: string;
|
||||
orderId: number;
|
||||
providerReference: string;
|
||||
returnContext: "checkout" | "account";
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [submitting, setSubmitting] = useState<"paid" | "failed" | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -108,7 +136,7 @@ function TestPaymentButtons({ orderNumber, orderId, providerReference }: { order
|
||||
setSubmitting(null);
|
||||
return;
|
||||
}
|
||||
router.push(`/checkout/verarbeitung?orderNumber=${encodeURIComponent(orderNumber)}`);
|
||||
router.push(`/checkout/verarbeitung?orderNumber=${encodeURIComponent(orderNumber)}&context=${returnContext}`);
|
||||
} catch {
|
||||
setError("Testzahlung konnte nicht ausgeführt werden.");
|
||||
setSubmitting(null);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { PaymentStep } from "../../../../checkout/components/PaymentStep";
|
||||
|
||||
// Shown only for a still-unpaid Überweisung order (see page.tsx's own
|
||||
// eligibility check, mirroring api/account/orders/[orderNumber]/
|
||||
// switch-to-stripe/route.ts's authoritative one) — lets a customer switch
|
||||
// to Kreditkarte/PayPal instead of waiting on their own bank transfer.
|
||||
// Reuses PaymentStep (the exact same Stripe collection UI checkout uses)
|
||||
// once this endpoint hands back a clientSecret — the order already
|
||||
// exists, this only changes how it gets paid.
|
||||
export function SwitchPaymentButton({ orderNumber }: { orderNumber: string }) {
|
||||
const [state, setState] = useState<
|
||||
| { step: "idle" }
|
||||
| { step: "loading" }
|
||||
| { step: "error"; reason: string }
|
||||
| { step: "paying"; clientSecret: string; orderId: number; testMode: boolean; providerReference?: string }
|
||||
>({ step: "idle" });
|
||||
|
||||
async function start() {
|
||||
setState({ step: "loading" });
|
||||
try {
|
||||
const res = await fetch(`/api/account/orders/${encodeURIComponent(orderNumber)}/switch-to-stripe`, {
|
||||
method: "POST",
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
setState({ step: "error", reason: data.reason || "Umstellung fehlgeschlagen." });
|
||||
return;
|
||||
}
|
||||
setState({
|
||||
step: "paying",
|
||||
clientSecret: data.clientSecret,
|
||||
orderId: data.orderId,
|
||||
testMode: Boolean(data.testMode),
|
||||
providerReference: data.providerReference,
|
||||
});
|
||||
} catch {
|
||||
setState({ step: "error", reason: "Umstellung gerade nicht möglich." });
|
||||
}
|
||||
}
|
||||
|
||||
if (state.step === "paying") {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full border border-border rounded-md p-5">
|
||||
<p className="font-semibold text-body-sm text-text-primary">Mit Kreditkarte/PayPal bezahlen</p>
|
||||
<PaymentStep
|
||||
clientSecret={state.clientSecret}
|
||||
orderNumber={orderNumber}
|
||||
orderId={state.orderId}
|
||||
testMode={state.testMode}
|
||||
providerReference={state.providerReference}
|
||||
returnContext="account"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 items-start">
|
||||
<button
|
||||
type="button"
|
||||
onClick={start}
|
||||
disabled={state.step === "loading"}
|
||||
className={`px-5 py-3 rounded-sm border border-border hover:border-brand font-bold text-body-sm text-text-primary transition-colors ${state.step === "loading" ? "opacity-70 pointer-events-none" : ""}`}
|
||||
>
|
||||
{state.step === "loading" ? "…" : "Zahlungsart ändern"}
|
||||
</button>
|
||||
{state.step === "error" && <p className="text-label text-red-600">{state.reason}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,11 +7,13 @@ import { Footer } from "../../../components/Footer";
|
||||
import { VatBreakdown } from "../../../components/VatBreakdown";
|
||||
import { formatPrice, formatDate } from "../../../lib/format";
|
||||
import { getSessionCustomer, getCustomerOrderDetail, customerOrderAction } from "../../../lib/customerAuth";
|
||||
import { getProductImagesByIds } from "../../../lib/payload";
|
||||
import { getProductImagesByIds, getPaymentMethods, groupPaymentMethodsForCheckout } from "../../../lib/payload";
|
||||
import { computeTaxBreakdown } from "@einfach-produktiv/invoicing";
|
||||
import { buildTrackingUrl, CARRIER_LABELS } from "../../../lib/tracking";
|
||||
import { OrderActionButton } from "./components/OrderActionButton";
|
||||
import { SwitchPaymentButton } from "./components/SwitchPaymentButton";
|
||||
import { OrderStatusBadge } from "../../components/OrderStatusBadge";
|
||||
import { PaymentStatusBadge } from "../../components/PaymentStatusBadge";
|
||||
|
||||
// Dynamic (was a static "Bestelldetails" title despite this being a
|
||||
// per-order route) — just formats the already-known order number into
|
||||
@@ -47,6 +49,13 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
|
||||
const action = customerOrderAction(order.status);
|
||||
const imagesByProductId = await getProductImagesByIds(order.items.map((item) => item.product));
|
||||
const taxBreakdown = computeTaxBreakdown(order.items, order.subtotal, order.discountAmount, order.shippingCost);
|
||||
// Same "Online-Zahlung" grouping/eligibility the switch-to-stripe route
|
||||
// itself re-checks authoritatively — only offer the button when it
|
||||
// would actually succeed.
|
||||
const canSwitchPayment =
|
||||
order.paymentProvider === "manual" &&
|
||||
order.status === "received" &&
|
||||
groupPaymentMethodsForCheckout(await getPaymentMethods()).some((m) => m.provider === "stripe");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -73,8 +82,14 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
|
||||
<p className="text-label text-text-muted">Zahlungsart</p>
|
||||
<p className="text-body-sm text-text-primary">{order.paymentMethodTitle}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-label text-text-muted">Zahlungsstatus</p>
|
||||
<PaymentStatusBadge paymentStatus={order.paymentStatus} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canSwitchPayment && <SwitchPaymentButton orderNumber={order.orderNumber} />}
|
||||
|
||||
{order.trackingNumber && (
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<p className="text-label text-text-muted">Sendungsverfolgung{order.carrier ? ` (${CARRIER_LABELS[order.carrier] ?? order.carrier})` : ""}</p>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Same visual pattern as OrderStatusBadge — but answers a different
|
||||
// question ("hat Stripe/die Buchhaltung eine Zahlung bestätigt?", not
|
||||
// "wo im Fulfillment steht die Bestellung"). 'not_applicable' (an
|
||||
// Überweisung order before payment is manually reconciled) reads as
|
||||
// "offen", same as a still-'pending' Stripe order — the customer doesn't
|
||||
// need to know the internal distinction between the two.
|
||||
const LABEL: Record<string, string> = {
|
||||
not_applicable: "Offen",
|
||||
pending: "Offen",
|
||||
paid: "Bezahlt",
|
||||
failed: "Fehlgeschlagen",
|
||||
refunded: "Erstattet",
|
||||
partially_refunded: "Teilweise erstattet",
|
||||
};
|
||||
|
||||
const STYLES: Record<string, string> = {
|
||||
not_applicable: "bg-bg-muted text-text-muted",
|
||||
pending: "bg-bg-muted text-text-muted",
|
||||
paid: "bg-success-subtle text-success",
|
||||
failed: "bg-red-50 text-red-600",
|
||||
refunded: "bg-bg-muted text-text-light",
|
||||
partially_refunded: "bg-orange-50 text-orange-600",
|
||||
};
|
||||
|
||||
export function PaymentStatusBadge({ paymentStatus }: { paymentStatus: string }) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-1 rounded-full text-label font-bold whitespace-nowrap ${STYLES[paymentStatus] ?? "bg-bg-muted text-text-muted"}`}
|
||||
>
|
||||
{LABEL[paymentStatus] ?? paymentStatus}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -523,6 +523,10 @@ export async function getCustomerOrders(token: string, customerId: number, exclu
|
||||
|
||||
export type CustomerOrderDetail = CustomerOrder & {
|
||||
id: number;
|
||||
// 'manual' (Überweisung) vs 'stripe' (Kreditkarte/PayPal) — see
|
||||
// api/account/orders/[orderNumber]/switch-to-stripe/route.ts, which only
|
||||
// offers a payment-method switch for a still-'manual' order.
|
||||
paymentProvider: "manual" | "stripe";
|
||||
// 'not_applicable' for Überweisung orders (never gated); see
|
||||
// spicy-leaping-pizza.md §1 — read by /api/checkout/status for the
|
||||
// post-Stripe-redirect polling page.
|
||||
|
||||
@@ -74,7 +74,7 @@ function escapeHtml(s: string): string {
|
||||
// this template (16px) — not squeezed into the narrow Gesamtsumme table
|
||||
// like an initial draft of the invoice version was before that got
|
||||
// widened per feedback.
|
||||
function vorkasseNotice(orderNumber: string, seller: CompanySettings | null): string {
|
||||
function vorkasseNotice(orderNumber: string, seller: CompanySettings | null, hasOnlinePaymentOption: boolean): string {
|
||||
const bankLine = seller && (seller.iban || seller.bic)
|
||||
? [seller.bankName, seller.iban && `IBAN ${seller.iban}`, seller.bic && `BIC ${seller.bic}`].filter(Boolean).join(" · ")
|
||||
: null;
|
||||
@@ -85,6 +85,7 @@ function vorkasseNotice(orderNumber: string, seller: CompanySettings | null): st
|
||||
<p style="margin:0 0 8px;">Bitte überweise den Rechnungsbetrag unter Angabe der Bestellnummer ${escapeHtml(orderNumber)} auf ${bankLine ? "folgende Bankverbindung:" : "die dir genannte Bankverbindung."}</p>
|
||||
${bankLine ? `<p style="margin:0 0 12px;font-weight:700;color:${TEXT_PRIMARY};">${escapeHtml(bankLine)}</p>` : ""}
|
||||
<p style="margin:0;">Deine Bestellung wird nach Zahlungseingang bearbeitet (in der Regel innerhalb von 1–2 Werktagen).</p>
|
||||
${hasOnlinePaymentOption ? `<p style="margin:8px 0 0;">Zahlungsart geändert? Solange die Überweisung noch nicht bei uns eingegangen ist, kannst du in deinem Konto jederzeit auf Kreditkarte/PayPal umsteigen.</p>` : ""}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -215,6 +216,12 @@ export type OrderConfirmationData = {
|
||||
// fragile thing a payment-methods rename already broke once this
|
||||
// session (see @einfach-produktiv/invoicing's isPaidImmediately()).
|
||||
isManualPayment: boolean;
|
||||
// Only meaningful when isManualPayment is true — whether at least one
|
||||
// Stripe-backed payment method is currently active, so the Vorkasse
|
||||
// notice can mention the option to switch instead of promising it
|
||||
// unconditionally. Set by the checkout route from the same
|
||||
// getPaymentMethods() call it already makes.
|
||||
hasOnlinePaymentOption?: boolean;
|
||||
};
|
||||
|
||||
export const SAMPLE_ORDER: OrderConfirmationData = {
|
||||
@@ -298,7 +305,7 @@ export function renderOrderConfirmationHtml(template: EmailTemplateContent, orde
|
||||
</tr>
|
||||
${taxRows}
|
||||
</table>
|
||||
${order.isManualPayment ? vorkasseNotice(order.orderNumber, seller) : ""}
|
||||
${order.isManualPayment ? vorkasseNotice(order.orderNumber, seller, Boolean(order.hasOnlinePaymentOption)) : ""}
|
||||
`;
|
||||
|
||||
return emailShell("✓", escapeHtml(template.heading), body, template.footerText, buildLegalFooterLines(seller));
|
||||
|
||||
Reference in New Issue
Block a user