import { config } from '../../config.js' import { loadTemplate } from '../mailTemplates.js' import { customerOf } from '../siteFeatures.js' import { sendAdminNotice, sendCustomerMail } from '../mailer.js' import { einreihen } from '../termine/outbox.js' import { formatTag } from './datum.js' import { formatEuro } from './preis.js' import { ladeEinstellungen, ladePreise, ladeUnterkunft } from './speicher.js' /** * Mails des Buchungskalenders: G16-G22 an Gaeste (ueber die Mail-Warteschlange termineOutbox, Absendername = Firma des Vermieters, * Antworten an dessen E-Mail), K33, K34, K36 und K37 an den Vermieter (Kunde des Portals, ohne BCC bei Gastdaten) und T23 ans Team. * `_aufrufe` protokolliert jeden Aufruf fuer Tests (die Mail geht trotzdem raus). Fehler werden geworfen; der Aufrufer * (ablauf.js) faengt sie ab, weil der gespeicherte Schritt nicht rueckgaengig gemacht wird. * * extra je Gastmail: G17 token (Roh-Token, nur hier und nur einmal bekannt; ergibt den Buchungslink), text (G18 Begruendung), * rechnung (G19: ukRechnungen-Dokument), stornoRechnung + erstattetCent + grund (G22), G21 braucht nichts. */ export const _aufrufe = [] /** Protokoll nur im Test (UK_TESTPROTOKOLL, setzt useTestEnv); nie Token, Werte oder Gastdaten. */ const testProtokoll = () => process.env.NODE_ENV === 'test' || process.env.UK_TESTPROTOKOLL === '1' const LEER = '–' const text = (v) => (typeof v === 'string' ? v.trim() : '') const GAST_ARTEN = ['G16', 'G17', 'G18', 'G19', 'G20', 'G21', 'G22'] const mehrzahl = (n, eins, viele) => `${n} ${n === 1 ? eins : viele}` const FRIST_FORMAT = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }) function fristText(wert) { const t = Date.parse(wert) if (!Number.isFinite(t)) return LEER const teile = Object.fromEntries(FRIST_FORMAT.formatToParts(new Date(t)).map((p) => [p.type, p.value])) return `${teile.day}.${teile.month}.${teile.year}, ${teile.hour}:${teile.minute} Uhr` } function anbieterKontakt(e) { return [text(e.firma), text(e.strasse), [text(e.plz), text(e.ort)].filter(Boolean).join(' '), text(e.email), text(e.telefon)].filter(Boolean).join('\n') } function postenText(posten) { if (!Array.isArray(posten) || posten.length === 0) return LEER return posten.map((p) => `${p.text}: ${formatEuro(p.menge * p.einzelCent)}`).join('\n') } const ZAHLART = { online: 'Online-Zahlung', vorort: 'Zahlung vor Ort' } /** Einstellungen des Vermieters: aus dem Kontext, sonst aus der Datenbank. */ async function einstellungenVon(ctx, featureId) { return ctx?.einstellungen ?? (featureId ? await ladeEinstellungen(featureId) : null) ?? {} } /** Gemeinsame Werte aller Mails zu einer Anfrage. */ async function werteZuAnfrage(ctx, anfrage) { const e = await einstellungenVon(ctx, anfrage.featureId ?? ctx?.featureId) const u = anfrage.unterkunftId ? await ladeUnterkunft(anfrage.unterkunftId) : null const n = Number(anfrage.naechte) || naechteVon(anfrage) return { einstellungen: e, unterkunft: u, werte: { gast_name: text(anfrage.gast?.name) || 'Gast', firma: text(e.firma) || text(u?.name) || 'Ihr Gastgeber', farbe: /^#[0-9a-fA-F]{6}$/.test(text(e.akzentfarbe)) ? e.akzentfarbe : '#111111', anbieter_kontakt: anbieterKontakt(e) || LEER, unterkunft: text(u?.name) || LEER, zeitraum: `${formatTag(anfrage.von)} – ${formatTag(anfrage.bis)}`, naechte: mehrzahl(n, 'Nacht', 'Nächte'), personen: mehrzahl(Number(anfrage.personen) || 0, 'Person', 'Personen'), nummer: text(anfrage.nummer) || LEER, }, } } function naechteVon(a) { return Math.max(0, Math.round((Date.parse(a.bis) - Date.parse(a.von)) / 86400000)) } async function stornoText(anfrage) { // Schnappschuss bei der Freigabe gilt; nur ohne ihn die aktuellen Bedingungen if (text(anfrage.stornobedingungenText)) return text(anfrage.stornobedingungenText) const preise = anfrage.unterkunftId ? await ladePreise(anfrage.unterkunftId) : null return text(preise?.stornobedingungen) || 'Es wurden keine besonderen Stornobedingungen vereinbart.' } export async function anGast(art, ctx, anfrage, extra = {}) { if (testProtokoll()) _aufrufe.push({ an: 'gast', art, anfrageId: anfrage?.$id, hatToken: Boolean(extra?.token), grund: extra?.grund }) if (!GAST_ARTEN.includes(art)) throw new TypeError(`${String(art).slice(0, 20)} ist keine Gastmail`) extra = extra ?? {} const gast = anfrage?.gast ?? {} if (art === 'G17' && (!text(extra.token) || !text(ctx?.siteKey))) throw new TypeError('Token oder siteKey für den Buchungslink fehlt') if (!text(gast.email)) throw new TypeError('Empfänger fehlt') const { einstellungen: e, werte } = await werteZuAnfrage(ctx, anfrage) const values = { ...werte } const blocks = {} let anhang = null if (art !== 'G16' && art !== 'G18' && art !== 'G21' && art !== 'G20' && art !== 'G22') values.stornobedingungen = await stornoText(anfrage) if (art === 'G17' || art === 'G19' || art === 'G20') values.gesamt = formatEuro(anfrage.summen?.bruttoCent ?? 0) if (art === 'G17' || art === 'G19') values.posten_text = postenText(anfrage.posten) if (art === 'G17') { values.frist = fristText(anfrage.fristAt) const token = text(extra.token) values.buchungs_link = `${config.publicBaseUrl}/unterkunft/${ctx?.siteKey ?? ''}/buchung/${anfrage.$id}?t=${token}` values.nachricht_vermieter = text(anfrage.nachrichtVermieter) blocks.nachricht = values.nachricht_vermieter !== '' } if (art === 'G18') { values.nachricht_vermieter = text(extra.text) blocks.nachricht = values.nachricht_vermieter !== '' } if (art === 'G19') { const zahlart = anfrage.zahlart values.zahlart_text = ZAHLART[zahlart] ?? LEER values.rechnung_nummer = text(extra.rechnung?.nummer) || LEER blocks.online_unbezahlt = zahlart === 'online' && anfrage.status !== 'bezahlt' if (extra.rechnung?.$id) anhang = { art: 'ukRechnung', rechnungId: extra.rechnung.$id } } if (art === 'G20') { values.rechnung_nummer = text(extra.rechnung?.nummer) || text(anfrage.rechnungNummer) || LEER } if (art === 'G21') values.frist = fristText(anfrage.fristAt) if (art === 'G22') { values.rechnung_nummer = text(extra.stornoRechnung?.nummer) || LEER values.nachricht_vermieter = text(extra.grund) blocks.nachricht = values.nachricht_vermieter !== '' const erstattung = Number(extra.erstattetCent) || 0 values.erstattung = erstattung > 0 ? formatEuro(erstattung) : '' blocks.erstattung = erstattung > 0 if (extra.stornoRechnung?.$id) anhang = { art: 'ukRechnung', rechnungId: extra.stornoRechnung.$id } } return einreihen({ featureId: anfrage.featureId ?? ctx?.featureId, bookingId: anfrage.$id, art: `uk-${art}`, an: text(gast.email), fromName: values.firma.slice(0, 120), replyTo: text(e.email) || null, template: art, values, blocks, ...(anhang ? { anhang: JSON.stringify(anhang) } : {}), }) } const portalUrl = (ctx) => `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=${encodeURIComponent(ctx?.projectId ?? '')}` /** * Mail an den Vermieter (K33, K34, K36 und K37, Kunde des Portals) bzw. ans Team (T23). K36/K37: values.anfrage; die Werte lassen sich ueberschreiben, * values.an ersetzt die Adresse (Standard ctx.besitzerEmail). T23 geht an die Team-Adressen und wirft nie. */ export async function anVermieter(template, ctx, values = {}) { if (testProtokoll()) _aufrufe.push({ an: 'vermieter', template, anfrageId: values?.anfrage?.$id }) const { anfrage, an, ...eigene } = values ?? {} const vorgabe = {} if (anfrage) { const { werte } = await werteZuAnfrage(ctx, anfrage) Object.assign(vorgabe, werte, { kunde_name: text(ctx?.kundeName) || (await kundenName(ctx)) || werte.firma, gast_email: text(anfrage.gast?.email) || LEER, nachricht_gast: text(anfrage.gast?.nachricht) || LEER, gesamt: formatEuro(anfrage.summen?.bruttoCent ?? 0), zahlart_text: ZAHLART[anfrage.zahlart] ?? LEER, }) } if (!anfrage) vorgabe.kunde_name = text(ctx?.kundeName) || (await kundenName(ctx)) const alle = { portal_url: portalUrl(ctx), ...vorgabe, ...eigene } if (template === 'T23') { const r = await sendAdminNotice({ template, values: nurPlatzhalter(template, alle) }) if (!r?.sent) console.warn(`[unterkunft] Team-Mail ${template} nicht gesendet: ${r?.reason || 'unbekannt'}`) return r } const empfaenger = text(an) || text(ctx?.besitzerEmail) if (!empfaenger) throw new TypeError('Empfänger fehlt') return sendCustomerMail(empfaenger, { template, values: nurPlatzhalter(template, alle), blocks: eigene.blocks ?? {}, bccTeam: ['K33', 'K34'].includes(template) }) } /** Nur die Werte, die die Vorlage kennt (keine Warnung wegen Zusatzwerten wie farbe oder anbieter_kontakt). */ function nurPlatzhalter(template, werte) { const kennt = loadTemplate(template).placeholders return Object.fromEntries(Object.entries(werte).filter(([k]) => kennt.includes(k) && k !== 'blocks')) } /** Name des Kunden (Vermieters) aus dem Kundendokument; Fehler beim Laden machen die Mail nicht kaputt. */ async function kundenName(ctx) { try { return text((await customerOf(ctx?.customerId))?.name) } catch { return '' } }