import { formatPrice } from "@/lib/format"; import { getSiteUrl } from "@/lib/url"; import { getCompanyInfo } from "@/lib/company-info"; import { getEmailStrings } from "@/lib/email-strings"; import type { Locale } from "@/i18n/config"; interface EmailContent { subject: string; html: string; text: string; } /** * Gemeinsamer Rahmen fuer alle Transaktions-E-Mails: bewusst mit * Inline-Styles und Tabellen-Layout statt Tailwind/externem CSS, da viele * E-Mail-Clients (Outlook, ältere Gmail-Ansichten) beides nicht zuverlaessig * unterstuetzen. */ async function emailLayout(locale: Locale, preheader: string, bodyHtml: string): Promise { const company = await getCompanyInfo(); return ` ${preheader}
WABENHAIN
HONIG AUS DEN WESTLICHEN WÄLDERN
${bodyHtml}
${company.name} · ${company.address.street}, ${company.address.zip} ${company.address.city}
`; } function button(url: string, label: string): string { return `${label}`; } interface OrderConfirmationInput { locale: Locale; orderNumber: string; firstName: string; fulfillmentMethod: "VERSAND" | "ABHOLUNG"; items: { productName: string; quantity: number; weightGrams: number; totalCents: number }[]; subtotalCents: number; shippingCents: number; discountCents?: number; totalCents: number; street: string; zip: string; city: string; giftMessage?: string | null; trackingUrl?: string; } export async function orderConfirmationEmail(order: OrderConfirmationInput): Promise { const s = getEmailStrings(order.locale).orderConfirmation; const rows = order.items .map( (item) => `${item.quantity} × ${item.productName} (${item.weightGrams} g)${formatPrice(item.totalCents, "EUR", order.locale)}` ) .join(""); const deliveryHtml = order.fulfillmentMethod === "VERSAND" ? `

${s.deliveryTo}
${order.street}
${order.zip} ${order.city}

` : `

${s.pickupNote}

`; const giftHtml = order.giftMessage ? `

${s.giftNote}

` : ""; const html = await emailLayout( order.locale, s.preheader(order.orderNumber), `

${s.heading(order.firstName)}

${s.intro(order.orderNumber)}

${rows}
${order.discountCents ? `` : ""}
${s.subtotal}${formatPrice(order.subtotalCents, "EUR", order.locale)}
${s.shipping}${order.shippingCents === 0 ? s.freeShipping : formatPrice(order.shippingCents, "EUR", order.locale)}
${s.discount}−${formatPrice(order.discountCents, "EUR", order.locale)}
${s.total}${formatPrice(order.totalCents, "EUR", order.locale)}
${deliveryHtml} ${giftHtml} ${order.trackingUrl ? `
${button(order.trackingUrl, s.trackOrder)}
` : ""} ` ); const text = `${s.heading(order.firstName)}\n\n${order.orderNumber}\n\n${order.items .map((i) => `${i.quantity} x ${i.productName} (${i.weightGrams} g) — ${formatPrice(i.totalCents, "EUR", order.locale)}`) .join("\n")}\n\n${s.subtotal}: ${formatPrice(order.subtotalCents, "EUR", order.locale)}\n${s.shipping}: ${ order.shippingCents === 0 ? s.freeShipping : formatPrice(order.shippingCents, "EUR", order.locale) }${order.discountCents ? `\n${s.discount}: −${formatPrice(order.discountCents, "EUR", order.locale)}` : ""}\n${s.total}: ${formatPrice(order.totalCents, "EUR", order.locale)}${ order.giftMessage ? `\n\n${s.giftNote}` : "" }${order.trackingUrl ? `\n\n${s.trackOrder}: ${order.trackingUrl}` : ""}`; return { subject: s.subject(order.orderNumber), html, text }; } export async function newsletterConfirmEmail(locale: Locale, confirmUrl: string): Promise { const s = getEmailStrings(locale).newsletterConfirm; const html = await emailLayout( locale, s.preheader, `

${s.heading}

${s.intro}

${button(confirmUrl, s.button)}

${s.ignoreNote}

` ); return { subject: s.subject, html, text: `${s.intro}: ${confirmUrl}`, }; } export async function passwordResetEmail(locale: Locale, resetUrl: string): Promise { const s = getEmailStrings(locale).passwordReset; const html = await emailLayout( locale, s.preheader, `

${s.heading}

${s.intro}

${button(resetUrl, s.button)}

${s.ignoreNote}

` ); return { subject: s.subject, html, text: `${s.intro} ${resetUrl}`, }; } /** * Sicherheitshinweis-Mail fuer sensible Kontoaenderungen (Passwort geaendert, * 2FA deaktiviert, Konto geloescht) — unabhaengig davon, ob die Aenderung * durch die Kundschaft selbst oder potenziell durch jemand anderen ausgeloest * wurde, damit ein Missbrauch schnellstmoeglich auffaellt. */ export type SecurityAlertEvent = "passwordChanged" | "twoFactorDisabled" | "accountDeleted"; export async function securityAlertEmail(locale: Locale, event: SecurityAlertEvent): Promise { const s = getEmailStrings(locale).securityAlert; const eventDescription = s[event]; const html = await emailLayout( locale, s.preheader, `

${s.heading}

${eventDescription}

${s.contactNote}

` ); return { subject: s.subject, html, text: `${eventDescription}\n\n${s.contactNote}`, }; } export async function emailVerificationEmail(locale: Locale, verifyUrl: string): Promise { const s = getEmailStrings(locale).emailVerification; const html = await emailLayout( locale, s.preheader, `

${s.heading}

${s.intro}

${button(verifyUrl, s.button)}

${s.ignoreNote}

` ); return { subject: s.subject, html, text: `${s.intro} ${verifyUrl}`, }; } interface OrderStatusUpdateInput { locale: Locale; orderNumber: string; firstName: string; } interface ShipmentEmailInput extends OrderStatusUpdateInput { carrier?: string | null; trackingNumber?: string | null; trackingUrl?: string | null; } export async function shipmentEmail(order: ShipmentEmailInput): Promise { const s = getEmailStrings(order.locale).shipment; const hasTracking = Boolean(order.carrier && order.trackingNumber); const trackingHtml = hasTracking ? `

${order.carrier}: ${order.trackingNumber}

${order.trackingUrl ? button(order.trackingUrl, s.trackButton) : ""} ` : ""; const html = await emailLayout( order.locale, s.preheader(order.orderNumber), `

${s.heading(order.firstName)}

${s.intro(order.orderNumber)}

${trackingHtml} ` ); const trackingText = hasTracking ? `\n\n${order.carrier}: ${order.trackingNumber}${order.trackingUrl ? `\n${order.trackingUrl}` : ""}` : ""; return { subject: s.subject(order.orderNumber), html, text: `${s.heading(order.firstName)}${trackingText}`, }; } export async function pickupReadyEmail(order: OrderStatusUpdateInput): Promise { const s = getEmailStrings(order.locale).pickupReady; const company = await getCompanyInfo(); const html = await emailLayout( order.locale, s.preheader(order.orderNumber), `

${s.heading(order.firstName)}

${s.intro(order.orderNumber)}

${company.address.street}
${company.address.zip} ${company.address.city}

` ); return { subject: s.subject(order.orderNumber), html, text: `${s.intro(order.orderNumber).replace(/<\/?strong>/g, "")}\n\n${company.address.street}\n${company.address.zip} ${company.address.city}`, }; } /** * Fuer Zahlungsmethoden mit verzoegerter Bestaetigung (z.B. * SEPA-Lastschrift), bei denen der Zahlungseinzug im Nachhinein fehlschlaegt * (checkout.session.async_payment_failed) — die Bestellung wurde bereits * storniert und der Bestand zurueckgebucht (siehe api/checkout/webhook). */ interface BackInStockInput { locale: Locale; name: string; slug: string; } export async function backInStockEmail(product: BackInStockInput): Promise { const s = getEmailStrings(product.locale).backInStock; const url = `${getSiteUrl()}/honig/${product.slug}`; const html = await emailLayout( product.locale, s.preheader(product.name), `

${s.heading}

${s.intro(product.name)}

${button(url, s.button)} ` ); return { subject: s.subject(product.name), html, text: `${s.preheader(product.name)} ${url}`, }; } export async function returnAcceptedEmail(order: OrderStatusUpdateInput): Promise { const s = getEmailStrings(order.locale).returnAccepted; const html = await emailLayout( order.locale, s.preheader(order.orderNumber), `

${s.heading(order.firstName)}

${s.intro(order.orderNumber)}

` ); return { subject: s.subject(order.orderNumber), html, text: s.intro(order.orderNumber).replace(/<\/?strong>/g, ""), }; } export async function returnRejectedEmail(order: OrderStatusUpdateInput): Promise { const s = getEmailStrings(order.locale).returnRejected; const html = await emailLayout( order.locale, s.preheader(order.orderNumber), `

${s.heading(order.firstName)}

${s.intro(order.orderNumber)}

` ); return { subject: s.subject(order.orderNumber), html, text: s.intro(order.orderNumber).replace(/<\/?strong>/g, ""), }; } export async function returnRefundedEmail(order: OrderStatusUpdateInput): Promise { const s = getEmailStrings(order.locale).returnRefunded; const html = await emailLayout( order.locale, s.preheader(order.orderNumber), `

${s.heading(order.firstName)}

${s.intro(order.orderNumber)}

` ); return { subject: s.subject(order.orderNumber), html, text: s.intro(order.orderNumber).replace(/<\/?strong>/g, ""), }; } export async function paymentFailedEmail(order: OrderStatusUpdateInput): Promise { const s = getEmailStrings(order.locale).paymentFailed; // Der Text (retryNote) fordert bereits zur erneuten Bestellung auf — ohne // einen tatsaechlich anklickbaren Button dazu bliebe das ein reiner // Lauftext-Hinweis ohne direkten Weg zurueck in den Shop. const shopUrl = `${getSiteUrl()}/shop`; const html = await emailLayout( order.locale, s.preheader(order.orderNumber), `

${s.heading(order.firstName)}

${s.intro(order.orderNumber)}

${s.retryNote}

${button(shopUrl, s.button)}
` ); return { subject: s.subject(order.orderNumber), html, text: `${s.intro(order.orderNumber).replace(/<\/?strong>/g, "")} ${s.retryNote}\n\n${s.button}: ${shopUrl}`, }; }