Buchungskalender: Mails an Gäste, Vermieter und Team

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:12:47 +00:00
1 parent 118be44847
commit e15486263f
18 files changed
+1095 -6

No files matched your search

+74 -2
View File
@@ -8,7 +8,7 @@ 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, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); T21 Neue Webshop-Anfrage (Webshop B1, Team-Hinweis); K24 Webshop-Anfrage entschieden (Webshop B2, Kunde); G09–G14 Käufermails des Webshops (Bestellbestätigung, versendet, abholbereit, Eingang Widerruf, Erstattung, Zahlung fehlgeschlagen; Webshop E4, eigener Fuß statt Terminplaner-Fuß, Sie-Form) und K25, K26 an den Händler (neue Bestellung, Widerruf; Webshop E4); K27–K30 Öffnungszeiten live (eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet; Öffnungszeiten live Task 13); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9)
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
const EXPECTED = [...ids('G', 15), ...ids('K', 30), ...ids('T', 21)]
const EXPECTED = [...ids('G', 22), ...ids('K', 34), ...ids('T', 22)]
// GUEST = Gästemails des Terminplaners (gemeinsamer Fuß, Anrede-Blöcke); SHOP_GUEST = Käufermails des Webshops (G09–G14, siehe unten)
const GUEST = ids('G', 8)
const SHOP_GUEST = ['G09', 'G10', 'G11', 'G12', 'G13', 'G14', 'G15']
@@ -27,7 +27,7 @@ for (const id of EXPECTED) {
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), [Finanzen] (Bankzugang), [Terminplaner] (Betriebsmeldung) oder [Webshop] (neue Anfrage)
assert.equal(id.startsWith('T'), /^\[(Abo|Speicher|Datenschutz|Zeit|Finanzen|Terminplaner|Webshop)\]/.test(tpl.subject) || id === 'T01' || id === 'T16' || id === 'T17', 'Team-Hinweise ab T02 mit [Abo], [Speicher], [Datenschutz], [Zeit], [Finanzen], [Terminplaner] oder [Webshop] (T16 Funktion bestellt und T17 Repo zuordnen ohne Kennung)')
assert.equal(id.startsWith('T'), /^\[(Abo|Speicher|Datenschutz|Zeit|Finanzen|Terminplaner|Webshop|Buchungskalender)\]/.test(tpl.subject) || id === 'T01' || id === 'T16' || id === 'T17', 'Team-Hinweise ab T02 mit [Abo], [Speicher], [Datenschutz], [Zeit], [Finanzen], [Terminplaner], [Webshop] oder [Buchungskalender] (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')
@@ -1280,3 +1280,75 @@ test('K26: Betreff, Platzhalter, Erklärung nur im Block, Hinweis auf die 14 Tag
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), K31–K34 und T22 (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), /<img|webklar-logo|project\.webklar\.com/i, `${id}: neutral`)
for (const on of [true, false]) {
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, on))
assert.deepEqual(mail.problems, [], id)
assert.doesNotMatch(mail.text, DU_FORM, `${id}: Sie-Form\n${mail.text}`)
assert.match(mail.html, /background:Wert-farbe/, id)
}
}
})
test('G17: Preis, Frist, Stornobedingungen, Knopf und der Satz zur verbindlichen Buchung', () => {
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('K31–K34, T22: Dateien, Platzhalter, Du-Form', () => {
const erwartet = {
K31: ['kunde_name', 'gast_name', 'gast_email', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'nachricht_gast', 'portal_url'],
K32: ['kunde_name', 'gast_name', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'gesamt', 'zahlart_text', 'portal_url'],
K33: ['kunde_name', 'unterkunft', 'portal_url'],
K34: ['kunde_name', 'projekt', 'preis', 'ust_hinweis', 'begruendung', 'portal_url'],
T22: ['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('K32').file, 'K32 Neue Buchung.html')
assert.equal(loadTemplate('T22').subject, '[Buchungskalender] Neue Anfrage: {{kunde_name}}')
})