Files
Webklar-Kundenbereich/test/featureMails.test.js
T
ClaudeandClaude Sonnet 5.5 e200efb7e5 Mails K15/K16 je Funktion (News-Wortlaut eingefroren)
K15 (Bestellung eingegangen) und K16 (Funktion bereit) tragen den Wortlaut der
Funktion als Platzhalter (funktion, funktion_gen/akk/nom/pron/dein, preis_hinweis,
danach, bereit, knopf, portal_url) statt fest News-Pinnwand. siteFeatures baut die
Werte mit bestellMail/bereitMail aus dem Register-Eintrag (mails.*, setupEstimate,
returnHash) und dem Katalogpreis je feature.product. K15 geht ohne Anhaenge raus.
Die News-Mails bleiben Zeichen fuer Zeichen gleich (Test featureMails friert
Betreff und Text ein).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:56:45 +00:00

303 lines
13 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 } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Mails K15 (Bestellung eingegangen) und K16 (Funktion bereit) je Funktion (Task B9a). Der News-Wortlaut ist als Literal
// eingefroren (vor dem Umbau der Vorlagen aus den damaligen Mails uebernommen): "News-Mails bleiben woertlich".
// B9b ergaenzt T16 und T19 in dieser Datei. websiteProjects, workorders und customers legt das Setup-Skript nicht an: ohne Schema.
const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures') } })
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 reg = await import('../server/services/featureRegistry.js')
const sf = await import('../server/services/siteFeatures.js')
const { loadTemplate, renderMail } = await import('../server/services/mailTemplates.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { mailSink } = await import('./support/mailSink.js')
const mails = mailSink()
setMailTransportForTests(mails)
after(() => setMailTransportForTests(null))
// Vorlagenprobleme (unbekannte oder fehlende Platzhalter) meldet der Mailer nur als Warnung: hier mitschreiben
const warnungen = []
const origWarn = console.warn
console.warn = (...args) => {
if (String(args[0]).startsWith('[mailer] Vorlage')) warnungen.push(args.join(' '))
else origWarn(...args)
}
after(() => {
console.warn = origWarn
})
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin', email: 'kunde1@example.test' })
st.invoices.set('in_1', { id: 'in_1', object: 'invoice', hosted_invoice_url: 'https://invoice.stripe.test/i/1' })
const BASE = 'https://project.webklar.com'
const FOOTER = [
'Viele Grüße',
'Dein WEBklar-Team',
'',
`Kundenportal (${BASE}/dashboard.html)`,
'',
'WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler',
'Telefon 0176 23726355 · support@webklar.com',
'Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein',
'',
]
let zaehler = 0
/** Projekt (Kunde c1) anlegen und die Funktion bestellen: liefert { projectId, sessionId }. */
async function bestelle(key, projectName) {
zaehler += 1
const projectId = `bm${zaehler}`
const sessionId = `cs_bm${zaehler}`
aw.seed('websiteProjects', { $id: projectId, customerId: 'c1', projectName })
await sf.markOrdered({ customerId: 'c1', projectId, key, sessionId, consent: {} })
return { projectId, sessionId }
}
const paidEvent = (sessionId, projectId, key, extra = {}) => ({
id: `evt_${sessionId}`,
object: 'event',
type: 'checkout.session.completed',
data: {
object: { id: sessionId, object: 'checkout.session', mode: 'payment', payment_status: 'paid', metadata: { customerId: 'c1', projectId, featureKey: key }, ...extra },
},
})
/** Bestellung bezahlen (Stripe-Ereignis) und die K15-Mail an den Kunden liefern. */
async function bezahle(key, projectName, extra = {}) {
const { projectId, sessionId } = await bestelle(key, projectName)
const vor = mails.sent.length
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, key, extra))
const k15 = mails.sent.slice(vor).filter((m) => m.subject.startsWith('Deine Bestellung'))
assert.equal(k15.length, 1, `genau eine K15-Mail: ${mails.sent.slice(vor).map((m) => m.subject)}`)
return { projectId, sessionId, mail: k15[0] }
}
// ------------------------------------------------------------------------- News bleibt woertlich ---
test('News-Mails bleiben wörtlich: K15 mit Rechnung', async () => {
const { mail } = await bezahle('news', 'Bäckerei', { invoice: 'in_1' })
assert.equal(mail.subject, 'Deine Bestellung: News-Pinnwand')
assert.equal(
mail.text,
[
'WEBklar',
'',
'Bestätigung',
'',
'✓ Bestellung eingegangen',
'',
'Deine Bestellung: News-Pinnwand',
'',
'Hallo Kunde Eins,',
'vielen Dank für Deine Bestellung und Deine Zahlung. Wir richten die News-Pinnwand jetzt für Dein Projekt ein.',
'',
'Funktion: News-Pinnwand – Einrichtung',
'Projekt: Bäckerei',
'Einmalig bezahlt: 30,00 € (brutto, einmalig; die Nutzung ist kostenlos, es läuft kein Abo)',
'Einrichtung: meist innerhalb von 1–2 Wochen',
'',
'Wie geht es weiter?',
'Sobald die News-Pinnwand auf Deiner Website eingebaut ist, schalten wir sie im Kundenportal frei und Du bekommst eine weitere E-Mail. Danach kannst Du Beiträge im Portal schreiben.',
'',
'Rechnung ansehen: https://invoice.stripe.test/i/1',
'',
`Zum Kundenportal: ${BASE}/dashboard.html#news`,
'',
...FOOTER,
].join('\n')
)
assert.deepEqual(mail.to.map((t) => t.address), ['kunde1@example.test'])
})
test('News-Mails bleiben wörtlich: K15 ohne Rechnung', async () => {
const { mail } = await bezahle('news', 'Café')
assert.equal(mail.subject, 'Deine Bestellung: News-Pinnwand')
assert.equal(
mail.text,
[
'WEBklar',
'',
'Bestätigung',
'',
'✓ Bestellung eingegangen',
'',
'Deine Bestellung: News-Pinnwand',
'',
'Hallo Kunde Eins,',
'vielen Dank für Deine Bestellung und Deine Zahlung. Wir richten die News-Pinnwand jetzt für Dein Projekt ein.',
'',
'Funktion: News-Pinnwand – Einrichtung',
'Projekt: Café',
'Einmalig bezahlt: 30,00 € (brutto, einmalig; die Nutzung ist kostenlos, es läuft kein Abo)',
'Einrichtung: meist innerhalb von 1–2 Wochen',
'',
'Wie geht es weiter?',
'Sobald die News-Pinnwand auf Deiner Website eingebaut ist, schalten wir sie im Kundenportal frei und Du bekommst eine weitere E-Mail. Danach kannst Du Beiträge im Portal schreiben.',
'',
`Zum Kundenportal: ${BASE}/dashboard.html#news`,
'',
...FOOTER,
].join('\n')
)
assert.doesNotMatch(mail.text, /Rechnung ansehen/)
})
test('News-Mails bleiben wörtlich: K16', async () => {
const { projectId } = await bezahle('news', 'Bistro')
const doc = await sf.getFeatureDoc('c1', projectId, 'news')
aw.seed('workorders', { ...aw.docs('workorders').get(doc.ticketId), status: 'Closed' })
const vor = mails.sent.length
const done = await sf.runActivation({ customerId: 'c1' })
assert.deepEqual(done.map((d) => d.projectId), [projectId])
const gesendet = mails.sent.slice(vor)
assert.equal(gesendet.length, 1)
const [mail] = gesendet
assert.equal(mail.subject, 'Deine News-Pinnwand ist bereit')
assert.equal(
mail.text,
[
'WEBklar',
'',
'Bereit',
'',
'✓ News-Pinnwand',
'',
'Deine News-Pinnwand ist bereit',
'',
'Hallo Kunde Eins,',
'die News-Pinnwand ist auf Deiner Website eingebaut und freigeschaltet. Du kannst ab sofort Beiträge schreiben, sie erscheinen direkt bei Dir auf der Seite.',
'',
'Funktion: News-Pinnwand',
'Projekt: Bistro',
'',
`Beitrag schreiben: ${BASE}/dashboard.html#news`,
'',
...FOOTER,
].join('\n')
)
assert.deepEqual(mail.to.map((t) => t.address), ['kunde1@example.test'])
})
// ------------------------------------------------------------------------------------- Termine ---
test('Termine-Mails: K15 mit den Werten des Terminplaners', async () => {
const termine = config.features.termine.mails
const { mail } = await bezahle('termine', 'Salon Haarlinie', { invoice: 'in_1' })
assert.equal(mail.subject, 'Deine Bestellung: Terminplaner')
assert.doesNotMatch(mail.text, /News|Beitr/)
assert.doesNotMatch(mail.html, /News|Beitr/)
assert.match(mail.text, /Wir richten den Terminplaner jetzt für Dein Projekt ein\./)
assert.match(mail.text, /Funktion: Terminplaner – Einrichtung/)
assert.match(mail.text, /Projekt: Salon Haarlinie/)
assert.ok(mail.text.includes(`Einmalig bezahlt: 149,00 € (${termine.preisHinweis})`), mail.text)
assert.ok(
mail.text.includes(`Sobald der Terminplaner auf Deiner Website eingebaut ist, schalten wir ihn im Kundenportal frei und Du bekommst eine weitere E-Mail. ${termine.danach}`),
mail.text
)
assert.match(mail.text, /Rechnung ansehen: https:\/\/invoice\.stripe\.test\/i\/1/)
assert.ok(mail.text.includes(`Zum Kundenportal: ${BASE}/dashboard.html#termine`), mail.text)
assert.match(mail.html, new RegExp(`href="${BASE.replace(/\./g, '\\.')}/dashboard\\.html#termine"`))
assert.deepEqual(mail.to.map((t) => t.address), ['kunde1@example.test'])
})
test('Termine-Mails: K16 mit den Werten des Terminplaners', () => {
const termine = config.features.termine.mails
const nachricht = sf.bereitMail(reg.featureConfig('termine'), { kundeName: 'Kunde Eins', projekt: 'Salon Haarlinie' })
assert.equal(nachricht.template, 'K16')
const mail = renderMail(nachricht.template, nachricht.values, nachricht.blocks)
assert.deepEqual(mail.problems, [])
assert.equal(mail.subject, 'Dein Terminplaner ist bereit')
assert.doesNotMatch(mail.text, /News|Beitr/)
assert.doesNotMatch(mail.html, /News|Beitr/)
assert.match(mail.text, /Hallo Kunde Eins,\nder Terminplaner ist auf Deiner Website eingebaut und freigeschaltet\./)
assert.ok(mail.text.includes(termine.bereit), mail.text)
assert.match(mail.text, /Funktion: Terminplaner\nProjekt: Salon Haarlinie/)
assert.ok(mail.text.includes(`Termine öffnen: ${BASE}/dashboard.html#termine`), mail.text)
})
// ----------------------------------------------------------------------------- Platzhalter, Blöcke ---
test('Platzhalter-/Blocklisten exakt: K15 und K16', () => {
const k15 = loadTemplate('K15')
assert.deepEqual(k15.placeholders, [
'kunde_name',
'projekt',
'funktion',
'funktion_gen',
'funktion_akk',
'funktion_nom',
'funktion_pron',
'preis',
'preis_hinweis',
'zeit',
'danach',
'portal_url',
'rechnung_url',
])
assert.deepEqual(k15.blocks, ['rechnung'])
assert.equal(k15.subject, 'Deine Bestellung: {{funktion}}')
const k16 = loadTemplate('K16')
assert.deepEqual(k16.placeholders, ['kunde_name', 'projekt', 'funktion', 'funktion_dein', 'funktion_nom', 'bereit', 'knopf', 'portal_url'])
assert.deepEqual(k16.blocks, [])
assert.equal(k16.subject, '{{funktion_dein}} ist bereit')
})
test('Die Mail-Werte je Funktion decken genau die Platzhalter der Vorlagen ab', () => {
const ctx = { kundeName: 'Kunde Eins', projekt: 'Salon', invoiceUrl: 'https://invoice.stripe.test/i/1' }
for (const key of reg.featureKeys()) {
const feature = reg.featureConfig(key)
const k15 = sf.bestellMail(feature, ctx)
const k16 = sf.bereitMail(feature, ctx)
assert.equal(k15.template, 'K15')
assert.equal(k16.template, 'K16')
assert.deepEqual(Object.keys(k15.values).sort(), [...loadTemplate('K15').placeholders].sort(), `${key}: K15`)
assert.deepEqual(Object.keys(k16.values).sort(), [...loadTemplate('K16').placeholders].sort(), `${key}: K16`)
assert.deepEqual(k15.blocks, { rechnung: true })
assert.deepEqual(sf.bestellMail(feature, { ...ctx, invoiceUrl: '' }).blocks, { rechnung: false })
for (const nachricht of [k15, k16]) {
const mail = renderMail(nachricht.template, nachricht.values, nachricht.blocks)
assert.deepEqual(mail.problems, [], `${key}: ${nachricht.template}`)
}
// Preis aus dem Katalog der Funktion (feature.product), nicht aus News
assert.equal(k15.values.preis, `${(config[feature.product].amountCents / 100).toFixed(2).replace('.', ',')} €`)
assert.equal(k15.values.portal_url, `${config.publicBaseUrl}/dashboard.html#${feature.returnHash}`)
assert.equal(k16.values.portal_url, k15.values.portal_url)
}
})
// ------------------------------------------------------------------------------------ Anhänge ---
test('K15 ohne Anhänge: die Termine-Bestellung ruft keinen Registerhaken', async () => {
const termine = config.features.termine
const aufrufe = []
const alt = {}
const spion = (name) => (...args) => {
aufrufe.push(name)
return name === 'mails.anhaenge' ? [] : true
}
for (const name of ['verfuegbar', 'bestellpruefung', 'unterlagenBereit', 'belehrung', 'freischaltbar']) {
alt[name] = termine[name]
termine[name] = spion(name)
}
termine.mails.anhaenge = spion('mails.anhaenge')
try {
const { mail } = await bezahle('termine', 'Anhang-Test')
assert.ok(!mail.attachments || mail.attachments.length === 0, 'K15 trägt keinen Anhang')
assert.deepEqual(aufrufe, [])
} finally {
for (const name of Object.keys(alt)) {
if (alt[name] === undefined) delete termine[name]
else termine[name] = alt[name]
}
delete termine.mails.anhaenge
}
})
test('keine Vorlagenwarnungen in allen Abläufen dieser Datei', () => {
assert.deepEqual(warnungen, [])
})