/** * Texte der Buchungs-E-Mails. Jede Mail nennt nur den tatsaechlich * bestaetigten Status — eine Teilzahlung heisst nie "vollstaendig bezahlt". */ import type { Booking, Installment } from "@/generated/prisma/client"; import type { ContactRequest } from "./contact"; import { escapeHtml, type EmailDraft } from "./email"; import { appUrl } from "./env"; import { formatEuro } from "./money"; import { PLAN_LABEL } from "./payment-plan"; import { PHASE_LABEL } from "./pricing"; import { formatDate, formatDateTime } from "./time"; type BookingWithInstallments = Booking & { installments: Installment[] }; const BRAND = "ARTISTA MODELCAMP 2027"; function statusLink(token: string): string { return appUrl(`/buchung/status/${token}`); } function planLines(booking: BookingWithInstallments): string[] { return booking.installments .slice() .sort((a, b) => a.seq - b.seq) .map((i) => { const due = i.dueAt ? `fällig am ${formatDate(i.dueAt)}` : "bei Buchung"; const state = i.status === "PAID" ? "bezahlt" : i.status === "CANCELED" ? "storniert" : i.status === "PROCESSING" ? "in Bearbeitung" : "offen"; return `Rate ${i.seq}: ${formatEuro(i.amountCents)}, ${due}, ${state}`; }); } function wrap(subject: string, paragraphs: string[], link?: { href: string; label: string }): { text: string; html: string } { const text = [...paragraphs, ...(link ? [`${link.label}: ${link.href}`] : []), "", `${BRAND}`].join("\n\n"); const html = `

${BRAND}

${escapeHtml(subject)}

${paragraphs.map((p) => `

${escapeHtml(p)}

`).join("\n")} ${link ? `

${escapeHtml(link.label)}

` : ""}
`; return { text, html }; } function draft(key: string, booking: Booking, subject: string, body: { text: string; html: string }): EmailDraft { return { dedupeKey: key, to: booking.email, subject: `${subject} | ${BRAND}`, bookingId: booking.id, ...body }; } export function bookingConfirmedEmail(booking: BookingWithInstallments, token: string): EmailDraft { const fullyPaid = booking.paidCents >= booking.totalCents; const subject = fullyPaid ? "Deine Buchung ist bestätigt und bezahlt" : "Deine Buchung ist bestätigt, erste Rate erhalten"; const lines = [ `Hallo ${booking.firstName},`, `vielen Dank! Deine Buchung ${booking.reference} ist bestätigt.`, `Gesamtpreis: ${formatEuro(booking.totalCents)} (${PHASE_LABEL[booking.pricePhase]}${booking.voucherDiscountCents > 0 ? ", inkl. Gutschein" : ""})\nZahlungsweise: ${PLAN_LABEL[booking.planType]}\nBezahlt: ${formatEuro(booking.paidCents)}\nOffen: ${formatEuro(Math.max(0, booking.totalCents - booking.paidCents))}`, planLines(booking).join("\n"), ]; if (!fullyPaid) { lines.push("Die weiteren Raten werden zu den genannten Terminen über die hinterlegte Zahlungsmethode eingezogen. Du erhältst jeweils eine Bestätigung."); } lines.push("Alle Details und den aktuellen Zahlungsstand findest du jederzeit auf deiner Buchungsseite."); return draft(`booking-confirmed:${booking.id}`, booking, subject, wrap(subject, lines, { href: statusLink(token), label: "Buchung ansehen" })); } export function paymentProcessingEmail(booking: Booking, token: string): EmailDraft { const subject = "Deine Zahlung wird verarbeitet"; return draft( `payment-processing:${booking.id}`, booking, subject, wrap( subject, [ `Hallo ${booking.firstName},`, `wir haben deine Buchung ${booking.reference} erhalten. Deine Zahlungsart braucht einige Tage, bis die Zahlung bestätigt ist. Dein Platz bleibt so lange reserviert.`, "Sobald die Zahlung eingegangen ist, bekommst du eine Bestätigung. Erst dann ist die Buchung verbindlich bestätigt.", ], { href: statusLink(token), label: "Status ansehen" }, ), ); } export function paymentFailedEmail(booking: Booking): EmailDraft { const subject = "Deine Zahlung ist fehlgeschlagen"; return draft( `payment-failed:${booking.id}`, booking, subject, wrap(subject, [ `Hallo ${booking.firstName},`, `leider konnte die Zahlung für deine Buchung ${booking.reference} nicht abgeschlossen werden. Die Reservierung wurde deshalb aufgehoben.`, "Du kannst jederzeit eine neue Buchung starten.", ]), ); } export function installmentPaidEmail(booking: BookingWithInstallments, installment: Installment, token: string): EmailDraft { const fullyPaid = booking.paidCents >= booking.totalCents; const subject = fullyPaid ? "Vollständig bezahlt. Danke!" : `Rate ${installment.seq} erhalten`; return draft( `installment-paid:${installment.id}`, booking, subject, wrap( subject, [ `Hallo ${booking.firstName},`, `wir haben Rate ${installment.seq} über ${formatEuro(installment.amountCents)} für deine Buchung ${booking.reference} erhalten.`, fullyPaid ? "Damit ist dein Platz vollständig bezahlt. Weitere Abbuchungen finden nicht statt." : `Bezahlt: ${formatEuro(booking.paidCents)} · Offen: ${formatEuro(booking.totalCents - booking.paidCents)}`, planLines(booking).join("\n"), ], { href: statusLink(token), label: "Buchung ansehen" }, ), ); } export function installmentActionEmail(booking: Booking, installment: Installment, token: string, requiresAction: boolean): EmailDraft { const subject = requiresAction ? `Bitte bestätige Rate ${installment.seq}` : `Rate ${installment.seq} konnte nicht eingezogen werden`; return draft( `installment-action:${installment.id}:${installment.attemptCount}:${requiresAction ? "auth" : "fail"}`, booking, subject, wrap( subject, [ `Hallo ${booking.firstName},`, requiresAction ? `deine Bank möchte die Zahlung von Rate ${installment.seq} (${formatEuro(installment.amountCents)}) zusätzlich bestätigen lassen.` : `der Einzug von Rate ${installment.seq} (${formatEuro(installment.amountCents)}) ist leider fehlgeschlagen.`, "Du kannst die Rate über den folgenden Link selbst begleichen, auch mit einer anderen Zahlungsmethode.", ], { href: statusLink(token), label: "Rate jetzt bezahlen" }, ), ); } export function adminReviewEmail(to: string, booking: Booking, reason: string): EmailDraft { const subject = `Prüfung nötig: Buchung ${booking.reference}`; const body = wrap(subject, [ `Die Buchung ${booking.reference} benötigt eine manuelle Entscheidung.`, `Grund: ${reason}`, "Details stehen im Adminbereich unter Buchungen.", ], { href: appUrl(`/admin/buchungen/${booking.id}`), label: "Im Adminbereich öffnen" }); return { dedupeKey: `admin-review:${booking.id}:${reason}`, to, subject, bookingId: booking.id, ...body }; } export function reviewCustomerEmail(booking: Booking, token: string): EmailDraft { const subject = "Deine Zahlung ist eingegangen, wir melden uns"; return draft( `review-customer:${booking.id}`, booking, subject, wrap( subject, [ `Hallo ${booking.firstName},`, `deine Zahlung für die Buchung ${booking.reference} ist bei uns eingegangen. Bevor wir die Buchung bestätigen können, müssen wir sie kurz manuell prüfen.`, "Wir melden uns umgehend bei dir. Solltest du keinen Platz erhalten, wird dir der gezahlte Betrag vollständig erstattet.", ], { href: statusLink(token), label: "Status ansehen" }, ), ); } /** Anfrage fuer ein Erstgespraech an den Veranstalter (src/lib/contact.ts). */ export function contactInquiryEmail(to: string, inquiry: ContactRequest, dedupeKey: string, receivedAt: Date): EmailDraft { const subject = `Anfrage Erstgespräch: ${inquiry.name}`; const lines = [ `Über die Webseite kam eine Anfrage für ein Erstgespräch (${formatDateTime(receivedAt)}).`, [ `Name: ${inquiry.name}`, `E-Mail: ${inquiry.email}`, `Telefon: ${inquiry.phone ?? "–"}`, `Erreichbar: ${inquiry.availability ?? "–"}`, ].join("\n"), inquiry.message ? `Nachricht:\n${inquiry.message}` : "Keine Nachricht angegeben.", ]; const reply = `mailto:${encodeURIComponent(inquiry.email)}?subject=${encodeURIComponent(`Dein Erstgespräch – ${BRAND}`)}`; return { dedupeKey, to, subject: `${subject} | ${BRAND}`, ...wrap(subject, lines, { href: reply, label: "Antworten" }) }; }