onFeaturePaid wertet den Fall aus markPaid aus. Trifft die Zahlung eine Funktion, die schon freigeschaltet, in Einrichtung oder abgeschaltet ist, geht nur eine Team-Meldung raus (T16 Block bereits_aktiv, bei Abo-Ende vor der Einrichtung T19), kein Ticket und keine K15. Beide Meldungen laufen genau einmal je Dokument: unter feature-meldung:<id> frisch lesen, Marker zahlungMeldung gemeldet erst nach gesendeter Mail. Scheitert der Versand, wirft onFeaturePaid (Webhook 500), Stripe wiederholt und der naechste Versuch sendet genau einmal. meldeErstattung ist die einzige Stelle fuer T19 (Zahlung nach dem Abo-Ende und spaeter die Meldung des Abo-Abgleichs), wirft nie. Bei Funktionen mit Wartungs-Abo-Pflicht zeigt T16 nach der Verbuchung den Block kein_abo, wenn das Abo nicht mehr laeuft; ein Lesefehler laesst den Block aus und blockiert nichts. customerOf und projectName sind exportiert. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
579 lines
27 KiB
JavaScript
579 lines
27 KiB
JavaScript
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 (Team-Hinweis je Funktion) und T19 (Erstattung faellig) 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] }
|
||
}
|
||
|
||
const failedEvent = (sessionId, projectId, key) => ({
|
||
id: `evt_f_${sessionId}`,
|
||
object: 'event',
|
||
type: 'checkout.session.async_payment_failed',
|
||
data: {
|
||
object: { id: sessionId, object: 'checkout.session', mode: 'payment', payment_status: 'unpaid', metadata: { customerId: 'c1', projectId, featureKey: key } },
|
||
},
|
||
})
|
||
|
||
/** Die T16-Mails (Team-Hinweis "Neue Funktion bestellt") ab einer Position von mails.sent. */
|
||
const t16Seit = (vor) => mails.sent.slice(vor).filter((m) => m.subject.startsWith('Neue Funktion bestellt'))
|
||
|
||
/** Bestellung bezahlen: die eine T16-Mail an das Team, dazu Projekt, Ticketnummer und Site-Key aus dem Dokument. */
|
||
async function t16Bezahlt(key, projectName) {
|
||
const { projectId, sessionId } = await bestelle(key, projectName)
|
||
const vor = mails.sent.length
|
||
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, key))
|
||
const t16 = t16Seit(vor)
|
||
assert.equal(t16.length, 1, `genau eine T16-Mail: ${mails.sent.slice(vor).map((m) => m.subject)}`)
|
||
const doc = await sf.getFeatureDoc('c1', projectId, key)
|
||
const woid = aw.docs('workorders').get(doc.ticketId).woid
|
||
return { projectId, sessionId, doc, woid, mail: t16[0] }
|
||
}
|
||
|
||
/** Zahlung fehlgeschlagen (SEPA): bestellen, dann async_payment_failed. */
|
||
async function t16Fehlgeschlagen(key, projectName) {
|
||
const { projectId, sessionId } = await bestelle(key, projectName)
|
||
const vor = mails.sent.length
|
||
await sf.handleFeatureCheckoutEvent(failedEvent(sessionId, projectId, key))
|
||
const t16 = t16Seit(vor)
|
||
assert.equal(t16.length, 1, `genau eine T16-Mail: ${mails.sent.slice(vor).map((m) => m.subject)}`)
|
||
return { projectId, mail: t16[0] }
|
||
}
|
||
|
||
/** Bezahlte Sitzung ohne Bestellung zum Projekt (Schlüssel der Funktion wie in den Metadaten, auch fremd oder leer): T16 mit Block "ungeklaert". */
|
||
async function t16Ungeklaert(sessionId, projectId, key) {
|
||
const vor = mails.sent.length
|
||
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, key))
|
||
const t16 = t16Seit(vor)
|
||
assert.equal(t16.length, 1, `genau eine T16-Mail: ${mails.sent.slice(vor).map((m) => m.subject)}`)
|
||
return t16[0]
|
||
}
|
||
|
||
const T16_FUSS = 'Automatische Benachrichtigung aus dem WEBklar Kundenportal · Ticketsystem (https://ticket.webklar.com/customers)'
|
||
|
||
// ------------------------------------------------------------------------- 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'])
|
||
})
|
||
|
||
test('News-Mails bleiben wörtlich: T16', async () => {
|
||
const { mail, woid, doc } = await t16Bezahlt('news', 'Bäckerei')
|
||
assert.equal(mail.subject, 'Neue Funktion bestellt: News-Pinnwand')
|
||
assert.equal(
|
||
mail.text,
|
||
[
|
||
'WEBklar',
|
||
'',
|
||
'Intern · Kundenportal',
|
||
'',
|
||
'Funktionen',
|
||
'',
|
||
'Neue Funktion bestellt: News-Pinnwand',
|
||
'',
|
||
'Kunde: Kunde Eins',
|
||
'E-Mail: kunde1@example.test',
|
||
'Projekt: Bäckerei',
|
||
`Ticket: ${woid}`,
|
||
`Site-Key: ${doc.siteKey}`,
|
||
'',
|
||
T16_FUSS,
|
||
'',
|
||
].join('\n')
|
||
)
|
||
assert.deepEqual(mail.to.map((t) => t.address), ['kenso@webklar.com', 'andrej@webklar.com'])
|
||
})
|
||
|
||
test('News-Mails bleiben wörtlich: T16 Zahlung fehlgeschlagen', async () => {
|
||
const { mail } = await t16Fehlgeschlagen('news', 'Bäckerei')
|
||
assert.equal(mail.subject, 'Neue Funktion bestellt: News-Pinnwand')
|
||
assert.equal(
|
||
mail.text,
|
||
[
|
||
'WEBklar',
|
||
'',
|
||
'Intern · Kundenportal',
|
||
'',
|
||
'Funktionen',
|
||
'',
|
||
'Neue Funktion bestellt: News-Pinnwand',
|
||
'',
|
||
'Kunde: Kunde Eins',
|
||
'E-Mail: kunde1@example.test',
|
||
'Projekt: Bäckerei',
|
||
'Ticket:',
|
||
'Site-Key:',
|
||
'',
|
||
'Zahlung fehlgeschlagen',
|
||
'Die Zahlung ist nicht eingegangen (zum Beispiel SEPA-Lastschrift abgelehnt). Es wurde nichts eingerichtet und kein Ticket angelegt; die Bestellung steht im Portal wieder auf „nicht bestellt“.',
|
||
'',
|
||
T16_FUSS,
|
||
'',
|
||
].join('\n')
|
||
)
|
||
})
|
||
|
||
test('News-Mails bleiben wörtlich: T16 bezahlt, aber nicht zuzuordnen', async () => {
|
||
const mail = await t16Ungeklaert('cs_unklar_news', 'pX_news', 'news')
|
||
assert.equal(mail.subject, 'Neue Funktion bestellt: News-Pinnwand')
|
||
assert.equal(
|
||
mail.text,
|
||
[
|
||
'WEBklar',
|
||
'',
|
||
'Intern · Kundenportal',
|
||
'',
|
||
'Funktionen',
|
||
'',
|
||
'Neue Funktion bestellt: News-Pinnwand',
|
||
'',
|
||
'Kunde: Kunde Eins',
|
||
'E-Mail: kunde1@example.test',
|
||
'Projekt: pX_news',
|
||
'Ticket:',
|
||
'Site-Key:',
|
||
'',
|
||
'Bezahlt, aber nicht zuzuordnen',
|
||
'Der Kunde hat bezahlt, die Zahlung passt aber zu keiner offenen Bestellung. Es wurde nichts eingerichtet und kein Ticket angelegt. Bitte von Hand prüfen: cs_unklar_news (Kunde c1, Projekt pX_news)',
|
||
'',
|
||
T16_FUSS,
|
||
'',
|
||
].join('\n')
|
||
)
|
||
})
|
||
|
||
// ------------------------------------------------------------------------------------- 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)
|
||
})
|
||
|
||
// -------------------------------------------------------------------- T16 je Funktion (Team-Hinweis) ---
|
||
test('Termine-T16: Betreff, Text und Knopf nennen den Terminplaner', async () => {
|
||
// c1 hat ein laufendes Wartungs-Abo: ohne es meldet T16 den Block kein_abo (Task B10b, test/featurePaid.test.js)
|
||
aw.seed('subscriptions', { $id: 'sub_w_mails', stripeSubscriptionId: 'sub_w_mails', customerId: 'c1', productKey: 'wartung', status: 'active' })
|
||
const { mail, projectId, woid, doc } = await t16Bezahlt('termine', 'Salon Haarlinie')
|
||
assert.equal(mail.subject, 'Neue Funktion bestellt: Terminplaner')
|
||
for (const teil of [mail.subject, mail.text, mail.html]) assert.doesNotMatch(teil, /News|Beitr/)
|
||
assert.match(mail.text, /Funktionen\n\nNeue Funktion bestellt: Terminplaner\n\nKunde: Kunde Eins\nE-Mail: kunde1@example\.test\nProjekt: Salon Haarlinie\n/)
|
||
assert.ok(mail.text.includes(`Ticket: ${woid}\nSite-Key: ${doc.siteKey}\n`), mail.text)
|
||
// der Knopf führt in das WOMS-Kundenpanel "Funktionen" mit geöffneter Anleitung dieses Projekts (anleitungsLink aus B8)
|
||
const link = `${config.womsBaseUrl}/customers/c1?tab=funktionen&anleitung=${projectId}`
|
||
assert.equal(link, reg.anleitungsLink('c1', projectId))
|
||
assert.ok(mail.text.includes(`Im Ticketsystem öffnen: ${link}`), mail.text)
|
||
assert.ok(mail.html.includes(`href="${link.replaceAll('&', '&')}"`), mail.html)
|
||
// die Warnblöcke bleiben bei einem ordentlich bezahlten Kauf aus (kein_abo nur ohne laufendes Abo, bereits_aktiv nur bei einer Zahlung auf ein vorhandenes Dokument: featurePaid.test.js; gesundheit füllt D4)
|
||
for (const warnung of ['Zahlung fehlgeschlagen', 'Bezahlt, aber nicht zuzuordnen', 'Kein laufendes Wartungs-Abo', 'Gesundheitsbereich prüfen', 'Bezahlt, aber schon vorhanden']) {
|
||
assert.ok(!mail.text.includes(warnung), `kein Block "${warnung}"`)
|
||
}
|
||
})
|
||
|
||
test('Termine-T16: Zahlung fehlgeschlagen und nicht zuzuordnen nennen den Terminplaner, ohne Knopf zur Anleitung', async () => {
|
||
const { mail: fehl } = await t16Fehlgeschlagen('termine', 'Salon Haarlinie')
|
||
assert.equal(fehl.subject, 'Neue Funktion bestellt: Terminplaner')
|
||
assert.match(fehl.text, /Neue Funktion bestellt: Terminplaner\n\nKunde: Kunde Eins/)
|
||
assert.match(fehl.text, /Zahlung fehlgeschlagen\nDie Zahlung ist nicht eingegangen/)
|
||
assert.doesNotMatch(fehl.text, /News|Beitr|Im Ticketsystem öffnen/, 'es wurde nichts bestellt: keine Anleitung')
|
||
|
||
const unklar = await t16Ungeklaert('cs_unklar_termine', 'pX_termine', 'termine')
|
||
assert.equal(unklar.subject, 'Neue Funktion bestellt: Terminplaner')
|
||
assert.match(unklar.text, /Bezahlt, aber nicht zuzuordnen\nDer Kunde hat bezahlt.*cs_unklar_termine \(Kunde c1, Projekt pX_termine\)\n/)
|
||
assert.doesNotMatch(unklar.text, /News|Beitr|Im Ticketsystem öffnen/)
|
||
})
|
||
|
||
test('T16 ohne bekannten Funktionsschlüssel: "Funktion", sonst leer', async () => {
|
||
for (const [sessionId, projectId, key] of [
|
||
['cs_ohne_schluessel', 'pY1', undefined],
|
||
['cs_fremder_schluessel', 'pY2', 'gibtesnicht'],
|
||
]) {
|
||
const mail = await t16Ungeklaert(sessionId, projectId, key)
|
||
assert.equal(mail.subject, 'Neue Funktion bestellt: Funktion', sessionId)
|
||
assert.match(mail.text, /Neue Funktion bestellt: Funktion\n\nKunde: Kunde Eins/)
|
||
assert.match(mail.text, /Ticket:\nSite-Key:\n/, 'Ticket und Site-Key leer')
|
||
assert.match(mail.text, new RegExp(`Bezahlt, aber nicht zuzuordnen\\nDer Kunde hat bezahlt.*${sessionId} \\(Kunde c1, Projekt ${projectId}\\)\\n`))
|
||
assert.doesNotMatch(mail.text, /News|Beitr|Im Ticketsystem öffnen/, sessionId)
|
||
}
|
||
})
|
||
|
||
test('T16 Blöcke kein_abo und bereits_aktiv: Wortlaut und Werte', () => {
|
||
const werte = { kunde_name: 'Kunde Eins', email: 'kunde1@example.test', projekt: 'Salon', funktion: 'Terminplaner', woid: '10042', sitekey: 'abc', sitzung: 'cs_test_1', zustand: 'aktiv', anleitung_url: '', gesundheit_text: '' }
|
||
const aus = { zahlung_fehlgeschlagen: false, ungeklaert: false, kein_abo: false, bereits_aktiv: false, anleitung: false, gesundheit: false }
|
||
const keinAbo = renderMail('T16', werte, { ...aus, kein_abo: true })
|
||
assert.deepEqual(keinAbo.problems, [])
|
||
assert.match(keinAbo.text, /Kein laufendes Wartungs-Abo\nDie Zahlung ist eingegangen und das Ticket 10042 ist angelegt, aber der Kunde hat kein laufendes Wartungs-Abo\. Die Freischaltung bleibt gesperrt\. Endet das Abo vor der Fertigstellung, wird die Einrichtung voll erstattet \(Mail „Erstattung fällig“\)\.\n/)
|
||
assert.doesNotMatch(keinAbo.text, /schon aktiv|nicht zuzuordnen/)
|
||
const aktiv = renderMail('T16', werte, { ...aus, bereits_aktiv: true })
|
||
assert.deepEqual(aktiv.problems, [])
|
||
assert.match(
|
||
aktiv.text,
|
||
/Bezahlt, aber schon vorhanden\nZahlung eingegangen, aber die Funktion war schon aktiv\. Es wurde kein Ticket angelegt und nichts eingerichtet\. Bitte prüfen: Einbau nachholen oder erstatten\. Sitzung: cs_test_1\n/
|
||
)
|
||
assert.doesNotMatch(aktiv.text, /Kein laufendes Wartungs-Abo/)
|
||
// ohne Block steht nichts davon in der Mail
|
||
const keiner = renderMail('T16', werte, aus)
|
||
assert.doesNotMatch(keiner.text, /Wartungs-Abo|schon aktiv|Im Ticketsystem öffnen|Gesundheitsbereich/)
|
||
})
|
||
|
||
test('T16 gesundheit: Block aus ohne Warntext, Block an mit dem Text', () => {
|
||
const werte = { kunde_name: 'Kunde Eins', email: 'kunde1@example.test', projekt: 'Salon', funktion: 'Terminplaner', woid: '10042', sitekey: 'abc', sitzung: '', zustand: '', anleitung_url: '', gesundheit_text: 'Branche Physiotherapie: Gesundheitsdaten möglich' }
|
||
const aus = renderMail('T16', werte, { gesundheit: false })
|
||
assert.deepEqual(aus.problems, [])
|
||
assert.doesNotMatch(aus.text, /Gesundheitsbereich prüfen|Physiotherapie/)
|
||
assert.doesNotMatch(aus.html, /Physiotherapie/)
|
||
const an = renderMail('T16', werte, { gesundheit: true })
|
||
assert.deepEqual(an.problems, [])
|
||
assert.ok(an.text.includes('Gesundheitsbereich prüfen: Branche Physiotherapie: Gesundheitsdaten möglich'), an.text)
|
||
})
|
||
|
||
test('T16 anleitung: Knopf nur mit Block, Link trägt &anleitung=<Projekt-ID>', () => {
|
||
const link = reg.anleitungsLink('c1', 'p42')
|
||
assert.ok(link.endsWith('&anleitung=p42'))
|
||
const werte = { kunde_name: 'Kunde Eins', email: 'kunde1@example.test', projekt: 'Salon', funktion: 'Terminplaner', woid: '10042', sitekey: 'abc', sitzung: '', zustand: '', anleitung_url: link, gesundheit_text: '' }
|
||
const an = renderMail('T16', werte, { anleitung: true })
|
||
assert.deepEqual(an.problems, [])
|
||
assert.ok(an.text.includes(`Im Ticketsystem öffnen: ${link}`), an.text)
|
||
assert.ok(an.html.includes('&anleitung=p42"'), an.html)
|
||
const aus = renderMail('T16', werte, { anleitung: false })
|
||
assert.doesNotMatch(aus.text, /Im Ticketsystem öffnen/)
|
||
assert.ok(!aus.text.includes('anleitung=p42'))
|
||
})
|
||
|
||
// ------------------------------------------------------------------------ T19 Erstattung fällig ---
|
||
test('T19 rendert beide Fälle', () => {
|
||
const werte = {
|
||
kunde_name: 'Kunde Eins',
|
||
email: 'kunde1@example.test',
|
||
projekt: 'Salon Haarlinie',
|
||
funktion: 'Terminplaner',
|
||
betrag: '149,00 €',
|
||
sitzung: 'cs_test_erstattung',
|
||
woid: '10042',
|
||
rechnung_url: 'https://invoice.stripe.test/i/1',
|
||
}
|
||
const gemeinsam = (mail) => {
|
||
assert.deepEqual(mail.problems, [])
|
||
assert.equal(mail.subject, '[Abo] Erstattung fällig: Terminplaner-Einrichtung')
|
||
for (const teil of ['Kunde: Kunde Eins', 'E-Mail: kunde1@example.test', 'Projekt: Salon Haarlinie', 'Betrag: 149,00 €', 'Sitzung: cs_test_erstattung']) {
|
||
assert.ok(mail.text.includes(teil), `T19 enthält: ${teil}\n---\n${mail.text}`)
|
||
}
|
||
// volle Erstattung von Hand, keine automatische Rückzahlung
|
||
assert.match(mail.text, /von Hand in Stripe/)
|
||
assert.match(mail.text, /Eine automatische Rückzahlung gibt es nicht\./)
|
||
assert.match(mail.html, /Automatische Benachrichtigung aus dem WEBklar Kundenportal · <a class="mut" href="https:\/\/ticket\.webklar\.com\/customers"[^>]*>Ticketsystem<\/a>/)
|
||
assert.doesNotMatch(mail.text, /News|Beitr/)
|
||
}
|
||
|
||
const nachEnde = renderMail('T19', werte, { zahlung_nach_ende: true, ticket_offen: false, rechnung: false })
|
||
gemeinsam(nachEnde)
|
||
assert.ok(nachEnde.text.includes('Die Zahlung ist erst nach dem Abo-Ende eingegangen, es wurde kein Ticket angelegt.'), nachEnde.text)
|
||
assert.doesNotMatch(nachEnde.text, /noch offen|10042|Rechnung ansehen/)
|
||
|
||
const ticketOffen = renderMail('T19', werte, { zahlung_nach_ende: false, ticket_offen: true, rechnung: false })
|
||
gemeinsam(ticketOffen)
|
||
assert.ok(ticketOffen.text.includes('Das Ticket 10042 ist noch offen.'), ticketOffen.text)
|
||
assert.match(ticketOffen.text, /Ticket mit einem Vermerk schließen/)
|
||
assert.match(ticketOffen.text, /schaltet dann nicht frei/)
|
||
assert.doesNotMatch(ticketOffen.text, /erst nach dem Abo-Ende|Rechnung ansehen/)
|
||
|
||
const mitRechnung = renderMail('T19', werte, { zahlung_nach_ende: true, ticket_offen: false, rechnung: true })
|
||
gemeinsam(mitRechnung)
|
||
assert.ok(mitRechnung.text.includes('Rechnung ansehen: https://invoice.stripe.test/i/1'), mitRechnung.text)
|
||
assert.match(mitRechnung.html, /<a href="https:\/\/invoice\.stripe\.test\/i\/1"[^>]*>Rechnung ansehen/)
|
||
})
|
||
|
||
// ----------------------------------------------------------------------------- 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('Platzhalter-/Blocklisten exakt: T16 und T19', () => {
|
||
const t16 = loadTemplate('T16')
|
||
assert.equal(t16.subject, 'Neue Funktion bestellt: {{funktion}}')
|
||
assert.deepEqual(t16.placeholders, ['kunde_name', 'email', 'projekt', 'funktion', 'woid', 'sitekey', 'sitzung', 'zustand', 'anleitung_url', 'gesundheit_text'])
|
||
assert.deepEqual(t16.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit'])
|
||
const t19 = loadTemplate('T19')
|
||
assert.equal(t19.file, 'T19 Erstattung faellig.html')
|
||
assert.equal(t19.subject, '[Abo] Erstattung fällig: {{funktion}}-Einrichtung')
|
||
assert.deepEqual(t19.placeholders, ['kunde_name', 'email', 'projekt', 'funktion', 'betrag', 'sitzung', 'woid', 'rechnung_url'])
|
||
assert.deepEqual(t19.blocks, ['zahlung_nach_ende', 'ticket_offen', 'rechnung'])
|
||
for (const tpl of [t16, t19]) assert.doesNotMatch(tpl.body, /News|Beitr|Terminplaner/, `${tpl.id}: keine feste Funktion`)
|
||
})
|
||
|
||
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, [])
|
||
})
|