\./)
assert.match(mail.html, /Wir verkaufen keine <Waffen>\.
Bitte melde Dich\./)
assert.doesNotMatch(mail.text, /299 €|UStG|angenommen/)
})
// ---------------------------------------------------------------------------
// Webshop E4: Käufermails G09–G14 (Sie-Form, Betreff in der Vorlage, Fuß mit Anbieter und Widerrufsfunktion)
// und K25, K26 an den Händler (Du-Form)
// ---------------------------------------------------------------------------
const SHOP_GEMEIN = ['farbe', 'betrieb_name', 'anbieter_zeile', 'impressum_url', 'datenschutz_url', 'widerruf_url']
test('G09–G15: Platzhalter, Blöcke und Betreff wie im Plan (G-Vorlagen des Webshops haben keine Anrede-Blöcke)', () => {
const erwartet = {
G09: {
s: 'Ihre Bestellung {{bestellnummer}} bei {{betrieb_name}}',
p: ['bestellnummer', 'datum', 'zahlungsdatum', 'positionen', 'zwischensumme', 'versand', 'gesamt', 'steuer_hinweis', 'lieferart_text', 'lieferadresse', 'abholort', 'lieferzeit', 'zahlungsart', 'ausschluss_text', 'agb', 'widerrufsbelehrung', 'widerrufsformular', 'gewaehrleistung'],
b: ['versand', 'abholung', 'ausschluss'],
},
G10: { s: 'Ihre Bestellung {{bestellnummer}} wurde versendet', p: ['bestellnummer', 'sendungsnummer', 'versanddienst'], b: ['sendung'] },
G11: { s: 'Ihre Bestellung {{bestellnummer}} ist abholbereit', p: ['bestellnummer', 'abholort', 'hinweis'], b: [] },
G12: { s: 'Eingangsbestätigung Ihres Widerrufs bei {{betrieb_name}}', p: ['name', 'bestellnummer', 'eingang', 'nachricht'], b: ['nachricht'] },
G13: { s: 'Erstattung zu Ihrer Bestellung {{bestellnummer}}', p: ['bestellnummer', 'betrag', 'grund'], b: ['grund'] },
G14: { s: 'Ihre Bestellung {{bestellnummer}} wurde nicht abgeschlossen', p: ['bestellnummer'], b: [] },
G15: { s: 'Wir haben Ihre Bestellung {{bestellnummer}} erhalten', p: ['bestellnummer', 'datum', 'positionen', 'gesamt'], b: [] },
}
assert.deepEqual(Object.keys(erwartet), SHOP_GUEST)
for (const [id, { s, p, b }] of Object.entries(erwartet)) {
const tpl = loadTemplate(id)
assert.equal(tpl.subject, s, id)
assert.deepEqual([...tpl.placeholders].sort(), [...p, ...SHOP_GEMEIN].sort(), `${id}: Platzhalter`)
assert.deepEqual([...tpl.blocks].sort(), [...b].sort(), `${id}: Blöcke`)
}
})
test('G09–G15: neutrales Layout in der Shop-Farbe, kein Logo, Fuß mit Anbieter, Impressum, Datenschutz und Widerrufsfunktion', () => {
for (const id of SHOP_GUEST) {
const tpl = loadTemplate(id)
const source = sourceOf(id)
assert.match(source, /]*style="[^"]*background:\{\{farbe\}\}/, `${id}: Kopfbalken in {{farbe}}`)
assert.doesNotMatch(source, / ]*>Impressum, id)
assert.match(mail.html, /href="Wert-datenschutz_url"[^>]*>Datenschutz, id)
assert.match(mail.html, /href="Wert-widerruf_url"[^>]*>Vertrag widerrufen, id)
assert.match(mail.html, /background:Wert-farbe/, id)
assert.doesNotMatch(mail.text, DU_FORM, `${id}: Sie-Form, nicht Du\n${mail.text}`)
}
}
})
const G_WERTE = { bestellnummer: '26-K7QM2X', betrieb_name: 'Töpferei ', farbe: '#112233', anbieter_zeile: 'Töpferei Muster, Hauptstr. 1, 12345 Musterstadt, info@example.com', impressum_url: 'https://shop.example/impressum', datenschutz_url: 'https://shop.example/datenschutz', widerruf_url: 'https://project.webklar.com/shop/sk/widerruf' }
const g = (id, werte, blocks = {}) => renderMail(id, { ...G_WERTE, ...werte }, blocks)
const G09_WERTE = {
datum: '05.10.2026',
zahlungsdatum: '07.10.2026',
positionen: '2 × Tasse (blau) – 24,00 €\n1 × Teller – 12,50 €',
zwischensumme: '36,50 €',
versand: '4,90 €',
gesamt: '41,40 €',
steuer_hinweis: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
lieferart_text: 'Versand nach Deutschland',
lieferadresse: 'Erika Beispiel\nMusterweg 3\n12345 Musterstadt',
abholort: 'Werkstatt, Hauptstr. 1',
lieferzeit: '2–4 Werktage',
zahlungsart: 'Kreditkarte',
ausschluss_text: 'Teller: Vom Widerruf ausgeschlossen: nach Kundenwunsch angefertigt',
agb: 'AGB-Text Zeile 1\nAGB-Text Zeile 2',
widerrufsbelehrung: 'Widerrufsbelehrung Zeile 1\nZeile 2',
widerrufsformular: 'An Töpferei Muster\nHiermit widerrufe ich',
gewaehrleistung: 'GESETZLICHE GEWÄHRLEISTUNG\n\nMindestens zwei Jahre',
}
test('G09 Versand: Betreff mit Bestellnummer und Shop, Positionen, Summen, Lieferadresse, alle Rechtstexte, Links', () => {
const mail = g('G09', G09_WERTE, { versand: true, abholung: false, ausschluss: true })
assert.deepEqual(mail.problems, [])
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X bei Töpferei ')
assert.match(mail.html, /Töpferei <Muster>/, 'Shopname maskiert')
assert.match(mail.html, /2 × Tasse \(blau\) – 24,00 € 1 × Teller – 12,50 €/)
assert.match(mail.html, /Erika Beispiel Musterweg 3 12345 Musterstadt/)
for (const teil of ['26-K7QM2X', '05.10.2026', '36,50 €', '4,90 €', '41,40 €', '§ 19 UStG', 'Versand nach Deutschland', '2–4 Werktage', 'Kreditkarte', 'Vom Widerruf ausgeschlossen: nach Kundenwunsch angefertigt', 'AGB-Text Zeile 2', 'Widerrufsbelehrung Zeile 1', 'Hiermit widerrufe ich', 'GESETZLICHE GEWÄHRLEISTUNG', 'Hinweis zur gesetzlichen Gewährleistung', 'Muster-Widerrufsformular', 'Allgemeine Geschäftsbedingungen']) {
assert.ok(mail.text.includes(teil), teil)
}
// I-1 b: ausdrückliche Annahme, Bestelldatum und Zahlungsdatum getrennt
assert.match(mail.text, /Mit dieser E-Mail nehmen wir Ihre Bestellung an\. Der Kaufvertrag ist damit geschlossen\./)
assert.match(mail.text, /Bestelldatum: 05\.10\.2026/)
assert.match(mail.text, /Zahlungsdatum: 07\.10\.2026/)
assert.match(mail.text, /Lieferadresse: Erika Beispiel/)
assert.doesNotMatch(mail.text, /Abholort|Werkstatt, Hauptstr/)
assert.match(mail.html, /href="https:\/\/project\.webklar\.com\/shop\/sk\/widerruf"/)
})
test('G09 Abholung ohne Ausschluss: Abholort statt Lieferadresse, kein Widerrufs-Ausschluss', () => {
const mail = g('G09', { ...G09_WERTE, lieferart_text: 'Abholung', versand: 'Abholung – kostenlos' }, { versand: false, abholung: true, ausschluss: false })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /Abholort: Werkstatt, Hauptstr\. 1/)
assert.doesNotMatch(mail.text, /Lieferadresse|Musterweg|Hinweis zum Widerrufsrecht|ausgeschlossen/)
assert.match(mail.text, /Abholung – kostenlos/)
})
test('G15: Eingang ohne Vertragsschluss - Lastschrift in Bearbeitung, Bestellbestätigung folgt, Positionen und Gesamt', () => {
const mail = g('G15', { datum: '05.10.2026', positionen: '2 × Tasse (blau) – 24,00 €', gesamt: '41,40 €' })
assert.deepEqual(mail.problems, [])
assert.equal(mail.subject, 'Wir haben Ihre Bestellung 26-K7QM2X erhalten')
assert.equal(loadTemplate('G15').file, 'G15 Bestellung eingegangen.html')
for (const teil of ['26-K7QM2X', '05.10.2026', '2 × Tasse (blau) – 24,00 €', '41,40 €', 'Ihre Zahlung per Lastschrift ist noch in Bearbeitung. Sobald sie bei uns eingegangen ist, erhalten Sie die Bestellbestätigung; erst damit kommt der Kaufvertrag zustande.']) {
assert.ok(mail.text.includes(teil), teil)
}
assert.doesNotMatch(mail.text, /Kaufvertrag ist damit geschlossen|Widerrufsbelehrung|Allgemeine Geschäftsbedingungen/)
})
test('G10: Sendungsnummer und Versanddienst nur im Block sendung', () => {
const mit = g('G10', { sendungsnummer: '0034<1>', versanddienst: 'DHL' }, { sendung: true })
assert.deepEqual(mit.problems, [])
assert.equal(mit.subject, 'Ihre Bestellung 26-K7QM2X wurde versendet')
assert.match(mit.text, /Versanddienst: DHL/)
assert.match(mit.text, /Sendungsnummer: 0034<1>/)
assert.match(mit.html, /0034<1>/)
const ohne = g('G10', { sendungsnummer: '', versanddienst: '' }, {})
assert.deepEqual(ohne.problems, [])
assert.doesNotMatch(ohne.text, /Sendungsnummer|Versanddienst/)
assert.match(ohne.text, /versendet/)
})
test('G11: Abholort und Hinweis', () => {
const mail = g('G11', { abholort: 'Werkstatt, Hauptstr. 1', hinweis: 'Di–Fr 10–16 Uhr' })
assert.deepEqual(mail.problems, [])
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X ist abholbereit')
assert.match(mail.text, /Abholort: Werkstatt, Hauptstr\. 1/)
assert.match(mail.text, /Hinweis: Di–Fr 10–16 Uhr/)
})
test('G12: Eingang mit Datum und Uhrzeit, Erklärung nur im Block nachricht, Händler meldet sich', () => {
const werte = { name: 'Erika ', bestellnummer: '26-ZZZZZZ', eingang: '05.10.2026, 14:03 Uhr', nachricht: 'Bitte stornieren.\nDanke' }
const mit = g('G12', werte, { nachricht: true })
assert.deepEqual(mit.problems, [])
assert.equal(mit.subject, 'Eingangsbestätigung Ihres Widerrufs bei Töpferei ')
assert.match(mit.text, /Guten Tag Erika ,/)
assert.match(mit.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
assert.match(mit.text, /Bestellnummer: 26-ZZZZZZ/)
assert.match(mit.html, /Bitte stornieren\. Danke/)
assert.match(mit.text, /meldet sich bei Ihnen/)
const ohne = g('G12', werte, {})
assert.doesNotMatch(ohne.text, /Bitte stornieren|Ihre Erklärung:/)
})
test('G13: Betrag, Grund nur im Block, Gutschrift über die ursprüngliche Zahlungsart', () => {
const mit = g('G13', { betrag: '12,50 €', grund: 'Artikel beschädigt' }, { grund: true })
assert.deepEqual(mit.problems, [])
assert.equal(mit.subject, 'Erstattung zu Ihrer Bestellung 26-K7QM2X')
assert.match(mit.text, /Erstatteter Betrag: 12,50 €/)
assert.match(mit.text, /Grund: Artikel beschädigt/)
assert.match(mit.text, /Die Gutschrift erfolgt über die ursprüngliche Zahlungsart\./)
assert.doesNotMatch(g('G13', { betrag: '12,50 €', grund: 'x' }, {}).text, /Grund:/)
})
test('G14: nichts berechnet, kein Betrag', () => {
const mail = g('G14', {})
assert.deepEqual(mail.problems, [])
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X wurde nicht abgeschlossen')
assert.match(mail.text, /wurde nicht abgeschlossen; es wurde nichts berechnet\./)
})
const K25_WERTE = { kunde_name: 'Erika', shopname: 'Töpferei', bestellnummer: '26-K7QM2X', gesamt: '41,40 €', positionen: '2 × Tasse – 24,00 €\n1 × Teller – 12,50 €', lieferart_text: 'Versand nach Deutschland', kaeufer_name: 'Max ', portal_url: 'https://portal.example/dashboard.html#webshop?projekt=p1' }
test('K25: Betreff, Platzhalter, Blöcke; Bestand-Warnung und Lastschrift-Hinweis nur im Block', () => {
const tpl = loadTemplate('K25')
assert.equal(tpl.file, 'K25 Neue Bestellung.html')
assert.equal(tpl.subject, 'Neue Bestellung {{bestellnummer}} in Deinem Webshop')
assert.deepEqual([...tpl.placeholders].sort(), Object.keys(K25_WERTE).sort())
assert.deepEqual(tpl.blocks, ['ueberverkauft', 'sepa'])
const normal = renderMail('K25', K25_WERTE, {})
assert.deepEqual(normal.problems, [])
assert.equal(normal.subject, 'Neue Bestellung 26-K7QM2X in Deinem Webshop')
assert.match(normal.text, /Hallo Erika/)
assert.match(normal.text, /Käufer: Max /)
assert.match(normal.html, /2 × Tasse – 24,00 € 1 × Teller – 12,50 €/)
assert.match(normal.html, /href="https:\/\/portal\.example\/dashboard\.html#webshop\?projekt=p1"/)
assert.doesNotMatch(normal.text, /Achtung|Lastschrift/)
const voll = renderMail('K25', K25_WERTE, { ueberverkauft: true, sepa: true })
assert.match(voll.text, /Achtung: Bestand reichte nicht/)
assert.match(voll.text, /Zahlung per Lastschrift/)
assert.doesNotMatch(renderMail('K25', K25_WERTE, { sepa: true }).text, /Achtung/)
assert.doesNotMatch(renderMail('K25', K25_WERTE, { ueberverkauft: true }).text, /Lastschrift/)
})
const K26_WERTE = { kunde_name: 'Erika', shopname: 'Töpferei', bestellnummer: '26-K7QM2X', kaeufer_name: 'Max ', kaeufer_email: 'max@example.com', eingang: '05.10.2026, 14:03 Uhr', nachricht: 'Ich widerrufe.\nBitte melden', portal_url: 'https://portal.example/dashboard.html#webshop?projekt=p1' }
test('K26: Betreff, Platzhalter, Erklärung nur im Block, Hinweis auf die 14 Tage', () => {
const tpl = loadTemplate('K26')
assert.equal(tpl.file, 'K26 Widerruf im Webshop.html')
assert.equal(tpl.subject, 'Widerruf in Deinem Webshop eingegangen')
assert.deepEqual([...tpl.placeholders].sort(), Object.keys(K26_WERTE).sort())
assert.deepEqual(tpl.blocks, ['nachricht'])
const mit = renderMail('K26', K26_WERTE, { nachricht: true })
assert.deepEqual(mit.problems, [])
assert.match(mit.text, /Hallo Erika/)
assert.match(mit.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
assert.match(mit.text, /E-Mail: max@example\.com/)
assert.match(mit.html, /Ich widerrufe\. Bitte melden/)
assert.match(mit.text, /innerhalb von 14 Tagen/)
assert.doesNotMatch(renderMail('K26', K26_WERTE, {}).text, /Ich widerrufe|Erklärung:/)
})
// ---------------------------------------------------------------------------
// Buchungskalender (Task 20): G16–G22 an Gäste (Sie-Form, neutral), K33, K34, K36 und K37 und T23 (Du-Form, WEBklar)
// ---------------------------------------------------------------------------
const UK_GAST = ['G16', 'G17', 'G18', 'G19', 'G20', 'G21', 'G22']
const UK_BASIS = ['gast_name', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'firma', 'farbe', 'anbieter_kontakt']
const UK_ERWARTET = {
G16: { p: [], b: [] },
G17: { p: ['posten_text', 'gesamt', 'frist', 'buchungs_link', 'stornobedingungen', 'nachricht_vermieter'], b: ['nachricht'] },
G18: { p: ['nachricht_vermieter'], b: ['nachricht'] },
G19: { p: ['posten_text', 'gesamt', 'zahlart_text', 'rechnung_nummer', 'stornobedingungen'], b: ['online_unbezahlt'] },
G20: { p: ['gesamt', 'rechnung_nummer'], b: [] },
G21: { p: ['frist'], b: [] },
G22: { p: ['rechnung_nummer', 'erstattung', 'nachricht_vermieter'], b: ['erstattung', 'nachricht'] },
}
test('G16–G22: Platzhalter und Blöcke, Sie-Form, kein WEBklar-Logo, keine festen Adressen', () => {
for (const id of UK_GAST) {
const tpl = loadTemplate(id)
assert.deepEqual([...tpl.placeholders].sort(), [...UK_BASIS, ...UK_ERWARTET[id].p].sort(), `${id}: Platzhalter`)
assert.deepEqual([...tpl.blocks].sort(), [...UK_ERWARTET[id].b].sort(), `${id}: Blöcke`)
assert.doesNotMatch(sourceOf(id), / {
const mail = renderMail('G17', sampleValues(loadTemplate('G17')), { nachricht: true })
for (const w of ['Wert-posten_text', 'Wert-gesamt', 'Wert-frist', 'Wert-stornobedingungen']) assert.ok(mail.text.includes(w), w)
assert.match(mail.html, /href="Wert-buchungs_link"[^>]*>Jetzt verbindlich buchen/)
assert.ok(mail.text.includes('Erst mit dem Klick auf „Zahlungspflichtig buchen“ auf der Buchungsseite ist Ihre Buchung verbindlich.'))
})
test('G19: dauerhafter Datenträger mit allen Angaben, Hinweis kein Widerrufsrecht, Bezahl-Hinweis nur im Block', () => {
const tpl = loadTemplate('G19')
const aus = renderMail('G19', sampleValues(tpl), { online_unbezahlt: false })
const an = renderMail('G19', sampleValues(tpl), { online_unbezahlt: true })
for (const w of ['Wert-posten_text', 'Wert-gesamt', 'Wert-zahlart_text', 'Wert-stornobedingungen', 'Wert-anbieter_kontakt', 'Wert-rechnung_nummer']) assert.ok(aus.text.includes(w), w)
assert.match(aus.text, /kein gesetzliches Widerrufsrecht/)
assert.ok(!aus.text.includes('Bezahl-Link'))
assert.ok(an.text.includes('Den Bezahl-Link finden Sie auf der Buchungsseite aus unserer vorigen E-Mail.'))
})
test('G22: Erstattung „wird veranlasst“ nur im Block', () => {
const tpl = loadTemplate('G22')
assert.ok(renderMail('G22', { ...sampleValues(tpl), erstattung: '50,00 €' }, { erstattung: true }).text.includes('Eine Erstattung von 50,00 € wird veranlasst.'))
assert.ok(!renderMail('G22', sampleValues(tpl), { erstattung: false }).text.includes('Erstattung von'))
})
test('K33–K37, T23: Dateien, Platzhalter, Du-Form', () => {
const erwartet = {
K36: ['kunde_name', 'gast_name', 'gast_email', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'nachricht_gast', 'portal_url'],
K37: ['kunde_name', 'gast_name', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'gesamt', 'zahlart_text', 'portal_url'],
K33: ['kunde_name', 'unterkunft', 'quelle', 'seit', 'portal_url'],
K34: ['kunde_name', 'projekt', 'preis', 'ust_hinweis', 'begruendung', 'portal_url'],
K35: ['kunde_name', 'projekt', 'loeschdatum', 'portal_url'],
T23: ['kunde_name', 'email', 'projekt', 'nachricht', 'woms_url'],
}
for (const [id, p] of Object.entries(erwartet)) {
const tpl = loadTemplate(id)
assert.deepEqual([...tpl.placeholders].sort(), [...p].sort(), id)
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true))
assert.deepEqual(mail.problems, [], id)
}
assert.equal(loadTemplate('K37').file, 'K37 Neue Buchung.html')
assert.equal(loadTemplate('K35').file, 'K35 Buchungskalender abgeschaltet.html')
assert.equal(loadTemplate('K35').subject, 'Buchungskalender abgeschaltet – bitte sichere Deine Rechnungen')
assert.equal(loadTemplate('T23').subject, '[Buchungskalender] Neue Anfrage: {{kunde_name}}')
})
|