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

163 lines
8.4 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 { sendCustomerMail } from '../mailer.js'
import { datumKurz, wochentag } from '../termine/zeit.js'
/**
* Mails von Öffnungszeiten live an Kunden (Vorlagen server/mail-templates/K27 bis K30):
* K27 eingeschaltet (Gratisweg im Wartungs-Abo; mit Block `wieder` nach der Rückkehr des Abos)
* K28 Feiertags-Erinnerung (eine Mail mit allen neuen Tagen, ohne BCC)
* K29 Wartungs-Abo beendet: Funktion läuft bis zur Frist, „für einmalig 30 € behalten“ (Varianten nach der Abo-Lage)
* K30 abgeschaltet: feste Zeiten auf der Seite, Eingaben noch bis zum Löschdatum
* K40, K41 dieselben Mails im Abo-Modell der Kundenansicht neu (k29Mail und k30Mail mit modell: true): ohne Preis und ohne Behalten
* Die Builder sind rein (kein Zugriff auf Appwrite, kein Versand) und liefern { template, values, blocks }, so wie
* siteFeatures.bestellMail: die Werte decken genau die Platzhalter der Vorlage ab. sendeZeitenMail verschickt sie.
*
* Anrede: `kunde` ist der Name oder das Kundendokument; ohne Namen bleibt kunde_name leer (wie bei K22/K23 im Terminplaner).
* Datumsangaben (`frist`, `loeschdatum`) sind Date-Objekte und stehen als TT.MM.JJJJ (Berliner Tag) in der Mail.
*/
const WOCHENTAG_KURZ = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
const TAG_FORM = /^\d{4}-\d{2}-\d{2}$/
const ADRESSE = /[^\s<>"',;()]+@[^\s<>"',;()]+/g
/** Adresse des Editors im Kundenportal. */
const portalUrl = () => `${config.publicBaseUrl}/dashboard.html#zeiten`
/** Anrede-Wert: Name aus einem String oder einem Kundendokument, getrimmt; sonst '' (renderMail macht aus „Hallo ,“ dann „Hallo,“, wie bei K22/K23). */
function kundenName(kunde) {
const name = typeof kunde === 'string' ? kunde : kunde?.name
return typeof name === 'string' ? name.trim() : ''
}
/** Date -> 'TT.MM.JJJJ' (Berliner Tag). Wirft bei etwas anderem als einem gültigen Date: ein leeres Datum in der Mail wäre falsch. */
function datum(wert, feld) {
if (!(wert instanceof Date) || Number.isNaN(wert.getTime())) throw new TypeError(`${feld}: gültiges Date erwartet`)
return datumKurz(wert)
}
/**
* K27: Öffnungszeiten live ist eingeschaltet.
* @param {{ kunde: string | { name?: string }, projekt: string, einbau?: string | null, wieder?: boolean, ticketNr?: string | number | null }} p
* einbau: 'angestossen' (Neubau läuft: Leisten erscheinen nach dem nächsten Veröffentlichen) oder 'manuell' (das Team baut ein);
* alles andere, auch ein fehlender Wert, sagt das Vorsichtige: das Team baut ein.
* Ausnahme: einbau null zusammen mit wieder = es gab keinen Neubau (das Abo kehrte vor dem Abschalten zurück, die Leisten waren nie weg):
* beide Einbau-Blöcke und der ganze Kasten „Wie geht es weiter?“ (Block weiter) entfallen.
* wieder: das Wartungs-Abo läuft wieder und die Funktion wurde automatisch wieder eingeschaltet.
* ticketNr: Nummer des Einbau-Tickets; ohne sie steht kein Ticket in der Mail.
*/
export function k27Mail({ kunde, projekt, einbau, wieder = false, ticketNr = null }) {
const nr = String(ticketNr ?? '').trim().replace(/^#/, '')
const keinNeubau = wieder === true && einbau === null
const angestossen = einbau === 'angestossen'
const manuell = !keinNeubau && !angestossen
return {
template: 'K27',
values: {
kunde_name: kundenName(kunde),
projekt,
// führendes Leerzeichen: steht direkt hinter einem Wort, ohne Ticket bleibt nichts übrig
ticket: nr ? ` (Ticket #${nr})` : '',
portal_url: portalUrl(),
},
blocks: { angestossen, manuell, wieder: Boolean(wieder), weiter: angestossen || manuell },
}
}
/** Eine Zeile je Erinnerungstag: 'Sa 03.10. – Tag der Deutschen Einheit', bei „bitte prüfen“ mit Zusatz. */
function tagesZeile(eintrag) {
if (typeof eintrag === 'string') return eintrag
const { tag, name, art } = eintrag || {}
const zeile = TAG_FORM.test(String(tag))
? `${WOCHENTAG_KURZ[wochentag(tag) - 1]} ${tag.slice(8, 10)}.${tag.slice(5, 7)}.${name ? ` – ${name}` : ''}`
: [tag, name].filter(Boolean).join(' – ')
return art === 'pruefen' ? `${zeile} (bitte prüfen)` : zeile
}
/**
* K28: Feiertage stehen an (eine Mail für alle neuen Tage; Aufrufer: Erinnerungs-Job, ohne BCC ans Team).
* @param {{ kunde: string | { name?: string }, projekt: string, tage: Array<{ tag: string, name?: string, art?: 'feiertag' | 'pruefen' }> }} p
* tage: [{ tag: 'YYYY-MM-DD', name, art }] wie zeitlogik.erinnerungsTage(); Blöcke: pruefen (mindestens ein „bitte prüfen“-Tag), mehrere (> 1 Tag).
*/
export function k28Mail({ kunde, projekt, tage }) {
const liste = Array.isArray(tage) ? tage : []
return {
template: 'K28',
values: {
kunde_name: kundenName(kunde),
projekt,
tage: liste.map(tagesZeile).join('\n'),
portal_url: portalUrl(),
},
blocks: { pruefen: liste.some((e) => e?.art === 'pruefen'), mehrere: liste.length > 1 },
}
}
/**
* K29: Das Wartungs-Abo ist beendet, die Funktion läuft noch bis zur Frist; für einmalig `preis` behalten.
* @param {{ kunde: string | { name?: string }, projekt: string, art?: string, frist: Date, preis: string, modell?: boolean }} p
* art: Abo-Lage aus aboStatus.aboLage(): 'hosting' (das Abo ist ein Hosting-Abo), 'wechsel_widerrufen' (Wechsel ins Wartungs-Abo
* widerrufen), sonst (auch 'beendet' und Unbekanntes) 'abo_ende'. frist: Tag des Abschaltens (aboEndeAt + 30 Tage).
* modell: Abo-Modell der Kundenansicht neu (Task 10): Vorlage K40 ohne Preis und ohne Behalten, die Blöcke bleiben gleich.
*/
export function k29Mail({ kunde, projekt, art, frist, preis, modell = false }) {
const blocks = { abo_ende: art !== 'hosting' && art !== 'wechsel_widerrufen', hosting: art === 'hosting', wechsel_widerrufen: art === 'wechsel_widerrufen' }
if (modell) {
return { template: 'K40', values: { kunde_name: kundenName(kunde), projekt, frist: datum(frist, 'frist'), portal_url: portalUrl() }, blocks }
}
return {
template: 'K29',
values: { kunde_name: kundenName(kunde), projekt, frist: datum(frist, 'frist'), preis, portal_url: portalUrl() },
blocks,
}
}
/**
* K30: Öffnungszeiten live ist abgeschaltet (Wartungs-Abo beendet, Frist zum Behalten abgelaufen).
* @param {{ kunde: string | { name?: string }, projekt: string, loeschdatum: Date, preis: string, modell?: boolean }} p
* loeschdatum: Tag, ab dem die Eingaben gelöscht werden (30 Tage nach dem Abschalten); preis: Preis zum Wiedereinschalten.
* modell: Abo-Modell der Kundenansicht neu (Task 10): Vorlage K41 ohne Preis.
*/
export function k30Mail({ kunde, projekt, loeschdatum, preis, modell = false }) {
if (modell) {
return { template: 'K41', values: { kunde_name: kundenName(kunde), projekt, loeschdatum: datum(loeschdatum, 'loeschdatum'), portal_url: portalUrl() } }
}
return {
template: 'K30',
values: { kunde_name: kundenName(kunde), projekt, loeschdatum: datum(loeschdatum, 'loeschdatum'), preis, portal_url: portalUrl() },
}
}
/**
* Fehlertext fürs Log: Name und gekürzte Meldung. SMTP-Antworten nennen gern die Adresse (und fremde Server den Namen):
* E-Mail-Adressen und der Name des Kunden werden unkenntlich gemacht.
*/
function fehlerText(err, customer) {
let meldung = String(err?.message ?? '').replace(ADRESSE, '[Adresse]')
const name = kundenName(customer)
if (name.length >= 2) meldung = meldung.split(name).join('[Name]')
return `${err?.name ?? 'Fehler'} ${meldung.slice(0, 200)}`
}
/**
* Eine der Mails K27 bis K30 an die E-Mail-Adresse des Kundenkontos senden. Wirft nie; das Log nennt weder Adresse noch Namen.
* @param {{ email?: string, name?: string } | null | undefined} customer Kundendokument
* @param {{ template: string, values?: object, blocks?: object }} nachricht Ergebnis von k27Mail ... k30Mail
* @param {{ bccTeam?: boolean }} [options] bccTeam false: keine Kopie ans Team (K28)
* @returns {Promise<{ sent: boolean, reason?: 'keine_adresse' | 'fehler' }>}
*/
export async function sendeZeitenMail(customer, nachricht, { bccTeam = true } = {}) {
const vorlage = nachricht?.template ?? 'Mail'
try {
const to = String(customer?.email ?? '').trim()
if (!to) {
console.warn(`[zeiten-mail] ${vorlage} entfällt: Kunde nicht mehr vorhanden oder ohne E-Mail-Adresse`)
return { sent: false, reason: 'keine_adresse' }
}
await sendCustomerMail(to, { ...nachricht, bccTeam })
return { sent: true }
} catch (err) {
console.error(`[zeiten-mail] ${vorlage} nicht gesendet: ${fehlerText(err, customer)}`)
return { sent: false, reason: 'fehler' }
}
}