162 lines
6.7 KiB
JavaScript
162 lines
6.7 KiB
JavaScript
/**
|
|
* Mails zur Auftragsbestätigung (Übersicht V4/V6, Strang L5): K31 (AB mit PDF und AGB an den Kunden, Teamkopie ohne
|
|
* Link), K32 (Eingangsbestätigung an den Kunden) und T22 (Hinweis ans Team). Der Bestätigungslink steht nur in der
|
|
* Mail an den Kunden: K31 geht ohne BCC raus, das Team bekommt eine Kopie ohne Link, Token und Anhänge.
|
|
* Kein Datenbankzugriff (außer lesePdf als Rückfall für die AB-PDF).
|
|
*/
|
|
import { readFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { config } from '../../config.js'
|
|
import { berlinDate, formatDe } from '../finance/dates.js'
|
|
import { euroDe } from '../finance/format.js'
|
|
import { sendAdminNotice, sendCustomerMail } from '../mailer.js'
|
|
import { lesePdf } from './ablage.js'
|
|
import { fruehesterBeginn } from './auftragSicht.js'
|
|
import { AGB_FASSUNG } from './konstanten.js'
|
|
import { FRUEHBEGINN_TEXT } from './texte.js'
|
|
import { hashToken } from './token.js'
|
|
|
|
// server/services/auftraege/ -> drei Ebenen nach oben = Repo
|
|
const STANDARD_AGB_VERZEICHNIS = fileURLToPath(new URL('../../../public/rechtliches/', import.meta.url))
|
|
let agbVerzeichnis = STANDARD_AGB_VERZEICHNIS
|
|
const FASSUNG_FORMAT = /^[A-Za-z0-9._-]{1,20}$/
|
|
|
|
/** Nur für Tests: anderes Verzeichnis für die AGB-PDFs (`null` = Vorgabe). */
|
|
export function setzeAgbVerzeichnisFuerTests(verzeichnis) {
|
|
agbVerzeichnis = verzeichnis || STANDARD_AGB_VERZEICHNIS
|
|
}
|
|
|
|
export function agbPdfPfad(fassung = AGB_FASSUNG) {
|
|
if (typeof fassung !== 'string' || !FASSUNG_FORMAT.test(fassung) || fassung.includes('..')) throw new TypeError('agbPdfPfad: ungültige AGB-Fassung')
|
|
return path.join(agbVerzeichnis, `agb-${fassung}.pdf`)
|
|
}
|
|
|
|
const zeitFormat = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
|
|
|
function zeitpunkt(iso) {
|
|
const datum = new Date(iso)
|
|
if (!iso || Number.isNaN(datum.getTime())) throw new TypeError('Zeitpunkt fehlt oder ist ungültig')
|
|
return datum
|
|
}
|
|
const tagText = (iso) => formatDe(berlinDate(zeitpunkt(iso)))
|
|
const zeitpunktText = (iso) => `${tagText(iso)}, ${zeitFormat.format(zeitpunkt(iso))}`
|
|
const preisText = (auftrag) => (auftrag.art === 'aufwand' ? `${euroDe(auftrag.stundensatzCents)} pro Stunde` : euroDe(auftrag.summeCents))
|
|
const empfaenger = (auftrag) => String(auftrag.gesendetAn || auftrag.kunde?.email || '').trim()
|
|
|
|
export function k31Mail(auftrag, { token, erinnerung = false, teamkopie = false } = {}) {
|
|
const kunde = auftrag.kunde ?? {}
|
|
return {
|
|
template: 'K31',
|
|
values: {
|
|
name: kunde.name,
|
|
nummer: auftrag.nummer,
|
|
titel: auftrag.titel,
|
|
datum: tagText(auftrag.gesendetAm),
|
|
betrag: preisText(auftrag),
|
|
// die Teamkopie enthält den Link nie, auch wenn ein Token mitgegeben wird
|
|
link: teamkopie ? '' : `${config.publicBaseUrl}/auftrag.html#t=${token}`,
|
|
gueltig_bis: tagText(auftrag.tokenGueltigBis),
|
|
},
|
|
blocks: {
|
|
erinnerung: Boolean(erinnerung),
|
|
privat: kunde.kundenart === 'privat' && !teamkopie,
|
|
festpreis: auftrag.art === 'festpreis',
|
|
aufwand: auftrag.art === 'aufwand',
|
|
teamkopie: Boolean(teamkopie),
|
|
kundenmail: !teamkopie,
|
|
},
|
|
}
|
|
}
|
|
|
|
function frueherBeginn(auftrag) {
|
|
const privat = auftrag.kunde?.kundenart === 'privat'
|
|
const tag = fruehesterBeginn(auftrag)
|
|
return { privat, fruehbeginn: privat && Boolean(auftrag.fruehbeginn), ohne: privat && !auftrag.fruehbeginn, beginnText: tag ? formatDe(tag) : '' }
|
|
}
|
|
|
|
export function k32Mail(auftrag) {
|
|
const kunde = auftrag.kunde ?? {}
|
|
const f = frueherBeginn(auftrag)
|
|
return {
|
|
template: 'K32',
|
|
values: {
|
|
name: kunde.name,
|
|
nummer: auftrag.nummer,
|
|
titel: auftrag.titel,
|
|
zeitpunkt: zeitpunktText(auftrag.bestaetigtAm),
|
|
fruehester_beginn: f.beginnText,
|
|
fruehbeginn_text: f.fruehbeginn ? FRUEHBEGINN_TEXT : '',
|
|
},
|
|
blocks: { fruehbeginn: f.fruehbeginn, ohne_fruehbeginn: f.ohne },
|
|
}
|
|
}
|
|
|
|
const UEBER = { link: 'Bestätigungslink', portal: 'Kundenportal' }
|
|
|
|
export function t22Mail(auftrag) {
|
|
const kunde = auftrag.kunde ?? {}
|
|
const f = frueherBeginn(auftrag)
|
|
return {
|
|
template: 'T22',
|
|
values: {
|
|
kunde: kunde.firma || kunde.name || kunde.email || '',
|
|
nummer: auftrag.nummer,
|
|
titel: auftrag.titel,
|
|
zeitpunkt: zeitpunktText(auftrag.bestaetigtAm),
|
|
ueber: UEBER[auftrag.bestaetigtUeber] ?? '',
|
|
woms_link: `${config.womsBaseUrl}/customers/${auftrag.kundenId}?tab=invoices`,
|
|
fruehester_beginn: f.beginnText,
|
|
},
|
|
blocks: { privat_fruehbeginn: f.fruehbeginn, privat_ohne_fruehbeginn: f.ohne },
|
|
}
|
|
}
|
|
|
|
/**
|
|
* K31 an den Kunden (ohne BCC, mit AB-PDF und AGB), danach die Teamkopie ohne Link und Anhänge. Wirft bei jedem Fehler
|
|
* der Kundenmail (L3 setzt dann mailStatus „fehler“); eine gescheiterte Teamkopie gilt nicht als Fehler.
|
|
*/
|
|
export async function sendeAuftragMail(auftrag, { pdf, token, erinnerung = false } = {}) {
|
|
const adresse = empfaenger(auftrag)
|
|
if (!adresse) throw new Error('Keine E-Mail-Adresse für die Auftragsbestätigung')
|
|
if (hashToken(token) === null) throw new TypeError('sendeAuftragMail: token fehlt oder hat ein falsches Format')
|
|
const abPdf = Buffer.isBuffer(pdf) ? pdf : await lesePdf(auftrag.pdfDatei)
|
|
const fassung = auftrag.agbFassung || AGB_FASSUNG
|
|
let agbPdf
|
|
try {
|
|
agbPdf = await readFile(agbPdfPfad(fassung))
|
|
} catch (err) {
|
|
throw new Error(`AGB-PDF ${fassung} nicht lesbar: ${err.code || err.message}`)
|
|
}
|
|
await sendCustomerMail(adresse, {
|
|
...k31Mail(auftrag, { token, erinnerung }),
|
|
attachments: [
|
|
{ filename: `${auftrag.nummer}.pdf`, content: abPdf, contentType: 'application/pdf' },
|
|
{ filename: `WEBklar-AGB-${fassung}.pdf`, content: agbPdf, contentType: 'application/pdf' },
|
|
],
|
|
bccTeam: false,
|
|
})
|
|
const kopie = await sendAdminNotice(k31Mail(auftrag, { erinnerung, teamkopie: true }))
|
|
if (!kopie.sent) console.error(`[auftraege] Teamkopie der Auftragsbestätigung ${auftrag.nummer} nicht gesendet: ${kopie.reason}`)
|
|
}
|
|
|
|
/** K32 an den Kunden (Kopie ans Team wie üblich: die Mail trägt keinen Link), danach T22 ans Team. Wirft nie. */
|
|
export async function sendeBestaetigtMails(auftrag) {
|
|
const adresse = empfaenger(auftrag)
|
|
if (!adresse) {
|
|
console.error(`[auftraege] K32 für ${auftrag.nummer} entfällt: keine E-Mail-Adresse`)
|
|
} else {
|
|
try {
|
|
await sendCustomerMail(adresse, k32Mail(auftrag))
|
|
} catch (err) {
|
|
console.error(`[auftraege] K32 für ${auftrag.nummer} fehlgeschlagen: ${err.message}`)
|
|
}
|
|
}
|
|
try {
|
|
const t22 = await sendAdminNotice(t22Mail(auftrag))
|
|
if (!t22.sent) console.error(`[auftraege] T22 für ${auftrag.nummer} nicht gesendet: ${t22.reason}`)
|
|
} catch (err) {
|
|
console.error(`[auftraege] T22 für ${auftrag.nummer} fehlgeschlagen: ${err.message}`)
|
|
}
|
|
}
|