Attach the invoice PDF to the payment-method-switched email, brand-tone copy
buildInvoiceAttachment() extracted out of sendOrderConfirmationEmail() so sendPaymentSwitchedEmail() can reuse the same invoice-PDF generation instead of duplicating it — the invoiceNumber stays the same, but paymentMethodTitle now reflects the confirmed instrument, which flips the PDF's own paid/unpaid display, so re-attaching a fresh copy matters here even though nothing else about the order changed. Default fallback copy replaced with brand-toned wording matching the existing order-confirmation fallback's voice, instead of a flat system-notice tone.
This commit is contained in:
@@ -30,7 +30,7 @@ const STATUS_TYPE_FALLBACK_HEADING: Record<string, string> = {
|
||||
"order-tracking-added": "Hier ist deine Sendungsnummer",
|
||||
"order-tracking-corrected": "Korrigierte Sendungsnummer",
|
||||
"order-delivered": "Dein Paket ist angekommen",
|
||||
"payment-method-switched": "Zahlung erhalten",
|
||||
"payment-method-switched": "Erledigt!",
|
||||
};
|
||||
|
||||
// Entered exclusively via EmailTemplates.ts's admin.livePreview.url (a
|
||||
|
||||
+51
-27
@@ -55,25 +55,20 @@ export type OrderConfirmationEmailData = OrderConfirmationData & {
|
||||
// its own try/catch and just sends without the attachment if generation
|
||||
// fails (still alerted, same severity as the frontend's own critical-error
|
||||
// path for this checkout flow).
|
||||
export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailData, customerEmail: string): Promise<boolean> {
|
||||
const fetchedTemplate = await getEmailTemplate("order-confirmation");
|
||||
// `active === false` is a deliberate admin decision to suppress this
|
||||
// email entirely — distinct from `fetchedTemplate` being null (no row
|
||||
// saved yet), which still sends below with the hardcoded default
|
||||
// wording. Checked before the fallback is applied, since the fallback
|
||||
// object has no `active` field of its own (implicitly always on).
|
||||
if (fetchedTemplate && !fetchedTemplate.active) return false;
|
||||
const template = fetchedTemplate ?? {
|
||||
subject: "Bestellt! Deine Ruhe kann kommen 🎉",
|
||||
heading: "Bestellt!",
|
||||
bodyText: "Deine Bestellung ist bei uns eingetrudelt — wir kümmern uns schon liebevoll darum, sie für dich zu packen.",
|
||||
footerText: null,
|
||||
};
|
||||
|
||||
const seller = await getSellerForInvoice();
|
||||
const html = renderOrderConfirmationHtml(template, order, seller);
|
||||
|
||||
let attachments: { filename: string; content: Buffer }[] | undefined;
|
||||
// Shared by sendOrderConfirmationEmail and sendPaymentSwitchedEmail — both
|
||||
// need to (re)generate the exact same invoice PDF for the exact same
|
||||
// order, just with a different email body wrapped around it. Regenerated
|
||||
// fresh each time rather than cached anywhere, same "deterministic
|
||||
// regeneration, not file storage" approach as the on-demand download
|
||||
// routes — this also means a switched-payment send picks up the now
|
||||
// up-to-date paymentMethodTitle, so the PDF's own "✓ Bereits beglichen"
|
||||
// vs. Vorkasse-notice branch (see @einfach-produktiv/invoicing's
|
||||
// isPaidImmediately()) reflects the real, current payment state even
|
||||
// though invoiceNumber/invoiceIssuedAt never change.
|
||||
async function buildInvoiceAttachment(
|
||||
order: OrderConfirmationEmailData,
|
||||
seller: Awaited<ReturnType<typeof getSellerForInvoice>>,
|
||||
): Promise<{ filename: string; content: Buffer }[] | undefined> {
|
||||
try {
|
||||
const pdf = await generateInvoicePdf(
|
||||
{
|
||||
@@ -125,15 +120,36 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
|
||||
},
|
||||
seller,
|
||||
);
|
||||
if (pdf) attachments = [{ filename: `Rechnung-${order.invoiceNumber}.pdf`, content: pdf }];
|
||||
else throw new Error("generateInvoicePdf returned null (missing invoice-settings?)");
|
||||
if (!pdf) throw new Error("generateInvoicePdf returned null (missing invoice-settings?)");
|
||||
return [{ filename: `Rechnung-${order.invoiceNumber}.pdf`, content: pdf }];
|
||||
} catch (err) {
|
||||
sendCriticalAlert("Rechnungs-PDF konnte nicht erzeugt werden", {
|
||||
orderNumber: order.orderNumber,
|
||||
invoiceNumber: order.invoiceNumber,
|
||||
error: String(err),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailData, customerEmail: string): Promise<boolean> {
|
||||
const fetchedTemplate = await getEmailTemplate("order-confirmation");
|
||||
// `active === false` is a deliberate admin decision to suppress this
|
||||
// email entirely — distinct from `fetchedTemplate` being null (no row
|
||||
// saved yet), which still sends below with the hardcoded default
|
||||
// wording. Checked before the fallback is applied, since the fallback
|
||||
// object has no `active` field of its own (implicitly always on).
|
||||
if (fetchedTemplate && !fetchedTemplate.active) return false;
|
||||
const template = fetchedTemplate ?? {
|
||||
subject: "Bestellt! Deine Ruhe kann kommen 🎉",
|
||||
heading: "Bestellt!",
|
||||
bodyText: "Deine Bestellung ist bei uns eingetrudelt — wir kümmern uns schon liebevoll darum, sie für dich zu packen.",
|
||||
footerText: null,
|
||||
};
|
||||
|
||||
const seller = await getSellerForInvoice();
|
||||
const html = renderOrderConfirmationHtml(template, order, seller);
|
||||
const attachments = await buildInvoiceAttachment(order, seller);
|
||||
|
||||
await transport.sendMail({
|
||||
// SPF confirmed 2026-07-29 for einfach-produktiv.com, and the SMTP
|
||||
@@ -165,13 +181,13 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
|
||||
// (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> {
|
||||
export async function sendPaymentSwitchedEmail(order: OrderConfirmationEmailData, 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.",
|
||||
subject: "Erledigt! Deine Zahlung ist da 🎉",
|
||||
heading: "Erledigt!",
|
||||
bodyText: "Deine Zahlung ist gerade bei uns eingetrudelt — ab jetzt läuft alles automatisch weiter, du musst dich um nichts mehr kümmern. Deine aktualisierte Rechnung findest du im Anhang.",
|
||||
footerText: null,
|
||||
};
|
||||
|
||||
@@ -179,10 +195,17 @@ export async function sendPaymentSwitchedEmail(orderNumber: string, customerEmai
|
||||
const html = renderOrderStatusHtml(
|
||||
template,
|
||||
"💳",
|
||||
orderNumber,
|
||||
`https://einfach-produktiv.mk360.de/konto/bestellungen/${encodeURIComponent(orderNumber)}`,
|
||||
order.orderNumber,
|
||||
`https://einfach-produktiv.mk360.de/konto/bestellungen/${encodeURIComponent(order.orderNumber)}`,
|
||||
seller,
|
||||
);
|
||||
// Same invoiceNumber as always (never re-issued for a switch, see
|
||||
// confirmPayment.ts), but paymentMethodTitle now reflects the actually-
|
||||
// confirmed instrument — worth a fresh PDF, not the original attachment,
|
||||
// since @einfach-produktiv/invoicing's own isPaidImmediately() check
|
||||
// reads that title to decide "✓ Bereits beglichen" vs. the Vorkasse
|
||||
// notice.
|
||||
const attachments = await buildInvoiceAttachment(order, seller);
|
||||
|
||||
await transport.sendMail({
|
||||
from: `"${seller?.emailFromName || seller?.sellerName || "Björn"}" <${seller?.emailFromAddress || seller?.sellerEmail || "hallo@einfach-produktiv.com"}>`,
|
||||
@@ -190,6 +213,7 @@ export async function sendPaymentSwitchedEmail(orderNumber: string, customerEmai
|
||||
to: customerEmail,
|
||||
subject: template.subject,
|
||||
html,
|
||||
attachments,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function sendConfirmedPaymentEmail(order: ConfirmPaymentOrderSnapsh
|
||||
const { customerEmail, ...emailData } = order;
|
||||
try {
|
||||
if (order.paymentSwitchedAt) {
|
||||
await sendPaymentSwitchedEmail(order.orderNumber, customerEmail);
|
||||
await sendPaymentSwitchedEmail(emailData, customerEmail);
|
||||
} else {
|
||||
await sendOrderConfirmationEmail(emailData, customerEmail);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user