Send a dedicated email for a switched Überweisung payment, not the full order-confirmation

New sendPaymentSwitchedEmail() — sent instead of sendOrderConfirmationEmail()
when confirmPaymentEmail.ts sees order.paymentSwitchedAt set. Resending the
full order-confirmation (with its invoice re-attached) after a payment-method
switch read like a brand-new purchase; this is a short, dedicated "Zahlung
erhalten" confirmation instead, matching the order-status-email shape.

Also bumps @einfach-produktiv/invoicing to 0.2.8, fixing a font-path bug
that broke every invoice PDF render inside the Payload backend specifically.
This commit is contained in:
Marco
2026-07-30 11:04:27 +00:00
parent 2e537bc78d
commit f0b2d21989
7 changed files with 85 additions and 7 deletions
+1
View File
@@ -335,6 +335,7 @@ export const ORDER_STATUS_EMAIL_ICON: Record<string, string> = {
"order-tracking-added": "📦",
"order-tracking-corrected": "📦",
"order-delivered": "🎉",
"payment-method-switched": "💳",
};
export function renderOrderStatusHtml(
+40 -1
View File
@@ -1,6 +1,6 @@
import { transport } from "./mailer";
import { getEmailTemplate } from "./payload";
import { renderOrderConfirmationHtml, type OrderConfirmationData } from "./emailTemplates";
import { renderOrderConfirmationHtml, renderOrderStatusHtml, type OrderConfirmationData } from "./emailTemplates";
import { generateInvoicePdf, getSellerForInvoice } from "./invoiceData";
import { sendCriticalAlert } from "./alertAdmin";
@@ -154,3 +154,42 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
});
return true;
}
// Sent instead of sendOrderConfirmationEmail() when the confirmed payment
// came from switchPaymentToStripe.ts (an existing Überweisung order the
// customer moved to Kreditkarte/PayPal), not a fresh checkout — see
// confirmPaymentEmail.ts's own branch on order.paymentSwitchedAt. A second
// full "Vielen Dank für deine Bestellung!" email (with its own invoice PDF
// re-attached) would read as a brand-new purchase; this is a short,
// dedicated confirmation instead, same shape as the order-status emails
// (icon + admin-editable text + "Bestellung ansehen" button), no item
// table/invoice attachment — the invoice itself didn't change, only how
// it got paid.
export async function sendPaymentSwitchedEmail(orderNumber: string, customerEmail: string): Promise<boolean> {
const fetchedTemplate = await getEmailTemplate("payment-method-switched");
if (fetchedTemplate && !fetchedTemplate.active) return false;
const template = fetchedTemplate ?? {
subject: "Zahlung erhalten — danke!",
heading: "Zahlung erhalten",
bodyText: "Deine Zahlung per Kreditkarte/PayPal ist bei uns eingegangen. An deiner Bestellung selbst ändert sich nichts — sie wird wie gewohnt bearbeitet.",
footerText: null,
};
const seller = await getSellerForInvoice();
const html = renderOrderStatusHtml(
template,
"💳",
orderNumber,
`https://einfach-produktiv.mk360.de/konto/bestellungen/${encodeURIComponent(orderNumber)}`,
seller,
);
await transport.sendMail({
from: `"${seller?.emailFromName || seller?.sellerName || "Björn"}" <${seller?.emailFromAddress || seller?.sellerEmail || "hallo@einfach-produktiv.com"}>`,
replyTo: seller?.sellerEmail || undefined,
to: customerEmail,
subject: template.subject,
html,
});
return true;
}
+2 -1
View File
@@ -838,7 +838,8 @@ export type EmailTemplateType =
| "order-returned"
| "order-tracking-added"
| "order-tracking-corrected"
| "order-delivered";
| "order-delivered"
| "payment-method-switched";
type PayloadEmailTemplate = {
type: EmailTemplateType;
+19 -3
View File
@@ -1,4 +1,4 @@
import { sendOrderConfirmationEmail, type OrderConfirmationEmailData } from "../orderEmail";
import { sendOrderConfirmationEmail, sendPaymentSwitchedEmail, type OrderConfirmationEmailData } from "../orderEmail";
import { sendCriticalAlert } from "../alertAdmin";
// The `order` snapshot returned by the backend's confirm-payment endpoint
@@ -9,7 +9,13 @@ import { sendCriticalAlert } from "../alertAdmin";
// templates), so it returns everything needed here instead of the
// frontend needing an authenticated order-read path it doesn't otherwise
// have (ORDER_SERVICE_SECRET only ever authorizes *creating* an order).
export type ConfirmPaymentOrderSnapshot = OrderConfirmationEmailData & { customerEmail: string };
export type ConfirmPaymentOrderSnapshot = OrderConfirmationEmailData & {
customerEmail: string;
// Present only when this payment came from switchPaymentToStripe.ts (an
// existing Überweisung order moved to Stripe) — see this function's own
// branch below.
paymentSwitchedAt?: string;
};
// Called from both the real Stripe webhook route and its PAYMENT_TEST_MODE
// test-confirm sibling, right after confirm-payment reports success (and
@@ -17,10 +23,20 @@ export type ConfirmPaymentOrderSnapshot = OrderConfirmationEmailData & { custome
// this). Mirrors exactly what app/api/checkout/route.ts already does for
// a manual/Überweisung order today, just triggered from the payment
// webhook instead of the checkout request itself for gated methods.
//
// A payment-method switch (paymentSwitchedAt set) sends a short dedicated
// "Zahlung erhalten" confirmation instead — the customer already got the
// full order-confirmation email (with its invoice) when they originally
// placed the Überweisung order; resending that same email here would read
// as a second, brand-new purchase.
export async function sendConfirmedPaymentEmail(order: ConfirmPaymentOrderSnapshot): Promise<void> {
const { customerEmail, ...emailData } = order;
try {
await sendOrderConfirmationEmail(emailData, customerEmail);
if (order.paymentSwitchedAt) {
await sendPaymentSwitchedEmail(order.orderNumber, customerEmail);
} else {
await sendOrderConfirmationEmail(emailData, customerEmail);
}
} catch (err) {
sendCriticalAlert("Bestätigungs-Mail konnte nach Zahlungsbestätigung nicht gesendet werden", {
orderNumber: order.orderNumber,