import { test, after, beforeEach, afterEach, mock } from 'node:test'
import { readFileSync } from 'node:fs'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { useTestEnv } from './support/testEnv.js'
// Mails K27–K30 (Öffnungszeiten live: eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet), ihre Builder und
// sendeZeitenMail (server/services/zeiten/mails.js, Task 13) sowie die neuen Blöcke behalten/neu in K15 und abo/team in T16.
// Die allgemeinen Vorlagenregeln (Kopf, Platzhalter, Blöcke, Logo, Text ohne Entities, Du-Form) prüft test/mailTemplates.test.js.
const aw = await startAppwriteMock()
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { loadTemplate, renderMail } = await import('../server/services/mailTemplates.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const reg = await import('../server/services/featureRegistry.js')
const sf = await import('../server/services/siteFeatures.js')
const { k27Mail, k28Mail, k29Mail, k30Mail, sendeZeitenMail } = await import('../server/services/zeiten/mails.js')
after(() => setMailTransportForTests(null))
const KUNDE = { $id: 'c1', name: 'Silke Müller', email: 'silke@example.com' }
const PORTAL = `${config.publicBaseUrl}/dashboard.html#zeiten`
let sink
let log
beforeEach(() => {
sink = mailSink()
setMailTransportForTests(sink)
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}), info: mock.method(console, 'info', () => {}), log: mock.method(console, 'log', () => {}) }
})
afterEach(() => mock.restoreAll())
const logText = () => Object.values(log).flatMap((m) => m.mock.calls.map((aufruf) => aufruf.arguments.map(String).join(' '))).join('\n')
const render = (nachricht) => renderMail(nachricht.template, nachricht.values, nachricht.blocks)
const an = (nachricht) => Object.entries(nachricht.blocks || {}).filter(([, on]) => on).map(([name]) => name).sort()
// Ein Beispiel je Variante: genau diese Nachrichten gehen durch die Prüfung "Werte decken die Platzhalter ab"
const FRIST = new Date('2026-11-04T09:00:00.000Z')
const BEISPIELE = {
'K27 angestossen': () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: false, ticketNr: 100123 }),
'K27 manuell mit Ticket': () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'manuell', wieder: false, ticketNr: 100123 }),
'K27 wieder ohne Ticket': () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: true }),
'K27 wieder ohne Neubau': () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: null, wieder: true }),
'K28 ein Tag': () => k28Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', tage: [{ tag: '2026-10-03', name: 'Tag der Deutschen Einheit', art: 'feiertag' }] }),
'K28 mehrere mit prüfen': () =>
k28Mail({
kunde: KUNDE.name,
projekt: 'Praxis Muster',
tage: [
{ tag: '2026-12-24', name: 'Heiligabend', art: 'pruefen' },
{ tag: '2026-12-25', name: '1. Weihnachtstag', art: 'feiertag' },
],
}),
'K29 beendet': () => k29Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'beendet', frist: FRIST, preis: '30 €' }),
'K29 hosting': () => k29Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'hosting', frist: FRIST, preis: '30 €' }),
'K29 Wechsel widerrufen': () => k29Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'wechsel_widerrufen', frist: FRIST, preis: '30 €' }),
K30: () => k30Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', loeschdatum: FRIST, preis: '30 €' }),
}
test('Vorlagen K27–K30: Datei, Betreff, Platzhalter und Blöcke stehen fest', () => {
const erwartet = {
K27: { file: 'K27 Zeiten eingeschaltet.html', subject: 'Öffnungszeiten live ist eingeschaltet', placeholders: ['kunde_name', 'projekt', 'ticket', 'portal_url'], blocks: ['angestossen', 'manuell', 'wieder', 'weiter'] },
K28: { file: 'K28 Zeiten Feiertage pruefen.html', subject: 'Feiertage stehen an – stimmen Deine Zeiten?', placeholders: ['kunde_name', 'projekt', 'tage', 'portal_url'], blocks: ['pruefen', 'mehrere'] },
K29: { file: 'K29 Zeiten Abo beendet.html', subject: 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?', placeholders: ['kunde_name', 'projekt', 'frist', 'preis', 'portal_url'], blocks: ['abo_ende', 'hosting', 'wechsel_widerrufen'] },
K30: { file: 'K30 Zeiten abgeschaltet.html', subject: 'Öffnungszeiten live ist abgeschaltet', placeholders: ['kunde_name', 'projekt', 'loeschdatum', 'preis', 'portal_url'], blocks: [] },
}
for (const [id, soll] of Object.entries(erwartet)) {
const tpl = loadTemplate(id)
assert.equal(tpl.file, soll.file, id)
assert.equal(tpl.subject, soll.subject, id)
assert.deepEqual(tpl.placeholders, soll.placeholders, id)
assert.deepEqual(tpl.blocks, soll.blocks, id)
}
})
test('Builder decken genau die Platzhalter und Blöcke der Vorlage ab', () => {
for (const [name, bauen] of Object.entries(BEISPIELE)) {
const nachricht = bauen()
const tpl = loadTemplate(nachricht.template)
assert.deepEqual(Object.keys(nachricht.values).sort(), [...tpl.placeholders].sort(), `${name}: Platzhalter`)
for (const block of Object.keys(nachricht.blocks || {})) assert.ok(tpl.blocks.includes(block), `${name}: Block ${block} gehört zur Vorlage`)
const mail = render(nachricht)
assert.deepEqual(mail.problems, [], name)
assert.equal(nachricht.values.portal_url, PORTAL, `${name}: Knopf führt in den Editor`)
assert.equal(nachricht.values.kunde_name, 'Silke Müller', name)
assert.match(mail.text, /Hallo Silke Müller,/, name)
assert.ok(mail.text.includes(`: ${PORTAL}`), `${name}: Knopf mit Adresse im Text`)
}
})
test('Anrede: Kunde als Name oder als Kundendokument, ohne Namen bleibt der Wert leer (wie K22/K23)', () => {
assert.equal(k30Mail({ kunde: KUNDE, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, 'Silke Müller')
for (const kunde of [undefined, null, '', ' ', {}, { name: null }]) {
assert.equal(k30Mail({ kunde, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, '', JSON.stringify(kunde))
}
})
test('K27: Text je nach einbau (angestossen: nach dem nächsten Veröffentlichen, manuell: das Team baut ein)', () => {
const auto = BEISPIELE['K27 angestossen']()
assert.equal(auto.template, 'K27')
assert.deepEqual(auto.blocks, { angestossen: true, manuell: false, wieder: false, weiter: true })
const textAuto = render(auto).text
assert.match(textAuto, /nach dem nächsten Veröffentlichen/)
assert.doesNotMatch(textAuto, /baut unser Team/)
assert.match(textAuto, /Ticket #100123/, 'Ticketnummer steht in beiden Fällen in der Mail')
const hand = BEISPIELE['K27 manuell mit Ticket']()
assert.deepEqual(hand.blocks, { angestossen: false, manuell: true, wieder: false, weiter: true })
const textHand = render(hand).text
assert.match(textHand, /baut unser Team/)
assert.doesNotMatch(textHand, /nach dem nächsten Veröffentlichen/)
assert.match(textHand, /Ticket #100123/)
// ohne bekannten Einbau sagt die Mail das Vorsichtige: das Team baut ein (null gilt nur mit wieder als "kein Einbau nötig", siehe unten)
for (const einbau of [undefined, null, '', 'unbekannt']) {
const unklar = k27Mail({ kunde: 'K', projekt: 'P', einbau })
assert.deepEqual(unklar.blocks, { angestossen: false, manuell: true, wieder: false, weiter: true }, String(einbau))
}
})
test('K27 wieder ohne Neubau (einbau null, Abo kehrt vor dem Abschalten zurück): beide Einbau-Blöcke und der Kasten "Wie geht es weiter" entfallen', () => {
const ohne = BEISPIELE['K27 wieder ohne Neubau']()
assert.deepEqual(ohne.blocks, { angestossen: false, manuell: false, wieder: true, weiter: false })
const mail = render(ohne)
assert.deepEqual(mail.problems, [])
assert.match(mail.text, /Dein Wartungs-Abo läuft wieder/)
assert.match(mail.text, /Zeiten und Einstellungen sind unverändert/)
// nichts, was nach Einbau klingt (die Leisten waren nie weg)
assert.doesNotMatch(mail.text, /Wie geht es weiter|nach dem nächsten Veröffentlichen|baut unser Team|Ohne gespeicherte Zeiten|Ticket/)
// Knopf, Projekt und Funktion bleiben
assert.match(mail.text, /Projekt: Praxis Muster/)
assert.match(mail.html, new RegExp(` {
const ohne = k27Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell' })
assert.equal(ohne.values.ticket, '')
assert.doesNotMatch(render(ohne).text, /Ticket|\(\)|\s\./)
const leer = k27Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: ' ' })
assert.equal(leer.values.ticket, '')
assert.equal(k27Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: '#100124' }).values.ticket, ' (Ticket #100124)')
const wieder = BEISPIELE['K27 wieder ohne Ticket']()
assert.deepEqual(wieder.blocks, { angestossen: true, manuell: false, wieder: true, weiter: true })
const mitWieder = render(wieder).text
assert.match(mitWieder, /Dein Wartungs-Abo läuft wieder/)
assert.match(mitWieder, /wieder eingeschaltet/)
assert.match(mitWieder, /Zeiten und Einstellungen sind unverändert/)
assert.doesNotMatch(render(BEISPIELE['K27 angestossen']()).text, /Wartungs-Abo läuft wieder/)
})
test('K27: Betreff, Projekt und Knopf', () => {
const mail = render(BEISPIELE['K27 angestossen']())
assert.equal(mail.subject, 'Öffnungszeiten live ist eingeschaltet')
assert.match(mail.text, /Projekt: Praxis Muster/)
assert.match(mail.html, new RegExp(` {
const ein = BEISPIELE['K28 ein Tag']()
assert.equal(ein.template, 'K28')
assert.equal(ein.values.tage, 'Sa 03.10. – Tag der Deutschen Einheit')
assert.deepEqual(an(ein), [])
assert.deepEqual(ein.blocks, { pruefen: false, mehrere: false })
const textEin = render(ein).text
assert.match(textEin, /Betrifft: Sa 03\.10\. – Tag der Deutschen Einheit/)
assert.doesNotMatch(textEin, /bitte prüfen|mehrere Tage/)
assert.match(textEin, /Stimmen Deine Zeiten/)
assert.match(textEin, /Google/, 'Hinweis: bei Google auch eintragen')
const viele = BEISPIELE['K28 mehrere mit prüfen']()
assert.equal(viele.values.tage, 'Do 24.12. – Heiligabend (bitte prüfen)\nFr 25.12. – 1. Weihnachtstag')
assert.deepEqual(viele.blocks, { pruefen: true, mehrere: true })
const mail = render(viele)
assert.match(mail.text, /Betrifft: Do 24\.12\. – Heiligabend \(bitte prüfen\)\nFr 25\.12\. – 1\. Weihnachtstag/)
assert.match(mail.text, /mehrere Tage/)
assert.match(mail.text, /Silvester/, 'Erklärung der „bitte prüfen“-Tage')
assert.match(mail.html, /Heiligabend \(bitte prüfen\)
Fr 25\.12\./, 'Zeilenumbrüche werden zu
')
})
test('K28: Namen werden HTML-sicher eingesetzt, Wochentage stimmen über die Woche', () => {
const nachricht = k28Mail({
kunde: 'K',
projekt: 'P',
tage: [
{ tag: '2026-10-05', name: 'A fett', art: 'feiertag' },
{ tag: '2026-10-06', name: 'B', art: 'feiertag' },
{ tag: '2026-10-07', name: 'C', art: 'feiertag' },
{ tag: '2026-10-08', name: 'D', art: 'feiertag' },
{ tag: '2026-10-09', name: 'E', art: 'feiertag' },
{ tag: '2026-10-10', name: 'F', art: 'feiertag' },
{ tag: '2026-10-11', name: 'G', art: 'feiertag' },
],
})
assert.deepEqual(nachricht.values.tage.split('\n').map((zeile) => zeile.slice(0, 9)), ['Mo 05.10.', 'Di 06.10.', 'Mi 07.10.', 'Do 08.10.', 'Fr 09.10.', 'Sa 10.10.', 'So 11.10.'])
const mail = render(nachricht)
assert.match(mail.html, /A <b>fett<\/b>/)
assert.doesNotMatch(mail.html, /fett/)
})
test('K28 geht ohne BCC ans Team, K27 mit', async () => {
const k28 = await sendeZeitenMail(KUNDE, BEISPIELE['K28 ein Tag'](), { bccTeam: false })
assert.deepEqual(k28, { sent: true })
assert.equal(sink.sent.length, 1)
assert.deepEqual(sink.sent[0].to.map((t) => t.address), ['silke@example.com'])
assert.ok(!sink.sent[0].bcc, 'keine Kopie ans Team')
assert.equal(sink.sent[0].subject, 'Feiertage stehen an – stimmen Deine Zeiten?')
assert.deepEqual(await sendeZeitenMail(KUNDE, BEISPIELE['K27 angestossen']()), { sent: true })
assert.equal(sink.sent.length, 2)
assert.deepEqual(sink.sent[1].bcc.map((t) => t.address), config.chatNotify.emails, 'Standard: Kopie ans Team')
assert.ok(sink.sent[1].bcc.length > 0)
assert.equal(sink.sent[1].subject, 'Öffnungszeiten live ist eingeschaltet')
assert.match(sink.sent[1].html, /Hallo Silke Müller,/)
})
test('K29: je Variante nur der passende Grund, Frist, Preis und Behalten-Knopf', () => {
const faelle = [
['K29 beendet', { abo_ende: true, hosting: false, wechsel_widerrufen: false }, /Dein Wartungs-Abo ist beendet/, /Hosting-Abo|Wechsel/],
['K29 hosting', { abo_ende: false, hosting: true, wechsel_widerrufen: false }, /Hosting-Abo/, /Dein Wartungs-Abo ist beendet|widerrufen/],
['K29 Wechsel widerrufen', { abo_ende: false, hosting: false, wechsel_widerrufen: true }, /Wechsel.*widerrufen/, /Dein Wartungs-Abo ist beendet|Dein Abo ist ein Hosting-Abo/],
]
for (const [name, blocks, passt, fremd] of faelle) {
const nachricht = BEISPIELE[name]()
assert.equal(nachricht.template, 'K29')
assert.deepEqual(nachricht.blocks, blocks, name)
assert.equal(nachricht.values.frist, '04.11.2026', `${name}: Datum TT.MM.JJJJ`)
assert.equal(nachricht.values.preis, '30 €')
const text = render(nachricht).text
assert.match(text, passt, name)
assert.doesNotMatch(text, fremd, name)
assert.match(text, /04\.11\.2026/, name)
assert.match(text, /einmalig 30 €/, name)
assert.match(text, /Für 30 € behalten: .*#zeiten/, `${name}: Knopf zum Behalten`)
}
// unbekannte Lage: lieber die Standard-Fassung (Abo beendet) als gar kein Grund
assert.deepEqual(k29Mail({ kunde: 'K', projekt: 'P', art: 'was-neues', frist: FRIST, preis: '30 €' }).blocks, { abo_ende: true, hosting: false, wechsel_widerrufen: false })
assert.equal(render(BEISPIELE['K29 beendet']()).subject, 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?')
})
test('K29/K30: Datum aus Date (Berliner Tag), ungültiges Datum wirft früh', () => {
// Berliner Tag, nicht UTC: 22:30 UTC am 30.10. ist 23:30 MEZ (noch der 30.), 23:30 UTC am 31.10. ist 00:30 MEZ (schon der 1.11.)
assert.equal(k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-30T22:30:00.000Z'), preis: '30 €' }).values.frist, '30.10.2026')
assert.equal(k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-31T23:30:00.000Z'), preis: '30 €' }).values.frist, '01.11.2026')
assert.throws(() => k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('kaputt'), preis: '30 €' }), TypeError)
assert.throws(() => k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', preis: '30 €' }), TypeError)
assert.throws(() => k30Mail({ kunde: 'K', projekt: 'P', loeschdatum: 'morgen', preis: '30 €' }), TypeError)
})
test('K30: Leisten weg, feste Zeiten, Daten bis zum Löschdatum, Wiedereinschalten für den Preis', () => {
const nachricht = BEISPIELE.K30()
assert.equal(nachricht.template, 'K30')
assert.equal(nachricht.values.loeschdatum, '04.11.2026')
assert.equal(nachricht.values.preis, '30 €')
const mail = render(nachricht)
assert.equal(mail.subject, 'Öffnungszeiten live ist abgeschaltet')
assert.match(mail.text, /wieder feste Zeiten/)
assert.match(mail.text, /04\.11\.2026/)
assert.match(mail.text, /für einmalig 30 € wieder einschalten/)
assert.match(mail.text, /Projekt: Praxis Muster/)
})
test('K29 und K30 gehen mit Kopie ans Team', async () => {
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE['K29 beendet']())).sent, true)
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K30())).sent, true)
for (const mail of sink.sent) assert.ok(mail.bcc?.length > 0)
})
test('ohne E-Mail-Adresse: nichts senden, nichts werfen', async () => {
for (const kunde of [{ name: 'Silke' }, { name: 'Silke', email: '' }, { email: ' ' }, { email: null }, null, undefined]) {
assert.deepEqual(await sendeZeitenMail(kunde, BEISPIELE.K30()), { sent: false, reason: 'keine_adresse' }, JSON.stringify(kunde))
}
assert.equal(sink.sent.length, 0)
})
test('Versandfehler werden gemeldet, nie geworfen; der Grund nennt keine Adresse', async () => {
setMailTransportForTests({
sendMail: async () => {
throw new Error('550 Recipient silke@example.com rejected for Silke Müller')
},
})
const ergebnis = await sendeZeitenMail(KUNDE, BEISPIELE.K30())
assert.equal(ergebnis.sent, false)
assert.equal(ergebnis.reason, 'fehler')
assert.doesNotMatch(JSON.stringify(ergebnis), /silke|Müller/i)
// auch ohne SMTP-Konfiguration (sendCustomerMail wirft) und mit unbrauchbarer Nachricht
setMailTransportForTests(null)
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K30())).sent, false)
setMailTransportForTests(sink)
assert.equal((await sendeZeitenMail(KUNDE, undefined)).sent, false)
assert.equal((await sendeZeitenMail(KUNDE, { template: 'K99' })).sent, false)
assert.equal(sink.sent.length, 0)
})
test('Log enthält weder Adresse noch Namen', async () => {
setMailTransportForTests({
sendMail: async () => {
throw new Error('550 Recipient silke@example.com rejected for Silke Müller (Praxis Muster)')
},
})
const ergebnis = await sendeZeitenMail(KUNDE, BEISPIELE['K29 beendet']())
assert.equal(ergebnis.sent, false)
const text = logText()
assert.match(text, /K29/, 'das Log nennt die Vorlage')
assert.ok(text.length > 0)
assert.doesNotMatch(text, /silke|example\.com|Müller/i, text)
// auch der Fall ohne Adresse loggt höchstens ohne Namen
await sendeZeitenMail({ name: 'Silke Müller' }, BEISPIELE.K30())
assert.doesNotMatch(logText(), /silke|Müller/i)
})
test('Erfolg schreibt nichts ins Log', async () => {
await sendeZeitenMail(KUNDE, BEISPIELE['K27 angestossen']())
assert.equal(logText(), '')
})
// ---------------------------------------------------------------- K15 behalten, T16 abo/team ---
test('K15: Block behalten ersetzt den Einrichtungstext, ohne ihn bleibt die Mail wie gehabt', () => {
const feature = reg.featureConfig('zeiten')
const ctx = { kundeName: 'Silke Müller', projekt: 'Praxis Muster', invoiceUrl: 'https://invoice.stripe.test/i/1' }
const neu = sf.bestellMail(feature, ctx)
// anlagen, anhang und zustimmungen (TP2 F8) bleiben aus: die Oeffnungszeiten haben keine Rechtsdokumente zum Anhaengen
assert.deepEqual(neu.blocks, { rechnung: true, anlagen: false, anhang: false, zustimmungen: false, behalten: false, neu: true })
assert.deepEqual(sf.bestellMail(feature, { ...ctx, behalten: false }).blocks, neu.blocks)
const behalten = sf.bestellMail(feature, { ...ctx, behalten: true })
assert.deepEqual(behalten.blocks, { rechnung: true, anlagen: false, anhang: false, zustimmungen: false, behalten: true, neu: false })
assert.deepEqual(behalten.values, neu.values, 'dieselben Werte, nur der Wortlaut wechselt')
const textNeu = render(neu)
assert.deepEqual(textNeu.problems, [])
assert.match(textNeu.text, /Wir richten Öffnungszeiten live jetzt für Dein Projekt ein\./)
assert.match(textNeu.text, /Funktion: Öffnungszeiten live – Einrichtung/)
assert.match(textNeu.text, /Einrichtung: meist innerhalb weniger Tage/)
assert.match(textNeu.text, /Sobald Öffnungszeiten live auf Deiner Website eingebaut ist/)
assert.doesNotMatch(textNeu.text, /behält|behalten/)
const mail = render(behalten)
assert.deepEqual(mail.problems, [])
assert.equal(mail.subject, 'Deine Bestellung: Öffnungszeiten live')
assert.match(mail.text, /Hallo Silke Müller,\nvielen Dank für Deine Bestellung und Deine Zahlung\. Du behältst Öffnungszeiten live für Dein Projekt/)
assert.match(mail.text, /Funktion: Öffnungszeiten live – behalten/)
assert.match(mail.text, /Einmalig bezahlt: 30,00 €/)
assert.match(mail.text, /nichts neu eingebaut/)
assert.match(mail.text, /Rechnung ansehen: https:\/\/invoice\.stripe\.test\/i\/1/)
assert.ok(mail.text.includes(`Zum Kundenportal: ${config.publicBaseUrl}/dashboard.html#zeiten`))
// nichts vom Einrichtungsablauf in der Behalten-Fassung
assert.doesNotMatch(mail.text, /Wir richten|Einrichtung|Sobald|eingebaut ist/)
})
test('K15: die Blöcke behalten und neu stehen im Kopf der Vorlage', () => {
const tpl = loadTemplate('K15')
assert.deepEqual(tpl.blocks, ['rechnung', 'behalten', 'neu', 'anlagen', 'anhang', 'zustimmungen'])
// jeder Block erscheint, und in der Behalten-Fassung (neu aus) fehlt jeder Rest von zeit/danach
const behalten = renderMail('K15', Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`])), { behalten: true })
assert.deepEqual(behalten.problems, [])
assert.doesNotMatch(behalten.text, /Wert-zeit|Wert-danach/)
})
test('T16: Blöcke abo und team nennen den Weg ohne Zahlung', () => {
const tpl = loadTemplate('T16')
assert.deepEqual(tpl.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit', 'abo', 'team', 'anhang_fehlt'])
const werte = { kunde_name: 'Silke Müller', email: 'silke@example.com', projekt: 'Praxis Muster', funktion: 'Öffnungszeiten live', woid: '10042', sitekey: 'abc', sitzung: '', zustand: '', anleitung_url: '', gesundheit_text: '' }
const aus = { zahlung_fehlgeschlagen: false, ungeklaert: false, kein_abo: false, bereits_aktiv: false, anleitung: false, gesundheit: false }
const abo = renderMail('T16', werte, { ...aus, abo: true, team: false })
assert.deepEqual(abo.problems, [])
assert.match(abo.text, /im Wartungs-Abo enthalten/i)
assert.doesNotMatch(abo.text, /vom Team freigeschaltet/i)
const team = renderMail('T16', werte, { ...aus, abo: false, team: true })
assert.deepEqual(team.problems, [])
assert.match(team.text, /vom Team freigeschaltet/i)
assert.doesNotMatch(team.text, /im Wartungs-Abo enthalten/i)
const keiner = renderMail('T16', werte, aus)
assert.doesNotMatch(keiner.text, /im Wartungs-Abo enthalten|vom Team freigeschaltet/i)
// sendT16 führt beide Blöcke als Vorgabe (aus), damit jede Team-Mail sie nennt
assert.match(readFileSync(new URL('../server/services/siteFeatures.js', import.meta.url), 'utf8'), /abo: false,\s+team: false,\s+\.\.\.bloecke/)
})