diff --git a/README.md b/README.md index 3ceffe6..55f5aa0 100644 --- a/README.md +++ b/README.md @@ -2104,9 +2104,21 @@ switching an existing Überweisung order to Stripe, not a fresh gated checkout — see the Payload backend's own README on `switchPaymentToStripeEndpoint`). Same shape as the order-status emails (icon + admin-editable heading/bodyText + "Bestellung ansehen" button via -`renderOrderStatusHtml`), no item table or invoice re-attachment — the -invoice itself didn't change, only how it got paid, and the customer -already received the full order-confirmation email once already. New +`renderOrderStatusHtml`), no item table, but **does** re-attach a freshly +generated invoice PDF — same `invoiceNumber` as always (never re-issued +for a switch), but `paymentMethodTitle` now reflects the actually- +confirmed instrument, which changes `@einfach-produktiv/invoicing`'s own +`isPaidImmediately()` check from the Vorkasse notice to "✓ Bereits +beglichen", so the customer's copy of "their invoice" should reflect +that. `buildInvoiceAttachment()` was extracted out of +`sendOrderConfirmationEmail()` so both senders share the exact same PDF- +generation call instead of duplicating that ~40-line object literal. +Default fallback copy (used whenever no admin template is saved/active) +matches the existing brand voice (`sendOrderConfirmationEmail`'s own +"Bestellt! Deine Ruhe kann kommen 🎉" fallback) rather than a flat +system-notice tone: *"Erledigt! Deine Zahlung ist da 🎉" / "Deine Zahlung +ist gerade bei uns eingetrudelt — ab jetzt läuft alles automatisch +weiter, du musst dich um nichts mehr kümmern."* New `payment-method-switched` `EmailTemplateType`, added to `/email-preview`'s valid types too. diff --git a/app/email-preview/[type]/page.tsx b/app/email-preview/[type]/page.tsx index 568db10..a1b4ecc 100644 --- a/app/email-preview/[type]/page.tsx +++ b/app/email-preview/[type]/page.tsx @@ -30,7 +30,7 @@ const STATUS_TYPE_FALLBACK_HEADING: Record = { "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 diff --git a/app/lib/orderEmail.ts b/app/lib/orderEmail.ts index ab3b66e..9e4d5bd 100644 --- a/app/lib/orderEmail.ts +++ b/app/lib/orderEmail.ts @@ -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 { - 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>, +): 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 { + 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 { +export async function sendPaymentSwitchedEmail(order: OrderConfirmationEmailData, customerEmail: string): Promise { 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; } diff --git a/app/lib/payments/confirmPaymentEmail.ts b/app/lib/payments/confirmPaymentEmail.ts index 125f476..592ff2e 100644 --- a/app/lib/payments/confirmPaymentEmail.ts +++ b/app/lib/payments/confirmPaymentEmail.ts @@ -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); }