Mails K22, K23 und Hinweis auf das Ende der Funktionen in K02/K03

Terminplaner TP2 E3. K22 "Terminplaner beendet" (Blöcke abo_ende, hosting,
vor_einrichtung, daten) und K23 "Terminplaner läuft wieder" im Layout von K19,
Du-Form, Texte als Entwurf für die Mail-Überarbeitung. K02 und K03 bekommen den
Block funktionen (in K03 innerhalb von abo); routes/abo.js setzt ihn nur beim
Wartungs-Abo und nie bei der Speicher-Kündigung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 14:09:05 +00:00
1 parent eae6eee03c
commit 973135d2de
7 files changed
+282 -8

No files matched your search

+116 -2
View File
@@ -6,9 +6,9 @@ import { contractAttachmentFor, termsDocFor } from '../server/services/products.
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; T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b)
// 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)
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', 19), 'T20']
const EXPECTED = [...ids('G', 8), ...ids('K', 23), ...ids('T', 19), 'T20']
const GUEST = ids('G', 8)
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
@@ -114,6 +114,108 @@ test('T16, T19: Betreff, Platzhalter und Blöcke stehen in der Vorlage', () => {
}
})
// Terminplaner TP2 (E3): K22 „Terminplaner beendet“ je Grund eine Variante, K23 „läuft wieder“; Texte sind Entwurf für die Mail-Überarbeitung
const K22_WERTE = { kunde_name: 'Erika', projekt: 'Praxis Muster', enddatum: '03.10.2026', loeschdatum: '02.11.2026', preis: '149 €', portal_url: 'https://portal.example/#termine' }
const k22 = (blocks) => renderMail('K22', K22_WERTE, blocks)
test('K22, K23: Betreff, Platzhalter und Blöcke stehen in der Vorlage', () => {
const tpl22 = loadTemplate('K22')
assert.equal(tpl22.file, 'K22 Terminplaner beendet.html')
assert.equal(tpl22.subject, 'Dein Terminplaner wurde beendet – {{projekt}}')
assert.deepEqual(tpl22.placeholders, ['kunde_name', 'projekt', 'enddatum', 'loeschdatum', 'preis', 'portal_url'])
assert.deepEqual(tpl22.blocks, ['abo_ende', 'hosting', 'vor_einrichtung', 'daten'])
const tpl23 = loadTemplate('K23')
assert.equal(tpl23.file, 'K23 Terminplaner laeuft wieder.html')
assert.equal(tpl23.subject, 'Dein Terminplaner läuft wieder – {{projekt}}')
assert.deepEqual(tpl23.placeholders, ['kunde_name', 'projekt', 'portal_url'])
assert.deepEqual(tpl23.blocks, [])
assert.equal(k22({}).subject, 'Dein Terminplaner wurde beendet – Praxis Muster')
})
test('K22 abo_ende: nennt das beendete Wartungs-Abo, keine Erstattung', () => {
const mail = k22({ abo_ende: true })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /Wartungs-Abo/)
assert.match(mail.text, /03\.10\.2026/, 'Enddatum')
assert.doesNotMatch(mail.text, /erstatten/)
assert.doesNotMatch(mail.text, /Hosting-Abo/, 'nicht die Hosting-Variante')
assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt/, 'der Kasten kommt nur mit dem Block daten')
})
test('K22 hosting: das Hosting-Abo reicht für den Terminplaner nicht', () => {
const mail = k22({ hosting: true })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /Hosting-Abo/)
assert.match(mail.text, /Wartungs-Abo/, 'sagt, was der Terminplaner braucht')
assert.doesNotMatch(mail.text, /erstatten/)
assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt/)
})
test('K22 vor_einrichtung: Einrichtung wird voll erstattet, nennt den Preis, kein Löschkasten', () => {
const mail = k22({ vor_einrichtung: true })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /149 €/)
assert.match(mail.text, /erstatten/)
assert.match(mail.text, /vor der Einrichtung/)
assert.doesNotMatch(mail.text, /02\.11\.2026|Was jetzt gilt|Rückkehr|löschen/, 'es gibt noch keine Termindaten, also keinen Löschkasten')
})
test('K22 daten: Kasten „Was jetzt gilt“ mit Löschdatum, Gästen und Rückkehr', () => {
const mail = k22({ daten: true })
assert.deepEqual(mail.problems, [])
const t = mail.text
assert.match(t, /Was jetzt gilt/)
assert.match(t, /Bis 02\.11\.2026 kannst Du Deine Termine im Kundenportal ansehen und als CSV exportieren/)
assert.match(t, /Gäste können ihre Termine weiter absagen/)
assert.match(t, /Bestehende Termine bleiben eingetragen, und die Erinnerungen an Deine Gäste laufen bis zur Löschung weiter/)
assert.match(t, /Bitte informiere Deine Gäste selbst/)
assert.match(t, /Ab 02\.11\.2026 löschen wir die Termindaten/)
assert.match(t, /30 Tage/, 'Löschfrist 30 Tage nach dem Abschalten')
assert.match(t, /Rückkehr: Läuft bis dahin wieder ein Wartungs-Abo, schalten wir den Terminplaner automatisch und ohne Gebühr wieder ein/)
assert.match(t, /Nach der Löschung braucht es einen neuen Fragebogen und die Einrichtung für 149 € noch einmal/)
assert.doesNotMatch(t, /erstatten/)
})
test('K22: Anrede, Projekt und Knopf ins Kundenportal stehen in jeder Variante', () => {
const mail = k22({ abo_ende: true, daten: true })
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /Hallo Erika,/)
assert.match(mail.text, /Praxis Muster/)
assert.match(mail.text, /Zum Kundenportal: https:\/\/portal\.example\/#termine/)
assert.match(mail.html, /href="https:\/\/portal\.example\/#termine"/)
})
test('K23: nennt das Projekt und führt ins Kundenportal', () => {
const mail = renderMail('K23', { kunde_name: 'Erika', projekt: 'Praxis Muster', portal_url: 'https://portal.example/#termine' })
assert.deepEqual(mail.problems, [])
assert.equal(mail.subject, 'Dein Terminplaner läuft wieder – Praxis Muster')
assert.match(mail.text, /Hallo Erika,/)
assert.match(mail.text, /Praxis Muster/)
assert.match(mail.text, /Wartungs-Abo/)
assert.match(mail.text, /Zum Kundenportal: https:\/\/portal\.example\/#termine/)
assert.match(mail.html, /<a href="https:\/\/portal\.example\/#termine"/)
})
// Terminplaner TP2 (E3): K02/K03 sagen, dass die Funktionen mit dem Wartungs-Abo enden (Sie-Form wie der Rest der Vorlagen)
test('K02, K03: Block funktionen nennt Terminplaner und Löschfrist, nur mit Block (in K03 nur innerhalb von abo)', () => {
const k02 = { name: 'K', produkt: 'Wartungs-Abo', eingang_zeitpunkt: '01.10.2026, 10:00', vertragsende: '01.10.2027', vertragsende_paragraph: '§ 11' }
assert.ok(loadTemplate('K02').blocks.includes('funktionen'))
const an = renderMail('K02', k02, { wartung: true, funktionen: true })
assert.deepEqual(an.problems, [])
assert.match(an.text, /Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm\. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren\./)
assert.doesNotMatch(renderMail('K02', k02, { wartung: true }).text, /Terminplaner/)
assert.doesNotMatch(renderMail('K02', { ...k02, produkt: 'Hosting-Abo' }, {}).text, /Terminplaner|Wartungs-Abo/, 'Hosting-Abo: kein Hinweis')
const k03 = { ...sampleValues(loadTemplate('K03')), vertragsende_paragraph: '§ 11' }
assert.ok(loadTemplate('K03').blocks.includes('funktionen'))
const abo = renderMail('K03', k03, { abo: true, wartung: true, funktionen: true })
assert.deepEqual(abo.problems, [])
assert.match(abo.text, /Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm\. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren\./)
assert.doesNotMatch(renderMail('K03', k03, { abo: true, wartung: true }).text, /Terminplaner/)
// Speicher-Kündigung (abo aus): der Block steht innerhalb von abo und fällt mit ihm weg
assert.doesNotMatch(renderMail('K03', k03, { abo: false, funktionen: true }).text, /Terminplaner/)
})
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 &lt;b&gt;&amp;&quot;&#39; hat im Kundenportal geschrieben/)
@@ -816,6 +918,18 @@ test('G08: Eingangsbestätigung mit Datum und Uhrzeit, Blöcke zugeordnet und na
assert.match(ohne.text, /Waren Sie das nicht, können Sie diese Nachricht ignorieren\./)
})
// Terminplaner TP2 (E3): K22 und K23 stehen in Du-Form (K02/K03 bleiben Sie-Form wie ihr Rest)
test('K22, K23: Du-Form fest, kein Sie und kein Ihr, auch mit allen Blöcken', () => {
for (const id of ['K22', 'K23']) {
const tpl = loadTemplate(id)
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.text, DU_FORM, `${id}: spricht den Kunden mit Du an`)
}
})
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)