Files
Webklar-Kundenbereich/server/services/unterkunft/mails.js
T

193 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ''
}
}