Automatische Mails im neuen Design (HTML mit Textfassung)

Alle 22 automatischen Mails gehen jetzt als HTML nach den Vorlagen aus
server/mail-templates raus, dazu eine aus dem HTML erzeugte Textfassung
(multipart/alternative). Werte werden HTML-sicher eingesetzt, optionale
Blöcke (WENN/ENDE) je nach Fall ein- oder ausgeblendet.

- Neu: services/mailTemplates.js (Vorlagen lesen, Platzhalter, Blöcke,
  Textfassung); mailer.js nimmt { template, values, blocks }.
- K11 "Passwort geändert" war nicht im Design-Export: aus den Bausteinen
  von K02 zusammengesetzt.
- T09 (Widerruf, intern): Fälle "kein Abo gefunden" und "Stripe-Kündigung
  fehlgeschlagen" ergänzt; bisher stand dort immer "auf Kündigung gesetzt".
- Einheitlich: Datum TT.MM.JJJJ, Beträge deutsch (T04 "50,00 €"),
  "WEBklar" statt "Webklar", Anrede mit Kundennamen, Team-Absender
  "WEBklar Kundenportal", Betreff T05 "Kündigung (Stripe)".
- Logo für die Mails: public/webklar-logo.png (192 px, aus dem Design-SVG).

Tests: 52 neue (jede Vorlage: Kopf, Platzhalter, Blöcke, Rendern,
Escaping, Textfassung), npm test 97/97 auf Node 20.19.5; alle 40 Auslöser
einmal durch den Server geschickt (Appwrite-Mock, Stripe-Double,
SMTP-Falle mit STARTTLS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-27 14:37:50 +00:00
1 parent e3daffffa3
commit 18c3cdf3e5
11 files changed
+657 -316

No files matched your search

+52 -89
View File
@@ -48,7 +48,6 @@ export const REQUIRED_CHECKS = [
{ key: 'replaced', label: 'Die Leistung wird durch das Wartungs-Abo ersetzt (Umzug zu WEBklar)' },
]
const FOOTER = 'WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler · Telefon 0176 23726355 · support@webklar.com'
const DAY_MS = 24 * 60 * 60 * 1000
function httpError(status, message) {
@@ -430,44 +429,35 @@ export async function createTakeoverRequest({ customer, input, uploads, consentI
}
const name = customer.name || customer.companyName || input.email
const summary = [
` Antrag: ${shortRef(doc.$id)}`,
` Leistung: ${servicesLabel(doc.services)}`,
` Bisher bei: ${doc.provider}`,
doc.domain ? ` Domain: ${doc.domain}` : null,
` Rechnung: ${fmtEuro(doc.invoiceAmountCents)} für ${monthsLabel(doc.billingMonths)}`,
` Bezahlt bis: ${fmtDate(doc.paidUntil)}`,
` Nachweise: ${files.map((f) => f.name).join(', ')}`,
].filter((l) => l !== null)
const summary = {
antrag_nr: shortRef(doc.$id),
leistung: servicesLabel(doc.services),
bisheriger_anbieter: doc.provider,
domain: doc.domain || '–',
rechnung_betrag: fmtEuro(doc.invoiceAmountCents),
rechnung_zeitraum: monthsLabel(doc.billingMonths),
bezahlt_bis: fmtDate(doc.paidUntil),
nachweise: files.map((f) => f.name).join(', '),
}
await sendCustomerMail(input.email, 'Eingangsbestätigung: Ihr Antrag auf Übernahmegutschrift – WEBklar', [
`Guten Tag ${customer.name || ''}`.trim() + ',',
'',
`wir haben Ihren Antrag auf eine Übernahmegutschrift am ${fmtDateTime(now)} erhalten.`,
'',
...summary,
'',
'Wir prüfen Ihre Angaben und den Nachweis und antworten Ihnen per E-Mail an diese Adresse.',
`Den Stand Ihres Antrags sehen Sie auch im Kundenportal: ${config.publicBaseUrl}/dashboard.html#gutschrift`,
'',
'Viele Grüße',
'Ihr WEBklar-Team',
'',
FOOTER,
]).catch((e) => console.error('[takeover] Eingangsbestaetigung fehlgeschlagen:', e.message))
await sendCustomerMail(input.email, {
template: 'K07',
values: { name: customer.name || '', eingang_zeitpunkt: fmtDateTime(now), ...summary },
}).catch((e) => console.error('[takeover] Eingangsbestaetigung fehlgeschlagen:', e.message))
// Nachweise bewusst nicht als Anhang: sie bleiben auf dem Server (Datenschutzerklaerung Abschnitt 7)
await sendAdminNotice(`Übernahmegutschrift beantragt: ${name}`, [
`${name}${customer.code ? ` (Kd.-Nr. ${customer.code})` : ''} hat eine Übernahmegutschrift beantragt.`,
`Antwort an: ${input.email}`,
'',
...summary,
'',
'Grund / Nachricht des Kunden:',
input.message,
'',
`Nachweise ansehen, prüfen und entscheiden: ${config.publicBaseUrl}/admin.html?gutschrift=${encodeURIComponent(doc.$id)}`,
])
await sendAdminNotice({
template: 'T10',
values: {
kunde_name: name,
kundennummer: customer.code || '–',
email: input.email,
...summary,
nachricht: input.message,
admin_link: `${config.publicBaseUrl}/admin.html?gutschrift=${encodeURIComponent(doc.$id)}`,
},
blocks: { nachricht: Boolean(input.message) },
})
return doc
}
@@ -486,15 +476,17 @@ export async function answerQuestion({ customer, doc, message, uploads }) {
history: withHistory(doc, { type: 'antwort', by: 'customer', text, files: added.map((f) => f.id) }),
})
const name = customer.name || customer.companyName || doc.email
await sendAdminNotice(`Übernahmegutschrift: Antwort von ${name}`, [
`${name} hat auf die Rückfrage zum Antrag ${shortRef(doc.$id)} geantwortet:`,
'',
text || '(ohne Text)',
...(added.length ? ['', `Neue Nachweise: ${added.map((f) => f.name).join(', ')}`] : []),
'',
`Prüfen und entscheiden: ${config.publicBaseUrl}/admin.html?gutschrift=${encodeURIComponent(doc.$id)}`,
])
await sendAdminNotice({
template: 'T11',
values: {
kunde_name: customer.name || customer.companyName || doc.email,
antrag_nr: shortRef(doc.$id),
antwort: text || '(ohne Text)',
neue_nachweise: added.map((f) => f.name).join(', '),
admin_link: `${config.publicBaseUrl}/admin.html?gutschrift=${encodeURIComponent(doc.$id)}`,
},
blocks: { neue_nachweise: added.length > 0 },
})
return updated
}
@@ -804,57 +796,28 @@ export async function applyTakeoverCredit(doc) {
}
}
function customerGreeting(doc, customer) {
return `Guten Tag ${customer?.name || ''}`.trim() + ','
}
async function mailDecision(doc, customer, action) {
const ref = shortRef(doc.$id)
const submitted = fmtDate(doc.$createdAt)
const common = { name: customer?.name || '', antrag_datum: fmtDate(doc.$createdAt), antrag_nr: shortRef(doc.$id) }
const note = String(doc.decisionNote || '').trim()
const footer = ['', 'Viele Grüße', 'Ihr WEBklar-Team', '', FOOTER]
if (action === 'approve') {
return sendCustomerMail(doc.email, 'Ihre Übernahmegutschrift ist genehmigt – WEBklar', [
customerGreeting(doc, customer),
'',
`wir haben Ihren Antrag auf Übernahmegutschrift vom ${submitted} geprüft und genehmigt.`,
'',
` Antrag: ${ref}`,
` Leistung: ${servicesLabel(doc.services)} (bisher bei ${doc.provider})`,
` Gutschrift: ${fmtEuro(doc.amountCents)}`,
` Berechnung: ${doc.calculation}`,
...(note ? ['', note] : []),
'',
'Die Gutschrift verrechnen wir automatisch mit den nächsten Monatsgebühren Ihres Wartungs-Abos.',
'Auf Ihren Rechnungen erscheint sie als angerechnetes Guthaben. Eine Auszahlung ist nicht möglich;',
'ein bei Vertragsende noch nicht verrechneter Rest entfällt.',
...footer,
])
return sendCustomerMail(doc.email, {
template: 'K09',
values: {
...common,
gutschrift_betrag: fmtEuro(doc.amountCents),
leistung: servicesLabel(doc.services),
bisheriger_anbieter: doc.provider,
berechnung: doc.calculation || '–',
anmerkung: note,
},
blocks: { anmerkung: Boolean(note) },
})
}
if (action === 'reject') {
return sendCustomerMail(doc.email, 'Ihr Antrag auf Übernahmegutschrift – WEBklar', [
customerGreeting(doc, customer),
'',
`wir haben Ihren Antrag auf Übernahmegutschrift vom ${submitted} (${ref}) geprüft und können ihn leider nicht genehmigen.`,
'',
note,
'',
'Bei Fragen antworten Sie einfach auf diese E-Mail.',
...footer,
])
return sendCustomerMail(doc.email, { template: 'K10', values: { ...common, begruendung: note } })
}
const question = [...historyOf(doc)].reverse().find((h) => h.type === 'rueckfrage')?.text || ''
return sendCustomerMail(doc.email, 'Rückfrage zu Ihrem Antrag auf Übernahmegutschrift – WEBklar', [
customerGreeting(doc, customer),
'',
`zu Ihrem Antrag auf Übernahmegutschrift vom ${submitted} (${ref}) haben wir eine Rückfrage:`,
'',
question,
'',
`Bitte antworten Sie im Kundenportal unter ${config.publicBaseUrl}/dashboard.html#gutschrift – dort können Sie`,
'auch weitere Nachweise hochladen. Alternativ antworten Sie einfach auf diese E-Mail.',
...footer,
])
return sendCustomerMail(doc.email, { template: 'K08', values: { ...common, rueckfrage_text: question } })
}
/**