Files
Webklar-Kundenbereich/test/zeitenMails.test.js
T
ClaudeandClaude Opus 5.5 efc6dcf332 Öffnungszeiten: Abo-Abgleich mit 30 Tagen Frist
gleicheZeitenAboAb(now) koppelt die im Wartungs-Abo enthaltenen Zeiten
(unlockedBy abo) an das Abo (Spec 5, Z11): Abo-Ende erkennen (aboEndeAt, K26),
nach 30 Berliner Kalendertagen abschalten (abo_ende, K27, Ersatz-Ticket),
bei Rückkehr des Abos wieder einschalten (Neubau nach dem Abschalten, K24
wieder), 30 Tage nach dem Abschalten Daten löschen und state off.
Lesefehler beim Abo: Kunde überspringen, nie abschalten oder löschen.

R25: activate() leert aboEndeAt und behaltenSessionId; starteBehalten lehnt
bei laufendem Wartungs-Abo mit 409 ab (Lesefehler 503).
R17: k24Mail mit einbau null und wieder schaltet beide Einbau-Blöcke ab,
K24 bekommt dafür den Block weiter um den Kasten "Wie geht es weiter?".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:16:56 +00:00

387 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 K24–K27 (Ö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 { k24Mail, k25Mail, k26Mail, k27Mail, 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 = {
'K24 angestossen': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: false, ticketNr: 100123 }),
'K24 manuell mit Ticket': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'manuell', wieder: false, ticketNr: 100123 }),
'K24 wieder ohne Ticket': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: true }),
'K24 wieder ohne Neubau': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: null, wieder: true }),
'K25 ein Tag': () => k25Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', tage: [{ tag: '2026-10-03', name: 'Tag der Deutschen Einheit', art: 'feiertag' }] }),
'K25 mehrere mit prüfen': () =>
k25Mail({
kunde: KUNDE.name,
projekt: 'Praxis Muster',
tage: [
{ tag: '2026-12-24', name: 'Heiligabend', art: 'pruefen' },
{ tag: '2026-12-25', name: '1. Weihnachtstag', art: 'feiertag' },
],
}),
'K26 beendet': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'beendet', frist: FRIST, preis: '30 €' }),
'K26 hosting': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'hosting', frist: FRIST, preis: '30 €' }),
'K26 Wechsel widerrufen': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'wechsel_widerrufen', frist: FRIST, preis: '30 €' }),
K27: () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', loeschdatum: FRIST, preis: '30 €' }),
}
test('Vorlagen K24–K27: Datei, Betreff, Platzhalter und Blöcke stehen fest', () => {
const erwartet = {
K24: { file: 'K24 Zeiten eingeschaltet.html', subject: 'Öffnungszeiten live ist eingeschaltet', placeholders: ['kunde_name', 'projekt', 'ticket', 'portal_url'], blocks: ['angestossen', 'manuell', 'wieder', 'weiter'] },
K25: { file: 'K25 Zeiten Feiertage pruefen.html', subject: 'Feiertage stehen an – stimmen Deine Zeiten?', placeholders: ['kunde_name', 'projekt', 'tage', 'portal_url'], blocks: ['pruefen', 'mehrere'] },
K26: { file: 'K26 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'] },
K27: { file: 'K27 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(k27Mail({ kunde: KUNDE, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, 'Silke Müller')
for (const kunde of [undefined, null, '', ' ', {}, { name: null }]) {
assert.equal(k27Mail({ kunde, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, '', JSON.stringify(kunde))
}
})
test('K24: Text je nach einbau (angestossen: nach dem nächsten Veröffentlichen, manuell: das Team baut ein)', () => {
const auto = BEISPIELE['K24 angestossen']()
assert.equal(auto.template, 'K24')
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['K24 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 = k24Mail({ kunde: 'K', projekt: 'P', einbau })
assert.deepEqual(unklar.blocks, { angestossen: false, manuell: true, wieder: false, weiter: true }, String(einbau))
}
})
test('K24 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['K24 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(`<a href="${PORTAL.replaceAll('.', '\\.')}"`))
// mit Neubau (angestossen oder manuell) bleibt der Kasten, auch beim Wiedereinschalten
for (const einbau of ['angestossen', 'manuell']) {
const text = render(k24Mail({ kunde: 'K', projekt: 'P', einbau, wieder: true })).text
assert.match(text, /Wie geht es weiter/, einbau)
}
// einbau null ohne wieder ist kein Fall der Mail: es bleibt beim Vorsichtigen
assert.deepEqual(k24Mail({ kunde: 'K', projekt: 'P', einbau: null }).blocks, { angestossen: false, manuell: true, wieder: false, weiter: true })
})
test('K24: ohne Ticketnummer steht kein leeres Ticket in der Mail; wieder nennt das Abo', () => {
const ohne = k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell' })
assert.equal(ohne.values.ticket, '')
assert.doesNotMatch(render(ohne).text, /Ticket|\(\)|\s\./)
const leer = k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: ' ' })
assert.equal(leer.values.ticket, '')
assert.equal(k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: '#100124' }).values.ticket, ' (Ticket #100124)')
const wieder = BEISPIELE['K24 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['K24 angestossen']()).text, /Wartungs-Abo läuft wieder/)
})
test('K24: Betreff, Projekt und Knopf', () => {
const mail = render(BEISPIELE['K24 angestossen']())
assert.equal(mail.subject, 'Öffnungszeiten live ist eingeschaltet')
assert.match(mail.text, /Projekt: Praxis Muster/)
assert.match(mail.html, new RegExp(`<a href="${PORTAL.replaceAll('.', '\\.')}"`))
})
test('K25: eine Zeile je Tag mit Wochentag, Datum und Namen; „bitte prüfen“ und Mehrzahl nur bei Bedarf', () => {
const ein = BEISPIELE['K25 ein Tag']()
assert.equal(ein.template, 'K25')
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['K25 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\)<br>Fr 25\.12\./, 'Zeilenumbrüche werden zu <br>')
})
test('K25: Namen werden HTML-sicher eingesetzt, Wochentage stimmen über die Woche', () => {
const nachricht = k25Mail({
kunde: 'K',
projekt: 'P',
tage: [
{ tag: '2026-10-05', name: 'A <b>fett</b>', 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 &lt;b&gt;fett&lt;\/b&gt;/)
assert.doesNotMatch(mail.html, /<b>fett/)
})
test('K25 geht ohne BCC ans Team, K24 mit', async () => {
const k25 = await sendeZeitenMail(KUNDE, BEISPIELE['K25 ein Tag'](), { bccTeam: false })
assert.deepEqual(k25, { 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['K24 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('K26: je Variante nur der passende Grund, Frist, Preis und Behalten-Knopf', () => {
const faelle = [
['K26 beendet', { abo_ende: true, hosting: false, wechsel_widerrufen: false }, /Dein Wartungs-Abo ist beendet/, /Hosting-Abo|Wechsel/],
['K26 hosting', { abo_ende: false, hosting: true, wechsel_widerrufen: false }, /Hosting-Abo/, /Dein Wartungs-Abo ist beendet|widerrufen/],
['K26 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, 'K26')
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(k26Mail({ kunde: 'K', projekt: 'P', art: 'was-neues', frist: FRIST, preis: '30 €' }).blocks, { abo_ende: true, hosting: false, wechsel_widerrufen: false })
assert.equal(render(BEISPIELE['K26 beendet']()).subject, 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?')
})
test('K26/K27: 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(k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-30T22:30:00.000Z'), preis: '30 €' }).values.frist, '30.10.2026')
assert.equal(k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-31T23:30:00.000Z'), preis: '30 €' }).values.frist, '01.11.2026')
assert.throws(() => k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('kaputt'), preis: '30 €' }), TypeError)
assert.throws(() => k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', preis: '30 €' }), TypeError)
assert.throws(() => k27Mail({ kunde: 'K', projekt: 'P', loeschdatum: 'morgen', preis: '30 €' }), TypeError)
})
test('K27: Leisten weg, feste Zeiten, Daten bis zum Löschdatum, Wiedereinschalten für den Preis', () => {
const nachricht = BEISPIELE.K27()
assert.equal(nachricht.template, 'K27')
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('K26 und K27 gehen mit Kopie ans Team', async () => {
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE['K26 beendet']())).sent, true)
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K27())).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.K27()), { 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.K27())
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.K27())).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['K26 beendet']())
assert.equal(ergebnis.sent, false)
const text = logText()
assert.match(text, /K26/, '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.K27())
assert.doesNotMatch(logText(), /silke|Müller/i)
})
test('Erfolg schreibt nichts ins Log', async () => {
await sendeZeitenMail(KUNDE, BEISPIELE['K24 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)
assert.deepEqual(neu.blocks, { rechnung: true, 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, 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'])
// 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'])
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'), /gesundheit: false, abo: false, team: false, \.\.\.bloecke/)
})