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>
387 lines
23 KiB
JavaScript
387 lines
23 KiB
JavaScript
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 <b>fett<\/b>/)
|
||
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/)
|
||
})
|