797 lines
56 KiB
JavaScript
797 lines
56 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||
import { test } from 'node:test'
|
||
import { escapeHtml, htmlToText, loadTemplate, renderMail, templateIds } from '../server/services/mailTemplates.js'
|
||
import { contractAttachmentFor, termsDocFor } from '../server/services/products.js'
|
||
|
||
const DIR = new URL('../server/mail-templates/', import.meta.url)
|
||
// G01–G08 Gästemails des Terminplaners (Task 9, neutrales Layout im Auftrag eines Betriebs), K20 und K21 Hinweise an den Betrieb;
|
||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; T18 Bankzugang erneuern (Finanzen)
|
||
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
|
||
const EXPECTED = [...ids('G', 8), ...ids('K', 21), ...ids('T', 18)]
|
||
const GUEST = ids('G', 8)
|
||
|
||
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
|
||
const allBlocks = (tpl, on) => Object.fromEntries(tpl.blocks.map((b) => [b, on]))
|
||
|
||
test('alle Vorlagen sind da', () => {
|
||
assert.deepEqual(templateIds(), EXPECTED)
|
||
})
|
||
|
||
for (const id of EXPECTED) {
|
||
test(`${id}: Kopfkommentar passt zur Vorlage`, () => {
|
||
const tpl = loadTemplate(id)
|
||
const source = readFileSync(new URL(tpl.file, DIR), 'utf8')
|
||
const title = source.match(/<title>([\s\S]*?)<\/title>/)[1]
|
||
assert.equal(title, tpl.subject, 'Betreff im Kopf und <title> sind gleich')
|
||
// F34: Team-Hinweise tragen [Abo], [Speicher] (Zusatzspeicher-Aufgaben), [Datenschutz] (Loeschfristen), [Zeit] (Push-Zeitmail) oder [Finanzen] (Bankzugang)
|
||
assert.equal(id.startsWith('T'), /^\[(Abo|Speicher|Datenschutz|Zeit|Finanzen)\]/.test(tpl.subject) || id === 'T01' || id === 'T16' || id === 'T17', 'Team-Hinweise ab T02 mit [Abo], [Speicher], [Datenschutz], [Zeit] oder [Finanzen] (T16 Funktion bestellt und T17 Repo zuordnen ohne Kennung)')
|
||
|
||
const used = new Set([...tpl.body.matchAll(/\{\{(\w+)\}\}/g), ...tpl.subject.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]))
|
||
assert.deepEqual([...used].sort(), [...tpl.placeholders].sort(), 'deklarierte = verwendete Platzhalter')
|
||
|
||
const opened = [...tpl.body.matchAll(/<!-- WENN (\w+) -->/g)].map((m) => m[1])
|
||
const closed = [...tpl.body.matchAll(/<!-- ENDE (\w+) -->/g)].map((m) => m[1])
|
||
assert.deepEqual([...new Set(opened)].sort(), [...tpl.blocks].sort(), 'deklarierte = verwendete Blöcke')
|
||
assert.deepEqual(opened.slice().sort(), closed.slice().sort(), 'jeder Block wird geschlossen')
|
||
})
|
||
|
||
test(`${id}: rendert ohne Reste, mit und ohne optionale Blöcke`, () => {
|
||
const tpl = loadTemplate(id)
|
||
for (const on of [true, false]) {
|
||
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, on))
|
||
assert.deepEqual(mail.problems, [])
|
||
assert.doesNotMatch(mail.html, /\{\{|WENN|ENDE|Platzhalter:/)
|
||
assert.match(mail.html, /<!--\[if mso\]>/, 'bedingter Outlook-Kommentar bleibt')
|
||
assert.equal((mail.html.match(/<!--/g) || []).length, 1, 'nur der Outlook-Kommentar bleibt')
|
||
// K und T tragen das WEBklar-Logo; G (Gästemails im Auftrag eines Betriebs) haben ein neutrales Layout ohne
|
||
if (/^[KT]/.test(id)) assert.match(mail.html, /src="https:\/\/project\.webklar\.com\/webklar-logo\.png"/)
|
||
else assert.doesNotMatch(mail.html, /webklar-logo\.png/, 'G-Vorlagen ohne WEBklar-Logo')
|
||
assert.doesNotMatch(mail.subject, /\{\{|\n/)
|
||
assert.doesNotMatch(mail.text, /<[a-z/!]|&[a-z#0-9]+;|\{\{/i, 'Textfassung ohne Tags und Entities')
|
||
}
|
||
})
|
||
}
|
||
|
||
test('Werte werden HTML-sicher eingesetzt, Zeilenumbrüche werden <br>', () => {
|
||
const mail = renderMail('T01', { kunde_name: 'Max <b>&"\'', nachricht: 'Zeile 1\nZeile <2>' })
|
||
assert.match(mail.html, /Max <b>&"' hat im Kundenportal geschrieben/)
|
||
assert.match(mail.html, /Zeile 1<br>Zeile <2>/)
|
||
assert.doesNotMatch(mail.html, /<b>&/)
|
||
assert.equal(mail.subject, 'Neue Portalnachricht von Max <b>&"\'')
|
||
assert.match(mail.text, /Zeile 1\nZeile <2>/)
|
||
assert.equal(escapeHtml('a&b'), 'a&b')
|
||
})
|
||
|
||
test('optionale Blöcke: nur eingeschaltete erscheinen', () => {
|
||
const values = { name: 'Max', produkt: 'Wartungs-Abo', preis: '50,00 € / Monat', angebot_text: 'Erste 2 Monate je 25,00 €', vertragsschluss: '01.10.2026', leistungsbeginn: '15.10.2026', mindestlaufzeit_bis: '15.10.2027', bedingungen_version: 'v3', zustimmung_zeitpunkt: '01.10.2026, 10:00' }
|
||
const early = renderMail('K01', values, { angebot: false, sofortbeginn: true, beginn_nach_widerrufsfrist: false })
|
||
assert.doesNotMatch(early.html, /Ihr Angebot:/)
|
||
assert.match(early.html, /Sie haben ausdrücklich verlangt/)
|
||
assert.doesNotMatch(early.html, /erst nach Ablauf der Widerrufsfrist/)
|
||
const late = renderMail('K01', values, { angebot: true, sofortbeginn: false, beginn_nach_widerrufsfrist: true })
|
||
assert.match(late.html, /Ihr Angebot:<\/strong> Erste 2 Monate je 25,00 € \(wie vereinbart\)/)
|
||
assert.doesNotMatch(late.html, /Sie haben ausdrücklich verlangt/)
|
||
assert.match(late.html, /Leistungsbeginn und erste Abbuchung: 15\.10\.2026\./)
|
||
})
|
||
|
||
test('verschachtelte Blöcke (T09) werden aufgelöst', () => {
|
||
const values = { email: 'a@example.com', name: 'A', kundennummer: '', stripe_abo_id: 'sub_1', abo_beginn: '01.10.2026', fristende: '15.10.2026', rechtzeitig: 'RECHTZEITIG' }
|
||
const ok = renderMail('T09', values, { abo_gefunden: true, auf_kuendigung_gesetzt: true })
|
||
assert.match(ok.html, /sub_1 <span[^>]*>\(auf Kündigung gesetzt\)<\/span>/)
|
||
assert.doesNotMatch(ok.html, /KEIN aktives Abo|NICHT automatisch/)
|
||
const failed = renderMail('T09', values, { abo_gefunden: true, auf_kuendigung_gesetzt: false, kuendigung_fehlgeschlagen: true })
|
||
assert.doesNotMatch(failed.html, /auf Kündigung gesetzt\)/)
|
||
assert.match(failed.html, /Abo sub_1 konnte NICHT automatisch auf Kündigung gesetzt werden/)
|
||
const none = renderMail('T09', values, { kein_abo_gefunden: true })
|
||
assert.doesNotMatch(none.html, /Stripe-Abo|Abo-Beginn|Widerrufsfrist bis/)
|
||
assert.match(none.html, /KEIN aktives Abo zu dieser E-Mail gefunden/)
|
||
assert.doesNotMatch(none.html, /WENN|ENDE/)
|
||
})
|
||
|
||
test('Anrede ohne Namen, fehlende und unbekannte Werte werden gemeldet', () => {
|
||
const mail = renderMail('K10', { name: '', antrag_datum: '01.10.2026', antrag_nr: 'AB12CD34', begruendung: 'x', extra: 1 }, { gibtsnicht: true })
|
||
assert.match(mail.html, /Guten Tag,<br>/)
|
||
assert.match(mail.text, /^Guten Tag,$/m)
|
||
assert.deepEqual(mail.problems.sort(), ['unbekannter Block "gibtsnicht"', 'unbekannter Platzhalter {{extra}}'])
|
||
const missing = renderMail('K10', { name: 'A', antrag_datum: '01.10.2026', antrag_nr: 'X' })
|
||
assert.deepEqual(missing.problems, ['kein Wert für {{begruendung}}'])
|
||
})
|
||
|
||
test('Textfassung K01: alle Vertragsangaben lesbar', () => {
|
||
// F34: Produkt und Preis kommen aus dem Katalog, der Produktblock (hier wartung) bringt die Liste „Enthalten“
|
||
const mail = renderMail(
|
||
'K01',
|
||
{ name: 'Erika Musterfrau', produkt: 'Wartungs-Abo', preis: '50,00 € / Monat', angebot_text: '', vertragsschluss: '01.10.2026', leistungsbeginn: '01.10.2026', mindestlaufzeit_bis: '01.10.2027', bedingungen_version: '2026-09-25-v3', zustimmung_zeitpunkt: '01.10.2026, 10:07' },
|
||
{ sofortbeginn: true, wartung: true }
|
||
)
|
||
assert.deepEqual(mail.problems, [])
|
||
const t = mail.text
|
||
for (const line of [
|
||
'Guten Tag Erika Musterfrau,',
|
||
'50,00 € / Monat',
|
||
'gemäß § 19 UStG ohne Umsatzsteuer',
|
||
'Vertragsschluss: 01.10.2026',
|
||
'Leistungsbeginn: 01.10.2026',
|
||
'Laufzeit: Mindestlaufzeit 12 Monate bis 01.10.2027. Kündigung zu diesem Termin bis einen Monat vorher. Danach läuft das Abo unbefristet und ist jederzeit zum Ende des Abrechnungsmonats kündbar.',
|
||
'• 1 Domain',
|
||
'Zum Kundenportal: https://project.webklar.com/dashboard.html',
|
||
'📎 Widerrufsbelehrung mit Muster-Widerrufsformular',
|
||
'Ihre Zustimmung zu den Vertragsbedingungen (Version 2026-09-25-v3) einschließlich des Auftragsverarbeitungsvertrags haben wir am 01.10.2026, 10:07 Uhr erhalten.',
|
||
'Vertrag widerrufen (https://project.webklar.com/widerrufen.html)',
|
||
'Vertrag kündigen (https://project.webklar.com/kuendigen.html)',
|
||
'Telefon 0176 23726355 · support@webklar.com',
|
||
'Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein',
|
||
]) {
|
||
assert.ok(t.split('\n').includes(line), `Zeile fehlt: ${line}\n---\n${t}`)
|
||
}
|
||
assert.doesNotMatch(t, /Ihr Wartungs-Abo ist abgeschlossen\. Alle Vertragsunterlagen/, 'Vorschautext nicht im Text')
|
||
})
|
||
|
||
test('htmlToText: Links, Buttons und Leerzeilen', () => {
|
||
const t = htmlToText('<body><table><tr><td class="px">A <a href="mailto:x@y.de">x@y.de</a> B</td></tr><tr><td class="px"><a href="https://e.de/p" style="display:block">Los →</a></td></tr></table></body>')
|
||
assert.equal(t, 'A x@y.de B\n\nLos: https://e.de/p\n')
|
||
})
|
||
|
||
test('Vorlagen-Ordner enthält nur Vorlagen', () => {
|
||
assert.deepEqual(readdirSync(DIR).filter((f) => !/^[GKT]\d{2} .*\.html$/.test(f)), [])
|
||
})
|
||
|
||
test('K09: Gutschrift wird mit dem Abo verrechnet, auch nach einem Wechsel', () => {
|
||
const tpl = loadTemplate('K09')
|
||
const mail = renderMail('K09', sampleValues(tpl), allBlocks(tpl, true))
|
||
assert.match(mail.text, /mit den nächsten Monatsgebühren Ihres Abos\. Auf Ihren Rechnungen erscheint sie als angerechnetes Guthaben\./)
|
||
assert.match(mail.text, /Eine Auszahlung ist nicht möglich\. Wechseln Sie in ein anderes Abo, verrechnen wir die Gutschrift weiter; endet Ihr Abo ohne einen solchen Wechsel, entfällt ein noch nicht verrechneter Rest\./)
|
||
assert.doesNotMatch(mail.html, /Wartungs-Abo|Vertragsende/)
|
||
})
|
||
|
||
// Task 7 (Fix 1): konnte das Vertragsereignis "abschluss" dauerhaft nicht gespeichert werden (4xx bei
|
||
// Appwrite), erfaehrt das Team das im T02-Hinweis statt in einem stillen Webhook-Fehler
|
||
test('T02: Block nachweis_fehlt weist auf den fehlenden Nachweis hin', () => {
|
||
const tpl = loadTemplate('T02')
|
||
const values = sampleValues(tpl)
|
||
const withBlock = renderMail('T02', values, { nachweis_fehlt: true })
|
||
assert.deepEqual(withBlock.problems, [])
|
||
assert.match(withBlock.text, /Nachweis nicht gespeichert/)
|
||
assert.match(withBlock.text, /contractEvents/)
|
||
assert.doesNotMatch(renderMail('T02', values, {}).text, /Nachweis nicht gespeichert/)
|
||
})
|
||
|
||
// Task 9 (Rechtsrunde 28.09.2026): Wechsel ins Wartungs-Abo wird erst mit der Zahlung wirksam
|
||
test('K12, K14, T12: Betreff, Platzhalter und Blöcke des Wechsels', () => {
|
||
assert.equal(loadTemplate('K12').subject, 'Ihr Wechsel ins WEBklar Wartungs-Abo – Bestätigung')
|
||
assert.deepEqual(loadTemplate('K12').placeholders, ['name', 'wechseltag', 'betrag_heute', 'monatspreis', 'mindestlaufzeit_bis', 'bedingungen_version', 'zustimmung_zeitpunkt'])
|
||
assert.equal(loadTemplate('K14').subject, 'Ihr Wechsel ins WEBklar Wartungs-Abo ist nicht zustande gekommen')
|
||
assert.deepEqual(loadTemplate('K14').placeholders, ['name', 'grund'])
|
||
assert.equal(loadTemplate('T12').subject, '[Abo] Wechsel ins Wartungs-Abo: {{email}}')
|
||
// F37: feste Platzhalter fuer T12
|
||
assert.deepEqual(loadTemplate('T12').placeholders, ['email', 'name', 'stripe_abo_id', 'wechseltag', 'betrag_heute', 'monatspreis'])
|
||
for (const block of ['gescheitert', 'lastschrift_gescheitert']) assert.ok(loadTemplate('T12').blocks.includes(block), block)
|
||
})
|
||
|
||
test('K12: Block zahlung_offen wörtlich, Block lastschrift mit dem Satz aus § 9 Abs. 3', () => {
|
||
const values = sampleValues(loadTemplate('K12'))
|
||
const offen = renderMail('K12', values, { zahlung_offen: true })
|
||
assert.ok(offen.text.includes('Der Wechsel wird wirksam, sobald die Zahlung bestätigt ist. Andernfalls läuft Ihr Hosting-Abo mit Preis und Mindestlaufzeit wie bisher weiter.'))
|
||
assert.doesNotMatch(offen.text, /SEPA-Lastschrift/)
|
||
const sepa = renderMail('K12', values, { lastschrift: true })
|
||
assert.match(sepa.text, /Wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt/)
|
||
assert.match(sepa.text, /Ihr Abbuchungstag bleibt der Wechseltag/)
|
||
assert.doesNotMatch(sepa.text, /sobald die Zahlung bestätigt ist/)
|
||
const plain = renderMail('K12', values, {})
|
||
assert.doesNotMatch(plain.text, /sobald die Zahlung bestätigt ist|Wird die Lastschrift nicht eingelöst/)
|
||
// Vertragsbestaetigung: Preis mit § 19 UStG, Mindestlaufzeit, Unterlagen, Zustimmung zum Sofortbeginn
|
||
for (const text of ['gemäß § 19 UStG ohne Umsatzsteuer', 'Mindestlaufzeit 12 Monate bis Wert-mindestlaufzeit_bis.', 'Widerrufsbelehrung mit Muster-Widerrufsformular', 'Sie haben ausdrücklich verlangt, dass wir vor Ablauf der Widerrufsfrist mit dem Wartungs-Abo beginnen']) {
|
||
assert.ok(plain.text.includes(text), text)
|
||
}
|
||
})
|
||
|
||
test('K14: Hosting-Abo läuft wie vor dem Wechsel weiter; SEPA: Abbuchungstag bleibt', () => {
|
||
const values = { name: 'Kunde', grund: 'Die SEPA-Lastschrift für die Wechselrechnung wurde nicht eingelöst.' }
|
||
const card = renderMail('K14', values, {})
|
||
assert.deepEqual(card.problems, [])
|
||
assert.ok(card.text.includes('Ihr Hosting-Abo läuft mit Preis und Mindestlaufzeit wie vor dem Wechsel weiter.'))
|
||
assert.ok(card.text.includes(values.grund))
|
||
assert.doesNotMatch(card.text, /Abbuchungstag/)
|
||
assert.match(renderMail('K14', values, { lastschrift: true }).text, /Ihr Abbuchungstag bleibt der Tag des Wechsels\./)
|
||
})
|
||
|
||
// Nach der Pruefung der Rechtsrunden-Texte: Stripe kann die Frist auch kuerzer setzen
|
||
test('Wechsel-Mails nennen keine feste Frist von 23 Stunden', () => {
|
||
for (const id of ['K12', 'K14', 'T12']) {
|
||
const tpl = loadTemplate(id)
|
||
assert.doesNotMatch(renderMail(id, sampleValues(tpl), allBlocks(tpl, true)).text, /23 Stunden/, id)
|
||
}
|
||
})
|
||
|
||
// Task 8 (Fix 1): K13 (Kunde) und T13 (Team) fuer den Zusatzspeicher, Aufbau wie K12/T12; Endfassung der Texte in Aufgabe 12
|
||
test('K13, T13: Betreff, Platzhalter und Blöcke des Zusatzspeichers', () => {
|
||
assert.equal(loadTemplate('K13').subject, 'Ihr E-Mail-Zusatzspeicher – Bestätigung')
|
||
assert.deepEqual(loadTemplate('K13').placeholders, ['name', 'pakete', 'gesamt_gb', 'monatspreis_zusatz', 'wirksam_ab'])
|
||
// Fix 2 (3): plus = Einrichtung mit anteiliger Berechnung, plus_bezahlt = Plus bis zur schon bezahlten Menge,
|
||
// bestellung = Unterlagen und Widerruf (bei beiden), minus = Verringerung zum Periodenende
|
||
assert.deepEqual(loadTemplate('K13').blocks, ['plus', 'plus_bezahlt', 'bestellung', 'minus'])
|
||
assert.equal(loadTemplate('T13').subject, '[Speicher] {{aktion}}: {{email}}')
|
||
for (const p of ['aktion', 'email', 'name', 'stripe_abo_id', 'postfaecher_hinweis', 'neue_groesse_gb', 'pakete', 'faellig']) {
|
||
assert.ok(loadTemplate('T13').placeholders.includes(p), p)
|
||
}
|
||
assert.deepEqual([...loadTemplate('T13').blocks].sort(), ['einrichten', 'nachweis_fehlt', 'periodenende', 'teilweise'])
|
||
|
||
const values = sampleValues(loadTemplate('K13'))
|
||
const plus = renderMail('K13', values, { plus: true, bestellung: true })
|
||
assert.deepEqual(plus.problems, [])
|
||
assert.match(plus.text, /gemäß § 19 UStG ohne Umsatzsteuer/)
|
||
assert.match(plus.text, /wird eingerichtet/)
|
||
assert.match(plus.text, /14 Tagen widerrufen/)
|
||
assert.match(plus.text, /Vertragsunterlagen im Anhang/)
|
||
assert.doesNotMatch(plus.text, /ohne anteilige Erstattung|keine zusätzlichen Kosten/)
|
||
const paid = renderMail('K13', values, { plus_bezahlt: true, bestellung: true })
|
||
assert.ok(paid.text.includes('Die Pakete sind für den laufenden Monat schon bezahlt; es entstehen keine zusätzlichen Kosten. Ab dem nächsten Abrechnungsmonat gelten Wert-pakete Pakete.'))
|
||
assert.match(paid.text, /14 Tagen widerrufen/)
|
||
assert.match(paid.text, /Vertragsunterlagen im Anhang/)
|
||
assert.doesNotMatch(paid.text, /wird eingerichtet|anteilig/)
|
||
const minus = renderMail('K13', values, { minus: true })
|
||
assert.match(minus.text, /ohne anteilige Erstattung/)
|
||
assert.match(minus.text, /keine neuen E-Mails/)
|
||
assert.doesNotMatch(minus.text, /wird eingerichtet|14 Tagen widerrufen|im Anhang|schon bezahlt/)
|
||
|
||
const t = loadTemplate('T13')
|
||
const teilweise = renderMail('T13', { ...sampleValues(t), pakete: 5 }, { einrichten: true, teilweise: true })
|
||
assert.deepEqual(teilweise.problems, [])
|
||
assert.match(teilweise.text, /Schritt 2 fehlgeschlagen/)
|
||
assert.match(teilweise.text, /von Hand auf 5 setzen/)
|
||
assert.match(renderMail('T13', sampleValues(t), { nachweis_fehlt: true }).text, /Nachweis nicht gespeichert/)
|
||
assert.match(renderMail('T13', sampleValues(t), { periodenende: true }).text, /Periodenende|zum Wert-faellig/)
|
||
assert.doesNotMatch(renderMail('T13', sampleValues(t), {}).text, /Schritt 2 fehlgeschlagen|Nachweis nicht gespeichert/)
|
||
})
|
||
|
||
// Task 10: Kuendigung und Widerruf mit Auswahl - K03/T08 nennen den Vertrag und die gekuendigten Pakete, K04/T09 die
|
||
// Bestellung; die Handgriffe je Bestellung und "wir pruefen" ohne offene Frist. Endfassung der Texte in Aufgabe 12
|
||
test('K03, K04, T08, T09: Vertrag bzw. Bestellung, Pakete und die Blöcke der Auswahl', () => {
|
||
for (const p of ['vertrag', 'pakete', 'pakete_neu']) assert.ok(loadTemplate('K03').placeholders.includes(p), `K03 ${p}`)
|
||
for (const b of ['zusatzspeicher', 'pruefen', 'abo']) assert.ok(loadTemplate('K03').blocks.includes(b), `K03 ${b}`)
|
||
assert.ok(loadTemplate('K04').placeholders.includes('bestellung'))
|
||
assert.deepEqual(loadTemplate('K04').blocks, ['kundennummer', 'abschluss', 'wechsel', 'zusatzspeicher', 'funktion', 'pruefen'])
|
||
for (const p of ['vertrag', 'pakete', 'fehler']) assert.ok(loadTemplate('T08').placeholders.includes(p), `T08 ${p}`)
|
||
for (const b of ['zusatzspeicher', 'kein_speicher', 'wechsel_ausstehend', 'speicher_fehlgeschlagen', 'nachweis_fehlt']) assert.ok(loadTemplate('T08').blocks.includes(b), `T08 ${b}`)
|
||
for (const p of ['bestellung', 'ereignisse']) assert.ok(loadTemplate('T09').placeholders.includes(p), `T09 ${p}`)
|
||
for (const b of ['abschluss', 'wechsel', 'zusatzspeicher', 'funktion', 'keine_frist', 'nachweis_fehlt']) assert.ok(loadTemplate('T09').blocks.includes(b), `T09 ${b}`)
|
||
|
||
const k03 = { ...sampleValues(loadTemplate('K03')), vertrag: 'E-Mail-Zusatzspeicher zum WEBklar Hosting-Abo', pakete: 2, pakete_neu: 1, vertragsende: '31.10.2026' }
|
||
const speicher = renderMail('K03', k03, { zusatzspeicher: true })
|
||
assert.deepEqual(speicher.problems, [])
|
||
assert.match(speicher.text, /Vertrag: E-Mail-Zusatzspeicher zum WEBklar Hosting-Abo/)
|
||
assert.match(speicher.text, /Ihr Zusatzspeicher verringert sich zum\n31\.10\.2026/)
|
||
assert.match(speicher.text, /um 2 Paket\(e\) gekündigt; danach bleiben 1 Paket\(e\)/)
|
||
assert.doesNotMatch(speicher.text, /Wartungs-Abo\n|Stundenkontingent|Stundenguthaben|Wir prüfen Ihre Kündigung/)
|
||
assert.match(renderMail('K03', k03, { pruefen: true }).text, /bestätigen Ihnen den Zeitpunkt, zu dem sie wirkt, gesondert per E-Mail/)
|
||
// Task 12 (Fix 1, 6): das Stundenkontingent gibt es nur im Wartungs-Abo (Block wartung innerhalb von abo); es ist
|
||
// kein Geldguthaben (v4 § 4 Abs. 1), darum „nicht genutzte Arbeitszeit“ statt „Stundenguthaben“
|
||
assert.match(renderMail('K03', k03, { abo: true, wartung: true }).text, /Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit Vertragsende\./)
|
||
assert.doesNotMatch(renderMail('K03', k03, { abo: true }).text, /Stundenkontingent|Stundenguthaben/)
|
||
assert.doesNotMatch(renderMail('K03', k03, { abo: true, wartung: true }).text, /Stundenguthaben/)
|
||
assert.match(renderMail('K03', k03, { abo: true }).text, /Domain-AuthCode/)
|
||
|
||
const k04 = { ...sampleValues(loadTemplate('K04')), bestellung: 'Wechsel ins Wartungs-Abo' }
|
||
assert.match(renderMail('K04', k04, { wechsel: true }).text, /Widerruf der Bestellung „Wechsel ins Wartungs-Abo“/)
|
||
// Task 12: Wortlaut der Hinweise zum Widerruf (Fassung 4)
|
||
assert.match(renderMail('K04', k04, { wechsel: true }).text, /zu den bisherigen Bedingungen weiter/)
|
||
assert.match(renderMail('K04', k04, { zusatzspeicher: true }).text, /Menge vor der Bestellung/)
|
||
assert.match(renderMail('K04', k04, { abschluss: true }).text, /keine weiteren Abbuchungen/)
|
||
assert.match(renderMail('K04', k04, { pruefen: true }).text, /prüfen, ob für diese Bestellung eine Widerrufsfrist läuft/)
|
||
assert.doesNotMatch(renderMail('K04', k04, {}).text, /Wie es weitergeht|WEBklar Wartungs-Abo“/)
|
||
|
||
const t08 = sampleValues(loadTemplate('T08'))
|
||
assert.match(renderMail('T08', t08, { wechsel_ausstehend: true }).text, /Wechsel ausstehend – Speicher nach Abschluss des Wechsels von Hand reduzieren/)
|
||
assert.match(renderMail('T08', t08, { speicher_fehlgeschlagen: true }).text, /NICHT automatisch verringert/)
|
||
assert.match(renderMail('T08', t08, { kein_speicher: true }).text, /kein Zusatzspeicher gebucht/)
|
||
assert.match(renderMail('T08', t08, { nachweis_fehlt: true }).text, /Nachweis nicht gespeichert/)
|
||
|
||
const t09 = { ...sampleValues(loadTemplate('T09')), ereignisse: 'speicher-1: 0 → 2 Pakete\nspeicher-2: 1 → 3 Pakete' }
|
||
const speicherT09 = renderMail('T09', t09, { abo_gefunden: true, zusatzspeicher: true })
|
||
assert.match(speicherT09.text, /Menge zurück/)
|
||
assert.match(speicherT09.text, /speicher-1: 0 → 2 Pakete\nspeicher-2: 1 → 3 Pakete/)
|
||
assert.doesNotMatch(speicherT09.text, /Abo in Stripe sofort beenden/)
|
||
assert.match(renderMail('T09', t09, { abo_gefunden: true, abschluss: true }).text, /Abo in Stripe sofort beenden/)
|
||
assert.match(renderMail('T09', t09, { abo_gefunden: true, keine_frist: true }).text, /keine offene Frist gefunden/)
|
||
assert.match(renderMail('T09', t09, { abo_gefunden: true, wechsel: true }).text, /Preis zurück auf das Hosting-Abo/)
|
||
// Fix 1: Paketzahl in jedem Speicherpfad (K03/T08), Pruefung der Kuendigungsart, T09-Hinweise
|
||
assert.match(renderMail('K03', { ...k03, pakete_angabe: 'alle gebuchten Pakete' }, { speicher_angaben: true, pruefen: true }).text, /Pakete: alle gebuchten Pakete/)
|
||
assert.match(renderMail('K03', { ...k03, kuendigungsart: 'außerordentliche Kündigung' }, { speicher_kind_pruefung: true }).text, /Ihre außerordentliche Kündigung wird geprüft/)
|
||
const t08a = { ...t08, pakete_angabe: '2 Paket(e)', kuendigungsart: 'Außerordentliche Kündigung' }
|
||
assert.match(renderMail('T08', t08a, { speicher_angaben: true, wechsel_ausstehend: true }).text, /Pakete: 2 Paket\(e\)/)
|
||
assert.match(renderMail('T08', t08a, { wechsel_ausstehend: true }).text, /gekündigt: 2 Paket\(e\)/)
|
||
assert.match(renderMail('T08', t08a, { speicher_kind_pruefung: true }).text, /Außerordentliche Kündigung des Zusatzspeichers/)
|
||
const t09b = { ...t09, fristende: 'keine Frist gefunden', pruefhinweis: 'Ereignisse nicht lesbar – bitte von Hand prüfen' }
|
||
assert.match(renderMail('T09', t09b, { abo_gefunden: true }).text, /Widerrufsfrist bis: keine Frist gefunden/)
|
||
assert.match(renderMail('T09', t09b, { abo_gefunden: true, nicht_pruefbar: true }).text, /Ereignisse nicht lesbar – bitte von Hand prüfen/)
|
||
assert.match(renderMail('T09', t09b, { abo_gefunden: true, wechsel: true, wechsel_offen: true }).text, /Wechselrechnung noch offen – stornieren/)
|
||
assert.match(renderMail('T09', t09b, { abo_gefunden: true, abschluss_verspaetet: true }).text, /als ordentliche Kündigung behandeln/)
|
||
assert.match(renderMail('T09', t09b, { abo_gefunden: true, bereits_widerrufen: true }).text, /schon widerrufen/)
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Task 12: Mails je Produkt (Hosting-Abo, Wartungs-Abo), Vertragsunterlagen der zugestimmten Fassung,
|
||
// Paragraph zum Vertragsende je Fassung, Folgen des Widerrufs wie in den Hinweisen der Fassung 4, UptimeRobot
|
||
// ---------------------------------------------------------------------------
|
||
|
||
test('Neue Vorlagen vorhanden', () => {
|
||
for (const id of ['K12', 'K13', 'K14', 'T12', 'T13']) assert.ok(templateIds().includes(id), id)
|
||
})
|
||
|
||
test('K01 Hosting: 1 Postfach, keine Arbeitsstunde; Wartung: beliebig viele Postfächer', () => {
|
||
const base = { name: 'K', produkt: 'Hosting-Abo', preis: '20,00 € / Monat', vertragsschluss: '01.10.2026', leistungsbeginn: '01.10.2026',
|
||
mindestlaufzeit_bis: '01.10.2027', bedingungen_version: '2026-10-05-v4', zustimmung_zeitpunkt: '01.10.2026, 10:00', angebot_text: '' }
|
||
const h = renderMail('K01', base, { hosting: true, sofortbeginn: true })
|
||
assert.equal(h.subject, 'Ihr WEBklar Hosting-Abo – Vertragsbestätigung')
|
||
assert.ok(h.text.includes('1 E-Mail-Postfach') && !h.text.includes('Stunde Arbeitszeit'), h.text)
|
||
assert.ok(h.text.includes('20,00 € / Monat') && h.text.includes('gemäß § 19 UStG ohne Umsatzsteuer'))
|
||
assert.ok(h.text.includes('Ihr Hosting-Abo ist bestätigt.') && h.text.includes('Abschluss Ihres Hosting-Abos'))
|
||
assert.doesNotMatch(h.text, /Stundenkontingent/)
|
||
assert.deepEqual(h.problems, [])
|
||
const w = renderMail('K01', { ...base, produkt: 'Wartungs-Abo', preis: '50,00 € / Monat' }, { wartung: true, sofortbeginn: true })
|
||
assert.ok(w.text.includes('beliebig viele Postfächer'), w.text)
|
||
assert.ok(w.text.includes('1 Stunde Arbeitszeit je bezahltem Monat') && w.text.includes('Stundenkontingent'))
|
||
assert.doesNotMatch(w.text, /1 E-Mail-Postfach\n/)
|
||
assert.deepEqual(w.problems, [])
|
||
})
|
||
|
||
test('T02 und T07 erinnern an UptimeRobot', () => {
|
||
// F38: echte Platzhalter, keine Unstimmigkeiten
|
||
const t02 = renderMail('T02', { produkt: 'Hosting-Abo', email: 'k@example.invalid', kunde_name: 'K', appwrite_id: 'c1', stripe_abo_id: 'sub_1', angebot_text: '' }, {})
|
||
assert.deepEqual(t02.problems, [])
|
||
assert.ok(t02.subject.startsWith('[Abo] Neues Hosting-Abo') && t02.text.includes('Website als Monitor in UptimeRobot anlegen'), t02.text)
|
||
assert.equal(loadTemplate('T02').file, 'T02 Neues Abo.html')
|
||
assert.match(renderMail('T02', { produkt: 'Hosting-Abo', email: 'k@example.invalid', kunde_name: 'K', appwrite_id: 'c1', stripe_abo_id: 'sub_1', angebot_text: '' }, { nachweis_fehlt: true }).text, /Nachweis nicht gespeichert/)
|
||
const t07 = renderMail('T07', { produkt: 'Hosting-Abo', email: 'k@example.invalid', stripe_abo_id: 'sub_1', status: 'canceled', vertragsende_paragraph: '§ 11' }, {})
|
||
assert.deepEqual(t07.problems, [])
|
||
assert.ok(t07.text.includes('Monitor in UptimeRobot entfernen'), t07.text)
|
||
})
|
||
|
||
test('Keine Vorlage nennt fest „Wartungs-Abo“ im Betreff außer K12, K14 und T12', () => {
|
||
for (const id of templateIds()) if (!['K12', 'K14', 'T12'].includes(id)) assert.ok(!loadTemplate(id).subject.includes('Wartungs-Abo'), id)
|
||
})
|
||
|
||
test('K01-Anhang: Fassung 3 für v3-Abos, Fassung 4 sonst', () => {
|
||
// contractAttachmentFor(termsVersion) liefert { filename, path } und wird von sendContractConfirmation benutzt
|
||
assert.equal(contractAttachmentFor('2026-09-25-v3').filename, 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf')
|
||
assert.equal(contractAttachmentFor('2026-10-05-v4').filename, 'WEBklar-Abo-Vertragsunterlagen.pdf')
|
||
assert.ok(existsSync(contractAttachmentFor('2026-10-05-v4').path))
|
||
assert.ok(existsSync(contractAttachmentFor('2026-09-25-v3').path))
|
||
})
|
||
|
||
// Rechtsrunde: In Fassung 4 ist § 9 der Wechsel, das Vertragsende steht in § 11 (F08)
|
||
test('K02, K03, T05, T07: Paragraph zum Vertragsende aus der Fassung des Kunden', () => {
|
||
assert.equal(termsDocFor('2026-09-25-v3').endParagraph, '§ 9')
|
||
assert.equal(termsDocFor(null).endParagraph, '§ 9')
|
||
assert.equal(termsDocFor('2026-10-05-v4').endParagraph, '§ 11')
|
||
for (const id of ['K02', 'K03', 'T05', 'T07']) {
|
||
const tpl = loadTemplate(id)
|
||
assert.ok(tpl.placeholders.includes('vertragsende_paragraph'), id)
|
||
const v4 = renderMail(id, { ...sampleValues(tpl), vertragsende_paragraph: '§ 11' }, allBlocks(tpl, true))
|
||
assert.deepEqual(v4.problems, [], id)
|
||
assert.doesNotMatch(v4.text, /§ 9 der Vertragsbedingungen/, id)
|
||
assert.match(v4.text, /§ 11 der Vertragsbedingungen/, id)
|
||
assert.match(renderMail(id, { ...sampleValues(tpl), vertragsende_paragraph: '§ 9' }, allBlocks(tpl, true)).text, /§ 9 der Vertragsbedingungen/, id)
|
||
}
|
||
})
|
||
|
||
test('K02–K06, T02, T05–T09: Produkt aus dem Katalog statt fest Wartungs-Abo', () => {
|
||
for (const id of ['K02', 'K03', 'K04', 'K05', 'K06', 'T02', 'T05', 'T06', 'T07', 'T08', 'T09']) {
|
||
assert.ok(loadTemplate(id).placeholders.includes('produkt'), id)
|
||
}
|
||
const values = { name: 'K', produkt: 'Hosting-Abo', eingang_zeitpunkt: '01.10.2026, 10:00', vertragsende: '01.10.2027', vertragsende_paragraph: '§ 11' }
|
||
const k02 = renderMail('K02', values, {})
|
||
assert.deepEqual(k02.problems, [])
|
||
assert.equal(k02.subject, 'Bestätigung Ihrer Kündigung – WEBklar Hosting-Abo')
|
||
assert.match(k02.text, /Kündigung des WEBklar Hosting-Abos vom 01\.10\.2026, 10:00 Uhr/)
|
||
assert.doesNotMatch(k02.text, /Stundenkontingent|Abo verwalten|Wartungs-Abo/)
|
||
const k02w = renderMail('K02', { ...values, produkt: 'Wartungs-Abo' }, { wartung: true })
|
||
assert.match(k02w.text, /Nicht genutzte Zeit aus dem Stundenkontingent entfällt mit Vertragsende\./)
|
||
assert.equal(renderMail('K03', { ...sampleValues(loadTemplate('K03')), produkt: 'Wartungs-Abo' }, {}).subject, 'Eingangsbestätigung Ihrer Kündigung – WEBklar Wartungs-Abo')
|
||
assert.equal(renderMail('K04', { ...sampleValues(loadTemplate('K04')), produkt: 'Hosting-Abo' }, {}).subject, 'Eingangsbestätigung Ihres Widerrufs – WEBklar Hosting-Abo')
|
||
for (const id of ['T05', 'T06', 'T07', 'T08', 'T09']) {
|
||
const tpl = loadTemplate(id)
|
||
assert.match(renderMail(id, { ...sampleValues(tpl), produkt: 'Hosting-Abo' }, allBlocks(tpl, true)).text, /^Abo: Hosting-Abo$/m, id)
|
||
}
|
||
})
|
||
|
||
// F36: die Folgen des Widerrufs je Bestellung wie in den Hinweisen zum Widerruf der Fassung 4
|
||
test('K04: Folgen des Widerrufs je Bestellung (Abschluss, Wechsel, Zusatzspeicher, wir prüfen)', () => {
|
||
const values = { name: 'K', produkt: 'Hosting-Abo', bestellung: 'Abschluss Ihres Abos', eingang_zeitpunkt: '01.10.2026, 10:00', email: 'k@example.invalid', kundennummer: '' }
|
||
const abschluss = renderMail('K04', values, { abschluss: true })
|
||
assert.deepEqual(abschluss.problems, [])
|
||
assert.match(abschluss.text, /keine weiteren Abbuchungen/)
|
||
assert.match(abschluss.text, /Wertersatz/)
|
||
assert.match(abschluss.text, /Zusatzspeicher[^.]*erstatten wir vollständig; Wertersatz verlangen wir dafür nicht/)
|
||
const wechsel = renderMail('K04', { ...values, bestellung: 'Wechsel ins Wartungs-Abo' }, { wechsel: true }).text
|
||
assert.match(wechsel, /Hosting-Abo läuft zu den bisherigen Bedingungen weiter, als hätte es den Wechsel nicht gegeben, einschließlich seiner ursprünglichen Mindestlaufzeit/)
|
||
assert.match(wechsel, /Mehrbetrag/)
|
||
assert.match(wechsel, /abzüglich des Wertersatzes/)
|
||
assert.match(wechsel, /§ 357a Abs\. 2 BGB/)
|
||
const speicher = renderMail('K04', { ...values, bestellung: 'E-Mail-Zusatzspeicher' }, { zusatzspeicher: true }).text
|
||
assert.match(speicher, /Menge vor der Bestellung/)
|
||
assert.match(speicher, /erstatten wir vollständig/)
|
||
assert.match(speicher, /Wertersatz verlangen wir nicht/)
|
||
assert.doesNotMatch(speicher, /abzüglich/)
|
||
assert.match(renderMail('K04', values, { pruefen: true }).text, /prüfen, ob für diese Bestellung eine Widerrufsfrist läuft/)
|
||
})
|
||
|
||
test('K12: Monatspreis mit gebuchtem Zusatzspeicher (Block), Widerruf des Wechsels wie in Fassung 4', () => {
|
||
const values = sampleValues(loadTemplate('K12'))
|
||
const mit = renderMail('K12', values, { zusatzspeicher: true })
|
||
assert.deepEqual(mit.problems, [])
|
||
assert.match(mit.text, /einschließlich Ihres gebuchten E-Mail-Zusatzspeichers/)
|
||
const ohne = renderMail('K12', values, {})
|
||
assert.doesNotMatch(ohne.text, /einschließlich Ihres gebuchten/)
|
||
assert.match(ohne.text, /Widerrufen Sie den Wechsel, läuft Ihr Hosting-Abo zu den bisherigen Bedingungen weiter/)
|
||
assert.match(ohne.text, /Mehrbetrag/)
|
||
// K13: Widerruf des Zusatzspeichers ohne Wertersatz
|
||
const k13 = renderMail('K13', sampleValues(loadTemplate('K13')), { plus: true, bestellung: true })
|
||
assert.match(k13.text, /erstatten alles, was Sie dafür gezahlt haben\. Wertersatz verlangen wir nicht\./)
|
||
})
|
||
|
||
// T09: die Handgriffe je Bestellung passen zu den Hinweisen zum Widerruf (Zusatzspeicher ohne Wertersatz)
|
||
test('T09: Handgriffe je Bestellung ohne Wertersatz beim Zusatzspeicher', () => {
|
||
const t09 = { ...sampleValues(loadTemplate('T09')), produkt: 'Hosting-Abo' }
|
||
const speicher = renderMail('T09', t09, { abo_gefunden: true, zusatzspeicher: true }).text
|
||
assert.match(speicher, /vollständig erstatten/)
|
||
assert.doesNotMatch(speicher, /Wertersatz für die Tage abziehen/)
|
||
const wechsel = renderMail('T09', t09, { abo_gefunden: true, wechsel: true }).text
|
||
assert.match(wechsel, /Mehrbetrag/)
|
||
assert.match(wechsel, /Wertersatz/)
|
||
const abschluss = renderMail('T09', t09, { abo_gefunden: true, abschluss: true }).text
|
||
assert.match(abschluss, /Zusatzspeicher[^.]*vollständig/)
|
||
})
|
||
|
||
// Task 12, Fix round 1: K01 je Fassung (v3-Zeile „1 E-Mail-Postfach · 1 Stunde Arbeitszeit pro Monat“ im Block wartung_v3),
|
||
// Zustimmung einschließlich AVV; K12 „Heute fällig“ nennt den Zusatzspeicher nur im Block; T07 nennt die Arbeitszeit nur bei Wartung
|
||
test('K01: Block wartung_v3 mit der Zeile der Fassung 3, wartung mit der Fassung 4, Zustimmung einschließlich AVV', () => {
|
||
const base = { name: 'K', produkt: 'Wartungs-Abo', preis: '50,00 € / Monat', angebot_text: '', vertragsschluss: '01.10.2026', leistungsbeginn: '01.10.2026',
|
||
mindestlaufzeit_bis: '01.10.2027', bedingungen_version: '2026-09-25-v3', zustimmung_zeitpunkt: '01.10.2026, 10:07' }
|
||
assert.ok(loadTemplate('K01').blocks.includes('wartung_v3'), 'Block wartung_v3 deklariert')
|
||
const v3 = renderMail('K01', base, { wartung_v3: true, sofortbeginn: true })
|
||
assert.deepEqual(v3.problems, [])
|
||
assert.match(v3.text, /• 1 E-Mail-Postfach/)
|
||
assert.match(v3.text, /• 1 Stunde Arbeitszeit pro Monat/)
|
||
assert.match(v3.text, /Dort sehen Sie jederzeit Ihr aktuelles Stundenkontingent\./)
|
||
assert.doesNotMatch(v3.text, /beliebig viele|je bezahltem Monat|Zusatzspeicher/)
|
||
assert.match(v3.text, /Ihre Zustimmung zu den Vertragsbedingungen \(Version 2026-09-25-v3\) einschließlich des Auftragsverarbeitungsvertrags haben wir am 01\.10\.2026, 10:07 Uhr erhalten\./)
|
||
const v4 = renderMail('K01', { ...base, bedingungen_version: '2026-10-05-v4' }, { wartung: true, sofortbeginn: true })
|
||
assert.match(v4.text, /• beliebig viele Postfächer/)
|
||
assert.match(v4.text, /1 Stunde Arbeitszeit je bezahltem Monat/)
|
||
assert.doesNotMatch(v4.text, /1 E-Mail-Postfach|pro Monat/)
|
||
const h = renderMail('K01', { ...base, produkt: 'Hosting-Abo', preis: '20,00 € / Monat' }, { hosting: true, sofortbeginn: true })
|
||
assert.doesNotMatch(h.text, /Stundenkontingent|Stunde Arbeitszeit/)
|
||
})
|
||
|
||
test('K12: „Heute fällig“ nennt den Zusatzspeicher nur im Block zusatzspeicher', () => {
|
||
const values = sampleValues(loadTemplate('K12'))
|
||
assert.match(renderMail('K12', values, { zusatzspeicher: true }).text, /Monatsgebühr des Wartungs-Abos mit gebuchtem Zusatzspeicher, abzüglich der Gutschrift/)
|
||
const ohne = renderMail('K12', values, {}).text
|
||
assert.match(ohne, /Monatsgebühr des Wartungs-Abos, abzüglich der Gutschrift/)
|
||
assert.doesNotMatch(ohne, /mit gebuchtem Zusatzspeicher/)
|
||
})
|
||
|
||
test('T07: Hinweis auf nicht genutzte Arbeitszeit nur im Block wartung', () => {
|
||
const values = { produkt: 'Wartungs-Abo', email: 'k@example.invalid', stripe_abo_id: 'sub_1', status: 'canceled', vertragsende_paragraph: '§ 11' }
|
||
assert.ok(loadTemplate('T07').blocks.includes('wartung'), 'Block wartung deklariert')
|
||
const w = renderMail('T07', values, { wartung: true })
|
||
assert.deepEqual(w.problems, [])
|
||
assert.match(w.text, /Nicht genutzte Arbeitszeit aus dem Stundenkontingent verfällt mit dem Vertragsende\. Gebuchter Zusatzspeicher endet mit dem Abo\./)
|
||
const h = renderMail('T07', { ...values, produkt: 'Hosting-Abo' }, {})
|
||
assert.deepEqual(h.problems, [])
|
||
assert.match(h.text, /Gebuchter Zusatzspeicher endet mit dem Abo\./)
|
||
assert.doesNotMatch(h.text, /Arbeitszeit|Stundenkontingent|Restguthaben/)
|
||
})
|
||
|
||
// Etappe 4: das Admin-Panel ist abgeschaltet, Team-Mails fuehren ins Ticketsystem
|
||
const TEAM_FOOTER = ['T01', 'T02', 'T03', 'T04', 'T05', 'T06', 'T07', 'T08', 'T09', 'T10', 'T11', 'T12', 'T13', 'T14', 'T16']
|
||
|
||
test('keine Vorlage verlinkt admin.html oder nennt den Admin-Bereich', () => {
|
||
for (const id of EXPECTED) {
|
||
const source = readFileSync(new URL(loadTemplate(id).file, DIR), 'utf8')
|
||
assert.ok(!source.includes('admin.html'), `${id}: admin.html`)
|
||
assert.ok(!source.includes('Admin-Bereich'), `${id}: Admin-Bereich`)
|
||
}
|
||
})
|
||
|
||
test('Fusszeile der Team-Mails: Ticketsystem', () => {
|
||
for (const id of TEAM_FOOTER) {
|
||
const tpl = loadTemplate(id)
|
||
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true))
|
||
assert.match(mail.html, /Automatische Benachrichtigung aus dem WEBklar Kundenportal · <a class="mut" href="https:\/\/ticket\.webklar\.com\/customers"[^>]*>Ticketsystem<\/a>/, id)
|
||
}
|
||
})
|
||
|
||
test('T01: Knopf antwortet im Ticketsystem beim Kunden (team_link)', () => {
|
||
const mail = renderMail('T01', { kunde_name: 'Berta', nachricht: 'Hallo', team_link: 'https://ticket.webklar.com/customers/chat?kunde=c1' })
|
||
assert.deepEqual(mail.problems, [])
|
||
assert.match(mail.html, /<a href="https:\/\/ticket\.webklar\.com\/customers\/chat\?kunde=c1"[^>]*>Im Ticketsystem antworten →<\/a>/)
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Terminplaner (Task 9): G01–G08 an Gäste, K20 und K21 an den Betrieb
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const sourceOf = (id) => readFileSync(new URL(loadTemplate(id).file, DIR), 'utf8')
|
||
const FOOTER_SENTENCE = 'Diese E-Mail wurde im Auftrag von Wert-betrieb_name über den Terminplaner von WEBklar versendet.'
|
||
// Sie-Formen und Du-Formen (der Rechtstext der Widerrufsbelehrung ist ein Platzhalterwert und kommt hier nicht vor)
|
||
const SIE_FORM = /(^|[^\p{L}])(Sie|Ihr\p{L}*|Ihnen)(?![\p{L}-])/u
|
||
const DU_FORM = /(^|[^\p{L}])(du|dein\p{L}*|dir|dich)(?![\p{L}-])/iu
|
||
const anrede = (tpl, sie, extra = {}) => ({ ...allBlocks(tpl, true), ...extra, sie, du: !sie })
|
||
|
||
test('G-Vorlagen: Betreff ist {{betreff}}, Blöcke sie und du, Platzhalter für Farbe und Betriebsfuß', () => {
|
||
for (const id of GUEST) {
|
||
const tpl = loadTemplate(id)
|
||
assert.equal(tpl.subject, '{{betreff}}', id)
|
||
for (const block of ['sie', 'du']) assert.ok(tpl.blocks.includes(block), `${id}: Block ${block}`)
|
||
for (const p of ['betreff', 'farbe', 'betrieb_name', 'betrieb_adresse', 'betrieb_telefon', 'betrieb_email', 'impressum_url', 'datenschutz_url']) {
|
||
assert.ok(tpl.placeholders.includes(p), `${id}: ${p}`)
|
||
}
|
||
}
|
||
})
|
||
|
||
test('G-Vorlagen: neutrales Layout, Kopf in der Betriebsfarbe, Fuß mit Betrieb, Links und Satz über den Terminplaner', () => {
|
||
for (const id of GUEST) {
|
||
const tpl = loadTemplate(id)
|
||
const source = sourceOf(id)
|
||
assert.match(source, /<td[^>]*style="[^"]*background:\{\{farbe\}\}/, `${id}: Kopfbalken in {{farbe}}`)
|
||
assert.doesNotMatch(source, /<img|webklar-logo|project\.webklar\.com/i, `${id}: kein Bild, kein WEBklar-Logo, keine Portal-Adresse`)
|
||
assert.doesNotMatch(source, /https?:\/\/(?!www\.w3\.org)|google|tracking|pixel/i, `${id}: keine festen Adressen, keine Werbung, kein Tracking`)
|
||
for (const on of [true, false]) {
|
||
const mail = renderMail(id, sampleValues(tpl), { ...allBlocks(tpl, on), sie: true, du: false })
|
||
assert.deepEqual(mail.problems, [], id)
|
||
assert.ok(mail.text.includes(FOOTER_SENTENCE), `${id}: Fußsatz\n${mail.text}`)
|
||
for (const wert of ['Wert-betrieb_adresse', 'Wert-betrieb_telefon', 'Wert-betrieb_email']) assert.ok(mail.text.includes(wert), `${id}: ${wert}`)
|
||
assert.match(mail.html, /href="Wert-impressum_url"[^>]*>Impressum</, id)
|
||
assert.match(mail.html, /href="Wert-datenschutz_url"[^>]*>Datenschutz</, id)
|
||
assert.match(mail.html, /background:Wert-farbe/, id)
|
||
}
|
||
}
|
||
})
|
||
|
||
test('G-Vorlagen: jeder Anredetext steht in sie und du, Du-Mail ohne Sie-Formen und Sie-Mail ohne Du-Formen', () => {
|
||
for (const id of GUEST) {
|
||
const tpl = loadTemplate(id)
|
||
const du = renderMail(id, sampleValues(tpl), anrede(tpl, false))
|
||
const sie = renderMail(id, sampleValues(tpl), anrede(tpl, true))
|
||
assert.deepEqual([du.problems, sie.problems], [[], []], id)
|
||
assert.doesNotMatch(du.text, SIE_FORM, `${id}: Du-Fassung enthält Sie-Form\n${du.text}`)
|
||
assert.doesNotMatch(du.text, / Sie | Ihr /, id)
|
||
assert.doesNotMatch(sie.text, DU_FORM, `${id}: Sie-Fassung enthält Du-Form\n${sie.text}`)
|
||
assert.match(sie.text, SIE_FORM, `${id}: Sie-Fassung spricht den Gast an`)
|
||
assert.match(du.text, DU_FORM, `${id}: Du-Fassung spricht den Gast an`)
|
||
assert.notEqual(du.text, sie.text, id)
|
||
// ohne gesetzte Anrede erscheint kein Anredetext (der Server setzt genau einen der beiden Blöcke)
|
||
const neither = renderMail(id, sampleValues(tpl), { ...allBlocks(tpl, true), sie: false, du: false })
|
||
assert.doesNotMatch(neither.text, SIE_FORM, id)
|
||
assert.doesNotMatch(neither.text, DU_FORM, id)
|
||
}
|
||
})
|
||
|
||
test('G02: Du-Mail enthält im Text kein „ Sie “ und kein „Ihr “', () => {
|
||
const tpl = loadTemplate('G02')
|
||
const mail = renderMail('G02', sampleValues(tpl), { ...allBlocks(tpl, true), sie: false, du: true })
|
||
assert.deepEqual(mail.problems, [])
|
||
assert.doesNotMatch(mail.text, / Sie |Ihr /)
|
||
assert.doesNotMatch(mail.html, / Sie | Ihr /)
|
||
})
|
||
|
||
test('G01 und G08 enthalten keinen vom Gast eingegebenen Text (kein Platzhalter gast_name)', () => {
|
||
for (const id of ['G01', 'G08']) {
|
||
assert.ok(!loadTemplate(id).placeholders.includes('gast_name'), `${id}: Platzhalter`)
|
||
assert.ok(!sourceOf(id).includes('gast_name'), `${id}: Quelltext`)
|
||
}
|
||
// ... die anderen Gästemails nennen den Gast beim Namen
|
||
for (const id of ['G02', 'G03', 'G04', 'G05', 'G06', 'G07']) assert.ok(loadTemplate(id).placeholders.includes('gast_name'), id)
|
||
})
|
||
|
||
test('G-Vorlagen: Platzhalter und Blöcke wie im Plan', () => {
|
||
const common = ['betreff', 'farbe', 'betrieb_name', 'betrieb_adresse', 'betrieb_telefon', 'betrieb_email', 'impressum_url', 'datenschutz_url']
|
||
const expected = {
|
||
G01: { p: ['leistung', 'datum', 'zeit', 'link'], b: ['bestaetigen', 'zu_viele'] },
|
||
G02: { p: ['gast_name', 'leistung', 'person', 'datum', 'zeit', 'dauer', 'preis', 'zahlung', 'nummer', 'link', 'ort', 'storno_text', 'widerrufsbelehrung', 'widerruf_link'], b: ['preis', 'person', 'storno', 'widerruf', 'kein_widerruf', 'anfrage_angenommen'] },
|
||
G03: { p: ['gast_name', 'leistung', 'datum', 'zeit', 'link'], b: [] },
|
||
G04: { p: ['gast_name', 'leistung', 'datum', 'zeit', 'nachricht'], b: ['nachricht'] },
|
||
G05: { p: ['gast_name', 'leistung', 'person', 'datum', 'zeit', 'link'], b: ['person'] },
|
||
G06: { p: ['gast_name', 'leistung', 'datum', 'zeit', 'absage_von', 'grund', 'nachricht'], b: ['grund', 'nachricht'] },
|
||
G07: { p: ['gast_name', 'leistung', 'alt_datum', 'alt_zeit', 'datum', 'zeit', 'link'], b: [] },
|
||
G08: { p: ['eingang', 'nummer'], b: ['zugeordnet', 'nach_frist'] },
|
||
}
|
||
for (const [id, { p, b }] of Object.entries(expected)) {
|
||
const tpl = loadTemplate(id)
|
||
assert.deepEqual([...tpl.placeholders].sort(), [...common, ...p].sort(), `${id}: Platzhalter`)
|
||
assert.deepEqual([...tpl.blocks].sort(), ['du', 'sie', ...b].sort(), `${id}: Blöcke`)
|
||
}
|
||
})
|
||
|
||
test('G01: Block bestaetigen mit Angaben und Knopf, Block zu_viele nur mit eigenem Text und ohne Knopf', () => {
|
||
const tpl = loadTemplate('G01')
|
||
const values = { ...sampleValues(tpl), betrieb_name: 'Salon Muster', betrieb_telefon: '06301 1234', leistung: 'Herrenschnitt', datum: 'Montag, 12.10.2026', zeit: '10:00', link: 'https://project.webklar.com/termin.html#v=abc' }
|
||
const sie = renderMail('G01', values, { sie: true, bestaetigen: true })
|
||
assert.deepEqual(sie.problems, [])
|
||
assert.match(sie.text, /Leistung: Herrenschnitt/)
|
||
assert.match(sie.text, /Datum: Montag, 12\.10\.2026/)
|
||
assert.match(sie.text, /Uhrzeit: 10:00 Uhr/)
|
||
assert.match(sie.text, /Termin bestätigen: https:\/\/project\.webklar\.com\/termin\.html#v=abc/)
|
||
assert.match(sie.text, /Das waren Sie nicht\? Dann ignorieren Sie diese E-Mail\./)
|
||
assert.doesNotMatch(sie.text, /höchstmögliche Zahl|rufen Sie uns an/)
|
||
assert.match(renderMail('G01', values, { du: true, bestaetigen: true }).text, /Das warst du nicht\? Dann ignoriere diese E-Mail\./)
|
||
|
||
// zu_viele: nichts reserviert, also weder Terminangaben noch Knopf noch Link
|
||
const viele = renderMail('G01', values, { sie: true, zu_viele: true })
|
||
assert.deepEqual(viele.problems, [])
|
||
assert.match(viele.text, /bereits die höchstmögliche Zahl offener Termine/)
|
||
assert.match(viele.text, /sagen Sie einen Ihrer offenen Termine ab/)
|
||
assert.match(viele.text, /rufen Sie uns an: 06301 1234\./)
|
||
assert.doesNotMatch(viele.text, /Herrenschnitt|12\.10\.2026|Uhrzeit|Termin bestätigen|termin\.html|ignorieren Sie den Knopf|Knopf/)
|
||
assert.doesNotMatch(viele.html, /termin\.html|Herrenschnitt/)
|
||
assert.match(renderMail('G01', values, { du: true, zu_viele: true }).text, /ruf uns an: 06301 1234\./)
|
||
|
||
// ohne gesetzten Block bleibt nur der gemeinsame Rahmen
|
||
const leer = renderMail('G01', values, { sie: true })
|
||
assert.doesNotMatch(leer.text, /Herrenschnitt|höchstmögliche Zahl|Termin bestätigen/)
|
||
})
|
||
|
||
test('G02: Zusammenfassung, Verwaltungslink, Preis, Person, Storno und Widerruf nur mit ihren Blöcken', () => {
|
||
const tpl = loadTemplate('G02')
|
||
const belehrung = 'Widerrufsbelehrung\n\nWiderrufsrecht: 14 Tage <b>ohne</b> Gründe.\nZweite Zeile & mehr'
|
||
const values = {
|
||
...sampleValues(tpl),
|
||
gast_name: 'Erika Muster', leistung: 'Herrenschnitt', person: 'Anna', datum: 'Montag, 12.10.2026', zeit: '10:00', dauer: '30 Minuten',
|
||
preis: '28,00 € inkl. USt', zahlung: 'Bezahlung vor Ort', nummer: 'AB12CD34', link: 'https://project.webklar.com/termin.html#m=tok', ort: 'Hauptstr. 1, 67722 Winnweiler',
|
||
storno_text: 'Absage bis 24 Stunden vorher kostenlos.', widerrufsbelehrung: belehrung, widerruf_link: 'https://project.webklar.com/widerrufen.html#m=tok',
|
||
}
|
||
const basis = renderMail('G02', values, { sie: true })
|
||
assert.deepEqual(basis.problems, [])
|
||
for (const zeile of ['Guten Tag Erika Muster,', 'Leistung: Herrenschnitt', 'Datum: Montag, 12.10.2026', 'Uhrzeit: 10:00 Uhr', 'Dauer: 30 Minuten', 'Ort: Hauptstr. 1, 67722 Winnweiler', 'Buchungsnummer: AB12CD34']) {
|
||
assert.ok(basis.text.split('\n').includes(zeile), `Zeile fehlt: ${zeile}\n${basis.text}`)
|
||
}
|
||
assert.match(basis.text, /Termin verwalten: https:\/\/project\.webklar\.com\/termin\.html#m=tok/)
|
||
assert.match(basis.text, /Ihr Termin ist bestätigt/)
|
||
assert.doesNotMatch(basis.text, /28,00|Bezahlung vor Ort|Anna|Absage bis 24 Stunden|Widerruf|angenommen/)
|
||
|
||
const voll = renderMail('G02', values, { sie: true, preis: true, person: true, storno: true, widerruf: true, anfrage_angenommen: true })
|
||
assert.deepEqual(voll.problems, [])
|
||
assert.match(voll.text, /Preis: 28,00 € inkl\. USt/)
|
||
assert.match(voll.text, /Zahlung: Bezahlung vor Ort/)
|
||
assert.match(voll.text, /Anna/)
|
||
assert.match(voll.text, /Absage bis 24 Stunden vorher kostenlos\./)
|
||
assert.match(voll.text, /Wert-betrieb_name hat Ihre Terminanfrage angenommen/)
|
||
assert.match(voll.text, /Vertrag widerrufen: https:\/\/project\.webklar\.com\/widerrufen\.html#m=tok/)
|
||
// Belehrung wörtlich, Zeilenumbrüche bleiben, Sonderzeichen sind maskiert
|
||
assert.match(voll.html, /Widerrufsrecht: 14 Tage <b>ohne<\/b> Gründe\.<br>Zweite Zeile & mehr/)
|
||
assert.ok(voll.text.includes('Widerrufsbelehrung\n\nWiderrufsrecht: 14 Tage <b>ohne</b> Gründe.\nZweite Zeile & mehr'), voll.text)
|
||
assert.doesNotMatch(voll.text, /kein Widerrufsrecht/, 'widerruf und kein_widerruf schließen sich aus')
|
||
// der Wortlaut der Belehrung steht in einem abgesetzten Kasten
|
||
assert.match(voll.html, /class="box"[^>]*><tr><td[^>]*>Widerrufsbelehrung<br><br>Widerrufsrecht/)
|
||
|
||
// Freizeitausnahme: Hinweis statt Widerrufsrecht
|
||
const kein = renderMail('G02', values, { sie: true, kein_widerruf: true })
|
||
assert.deepEqual(kein.problems, [])
|
||
assert.ok(kein.text.includes('Für diese Buchung besteht kein Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Absagen richten sich nach den Stornobedingungen.'), kein.text)
|
||
assert.doesNotMatch(kein.text, /Vertrag widerrufen|Widerrufsbelehrung|Muster-Widerrufsformular/)
|
||
const keinDu = renderMail('G02', values, { du: true, kein_widerruf: true })
|
||
assert.ok(keinDu.text.includes('Für diese Buchung besteht kein Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Absagen richten sich nach den Stornobedingungen.'), keinDu.text)
|
||
assert.doesNotMatch(keinDu.text, SIE_FORM)
|
||
assert.doesNotMatch(basis.text, /kein Widerrufsrecht/)
|
||
|
||
const du = renderMail('G02', values, { du: true, anfrage_angenommen: true, widerruf: true })
|
||
assert.match(du.text, /Dein Termin ist bestätigt/)
|
||
assert.match(du.text, /hat deine Terminanfrage angenommen/)
|
||
assert.doesNotMatch(du.text, SIE_FORM)
|
||
})
|
||
|
||
test('G03, G04, G05, G06, G07: Inhalt je Mail', () => {
|
||
const g03 = renderMail('G03', { ...sampleValues(loadTemplate('G03')), gast_name: 'Erika', betrieb_name: 'Salon Muster', link: 'https://x.example/m' }, { sie: true })
|
||
assert.match(g03.text, /Salon Muster prüft sie und meldet sich bei Ihnen/)
|
||
assert.match(g03.text, /Anfrage absagen: https:\/\/x\.example\/m/)
|
||
|
||
const t04 = loadTemplate('G04')
|
||
const g04 = renderMail('G04', { ...sampleValues(t04), nachricht: 'Am Montag sind wir leider im Urlaub.' }, { sie: true })
|
||
assert.doesNotMatch(g04.text, /Urlaub/)
|
||
assert.match(g04.text, /leider kann Wert-betrieb_name Ihre Terminanfrage nicht annehmen/)
|
||
assert.match(renderMail('G04', { ...sampleValues(t04), nachricht: 'Am Montag sind wir leider im Urlaub.' }, { sie: true, nachricht: true }).text, /Am Montag sind wir leider im Urlaub\./)
|
||
|
||
const t05 = loadTemplate('G05')
|
||
const g05 = renderMail('G05', { ...sampleValues(t05), person: 'Anna', link: 'https://x.example/m' }, { du: true, person: true })
|
||
assert.match(g05.text, /Anna/)
|
||
assert.match(g05.text, /Termin verwalten: https:\/\/x\.example\/m/)
|
||
assert.match(g05.text, /Erinnerung/)
|
||
assert.doesNotMatch(renderMail('G05', { ...sampleValues(t05), person: 'Anna' }, { du: true }).text, /Anna/)
|
||
|
||
const t06 = loadTemplate('G06')
|
||
const werte06 = { ...sampleValues(t06), absage_von: 'dem Betrieb', grund: 'Krankheit im Team', nachricht: 'Wir melden uns wegen eines neuen Termins.' }
|
||
const g06 = renderMail('G06', werte06, { sie: true })
|
||
assert.match(g06.text, /Abgesagt von: dem Betrieb/)
|
||
assert.doesNotMatch(g06.text, /Krankheit|Grund:|neuen Termins/)
|
||
const g06grund = renderMail('G06', werte06, { sie: true, grund: true })
|
||
assert.match(g06grund.text, /Grund: Krankheit im Team/)
|
||
assert.doesNotMatch(g06grund.text, /neuen Termins/)
|
||
const g06nachricht = renderMail('G06', werte06, { sie: true, nachricht: true })
|
||
assert.match(g06nachricht.text, /Nachricht von Wert-betrieb_name\s+Wir melden uns wegen eines neuen Termins\./)
|
||
assert.doesNotMatch(g06nachricht.text, /Krankheit/)
|
||
const beide = renderMail('G06', werte06, { sie: true, grund: true, nachricht: true })
|
||
assert.deepEqual(beide.problems, [])
|
||
assert.match(beide.text, /Krankheit im Team/)
|
||
assert.match(beide.text, /neuen Termins/)
|
||
|
||
const t07 = loadTemplate('G07')
|
||
const g07 = renderMail('G07', { ...sampleValues(t07), alt_datum: '12.10.2026', alt_zeit: '10:00', datum: '13.10.2026', zeit: '11:30' }, { du: true })
|
||
assert.match(g07.text, /Bisher: 12\.10\.2026, 10:00 Uhr/)
|
||
assert.match(g07.text, /Neu: 13\.10\.2026, 11:30 Uhr/)
|
||
})
|
||
|
||
test('G08: Eingangsbestätigung mit Datum und Uhrzeit, Blöcke zugeordnet und nach_frist, nichts vom Gast', () => {
|
||
const tpl = loadTemplate('G08')
|
||
const values = { ...sampleValues(tpl), betrieb_name: 'Salon Muster', eingang: '02.10.2026, 10:07 Uhr', nummer: 'AB12CD34' }
|
||
const ohne = renderMail('G08', values, { sie: true })
|
||
assert.deepEqual(ohne.problems, [])
|
||
assert.match(ohne.text, /Eingegangen am: 02\.10\.2026, 10:07 Uhr/)
|
||
assert.match(ohne.text, /Widerruf/)
|
||
assert.doesNotMatch(ohne.text, /AB12CD34|bereits abgelaufen/)
|
||
const zugeordnet = renderMail('G08', values, { sie: true, zugeordnet: true })
|
||
assert.match(zugeordnet.text, /Buchung AB12CD34/)
|
||
assert.match(zugeordnet.text, /Der Termin ist abgesagt\./)
|
||
assert.doesNotMatch(zugeordnet.text, /bereits abgelaufen/)
|
||
assert.doesNotMatch(zugeordnet.text, /keiner Buchung zuordnen/, 'zugeordnet widerspricht sich nicht selbst')
|
||
const spaet = renderMail('G08', values, { sie: true, nach_frist: true })
|
||
assert.match(spaet.text, /Nach unseren Unterlagen war die Widerrufsfrist bei Eingang Ihrer Erklärung bereits abgelaufen\./)
|
||
assert.doesNotMatch(spaet.text, /Der Termin ist abgesagt|AB12CD34|keiner Buchung zuordnen/)
|
||
// der Satz für alle Fälle ist in jedem Zustand wahr, in beiden Anreden
|
||
for (const blocks of [{}, { zugeordnet: true }, { nach_frist: true }]) {
|
||
assert.match(renderMail('G08', values, { sie: true, ...blocks }).text, /Salon Muster prüft Ihre Erklärung und meldet sich bei Ihnen, falls noch etwas zu klären ist\./)
|
||
assert.match(renderMail('G08', values, { du: true, ...blocks }).text, /Salon Muster prüft deine Erklärung und meldet sich bei dir, falls noch etwas zu klären ist\./)
|
||
}
|
||
assert.match(renderMail('G08', values, { du: true, nach_frist: true }).text, /Nach unseren Unterlagen war die Widerrufsfrist bei Eingang deiner Erklärung bereits abgelaufen\./)
|
||
// Hinweis für Dritte: wer nichts erklärt hat, kann die Mail ignorieren
|
||
assert.match(ohne.text, /Waren Sie das nicht, können Sie diese Nachricht ignorieren\./)
|
||
})
|
||
|
||
test('K20 und K21: WEBklar-Layout in Du-Form, Gastdaten maskiert, Blöcke je Fall', () => {
|
||
for (const id of ['K20', 'K21']) {
|
||
const tpl = loadTemplate(id)
|
||
assert.equal(tpl.subject, '{{betreff}}', id)
|
||
for (const p of ['betreff', 'kunde_name', 'leistung', 'person', 'datum', 'zeit', 'gast_name', 'gast_email', 'gast_telefon', 'gast_nachricht', 'portal_link']) assert.ok(tpl.placeholders.includes(p), `${id}: ${p}`)
|
||
assert.ok(!tpl.blocks.includes('sie') && !tpl.blocks.includes('du'), `${id}: Du-Form fest, keine Anredeblöcke`)
|
||
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true))
|
||
assert.doesNotMatch(mail.text, SIE_FORM, `${id}: Du-Form\n${mail.text}`)
|
||
assert.match(mail.text, /Hallo Wert-kunde_name,/, id)
|
||
assert.match(mail.html, /<a href="Wert-portal_link"/, id)
|
||
assert.doesNotMatch(sourceOf(id), /google|tracking|pixel/i, id)
|
||
}
|
||
assert.deepEqual([...loadTemplate('K20').blocks], ['anfrage'])
|
||
assert.deepEqual([...loadTemplate('K21').blocks].sort(), ['abgesagt', 'nach_frist', 'nicht_zugeordnet', 'verschoben', 'widerrufen'])
|
||
for (const p of ['alt_datum', 'alt_zeit', 'eingang']) assert.ok(loadTemplate('K21').placeholders.includes(p), `K21: ${p}`)
|
||
|
||
const t20 = loadTemplate('K20')
|
||
const werte20 = { ...sampleValues(t20), kunde_name: 'Berta', leistung: 'Herrenschnitt', datum: '12.10.2026', zeit: '10:00', gast_name: 'Max <b>Muster</b>', gast_email: 'max@example.com', gast_telefon: '0170 1', gast_nachricht: 'Bitte\nFenster', portal_link: 'https://project.webklar.com/dashboard.html' }
|
||
const buchung = renderMail('K20', werte20, {})
|
||
assert.deepEqual(buchung.problems, [])
|
||
assert.match(buchung.html, /Max <b>Muster<\/b>/)
|
||
assert.match(buchung.html, /Bitte<br>Fenster/)
|
||
assert.match(buchung.text, /E-Mail: max@example\.com/)
|
||
assert.match(buchung.text, /Im Portal ansehen: https:\/\/project\.webklar\.com\/dashboard\.html/)
|
||
assert.doesNotMatch(buchung.text, /noch nicht bestätigt/)
|
||
assert.match(renderMail('K20', werte20, { anfrage: true }).text, /Diese Anfrage ist noch nicht bestätigt/)
|
||
|
||
const t21 = loadTemplate('K21')
|
||
const werte21 = { ...sampleValues(t21), alt_datum: '11.10.2026', alt_zeit: '09:00', datum: '12.10.2026', zeit: '10:00', eingang: '02.10.2026, 10:07 Uhr' }
|
||
const fall = (block) => renderMail('K21', werte21, { [block]: true })
|
||
assert.match(fall('abgesagt').text, /hat den Termin abgesagt/)
|
||
assert.match(fall('verschoben').text, /Bisher: 11\.10\.2026, 09:00 Uhr/)
|
||
assert.match(fall('verschoben').text, /Neu: 12\.10\.2026, 10:00 Uhr/)
|
||
assert.match(fall('widerrufen').text, /Eingegangen am: 02\.10\.2026, 10:07 Uhr/)
|
||
assert.match(fall('widerrufen').text, /widerrufen/)
|
||
assert.match(fall('nach_frist').text, /Nach unseren Unterlagen war die Widerrufsfrist bei Eingang bereits abgelaufen/)
|
||
assert.match(fall('nicht_zugeordnet').text, /keiner Buchung zuordnen/)
|
||
// je Fall erscheint nur der eigene Abschnitt
|
||
const SATZ = { abgesagt: 'hat den Termin abgesagt', verschoben: 'hat den Termin verschoben', widerrufen: 'hat den Vertrag widerrufen', nach_frist: 'Nach unseren Unterlagen war die Widerrufsfrist bei Eingang bereits abgelaufen', nicht_zugeordnet: 'keiner Buchung zuordnen' }
|
||
for (const block of Object.keys(SATZ)) {
|
||
const mail = fall(block)
|
||
assert.deepEqual(mail.problems, [], block)
|
||
for (const [anderer, satz] of Object.entries(SATZ)) assert.equal(mail.text.includes(satz), anderer === block, `${block}: ${satz}`)
|
||
}
|
||
})
|