/**
* 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" }) };
}