Terminplaner: Kurzfassung der Einbauanleitung fuer das Einrichtungs-Ticket, T16-Gesundheitsblock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90692b646b
commit
2db3eca357
9 files changed
+1181
-49
No files matched your search
+112
-2
@@ -8,7 +8,7 @@ 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 aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), termineConfig: schemaKeys('termineConfig') } })
|
||||
const st = await startFakeStripe()
|
||||
after(() => Promise.all([aw.close(), st.close()]))
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
@@ -365,7 +365,7 @@ test('Termine-T16: Betreff, Text und Knopf nennen den Terminplaner', async () =>
|
||||
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)
|
||||
// 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 nur bei Treffern der Stichwortprüfung: T16 Gesundheit unten)
|
||||
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}"`)
|
||||
}
|
||||
@@ -428,6 +428,116 @@ test('T16 gesundheit: Block aus ohne Warntext, Block an mit dem Text', () => {
|
||||
assert.ok(an.text.includes('Gesundheitsbereich prüfen: Branche Physiotherapie: Gesundheitsdaten möglich'), an.text)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------- T16 Block gesundheit (Termine, Task D4) ---
|
||||
const { ausVorlage } = await import('../server/services/termine/vorlagen.js')
|
||||
const GESUNDHEIT_ZEILE = 'Gesundheitsbereich prüfen: '
|
||||
|
||||
/** Einstellung eines Salons mit den Namen und Beschreibungen der Leistungen, die der Test braucht. */
|
||||
function salon(leistungen = []) {
|
||||
const e = ausVorlage('salon', { name: 'Salon Haarlinie', email: 'info@haarlinie.example', telefon: '06301 1234', adresse: 'Hauptstraße 1, 67722 Winnweiler', bundesland: 'RP' }, new Date('2026-10-05T10:00:00Z'))
|
||||
leistungen.forEach(([name, beschreibung], i) => Object.assign(e.leistungen[i], { name, beschreibung: beschreibung ?? '' }))
|
||||
return e
|
||||
}
|
||||
|
||||
/** Termine bestellen (mit einer Einstellung im termineConfig-Dokument), bezahlen und die T16-Mail liefern. */
|
||||
async function t16MitEinstellung(einstellung, { fehlerBeimLesen = false } = {}) {
|
||||
if (!aw.docs('subscriptions').has('sub_w_mails')) {
|
||||
aw.seed('subscriptions', { $id: 'sub_w_mails', stripeSubscriptionId: 'sub_w_mails', customerId: 'c1', productKey: 'wartung', status: 'active' })
|
||||
}
|
||||
const { projectId, sessionId } = await bestelle('termine', 'Salon Haarlinie')
|
||||
const doc = await sf.getFeatureDoc('c1', projectId, 'termine')
|
||||
if (einstellung) {
|
||||
aw.seed('termineConfig', { $id: doc.$id, customerId: 'c1', projectId, einstellung: JSON.stringify(einstellung), version: 1, updatedAt: '2026-10-03T08:15:00.000Z', updatedBy: '' })
|
||||
}
|
||||
// der erste Lesezugriff gehoert dem Ticket (Kurzfassung), der zweite dem Hinweis fuer T16
|
||||
if (fehlerBeimLesen) aw.failNext({ collection: 'termineConfig', method: 'GET', status: 500, after: 1 })
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, 'termine'))
|
||||
const t16 = t16Seit(vor)
|
||||
assert.equal(t16.length, 1, `genau eine T16-Mail: ${mails.sent.slice(vor).map((m) => m.subject)}`)
|
||||
const gesundheit = t16[0].text.split('\n').find((z) => z.startsWith(GESUNDHEIT_ZEILE))
|
||||
return { mail: t16[0], doc: await sf.getFeatureDoc('c1', projectId, 'termine'), gesundheit: gesundheit ? gesundheit.slice(GESUNDHEIT_ZEILE.length) : null }
|
||||
}
|
||||
|
||||
test('T16 Termine mit Leistung Physiotherapie: Warnblock "Gesundheitsbereich prüfen" mit dem Fundort, Ticket und Mail gehen raus', async () => {
|
||||
const { mail, doc, gesundheit } = await t16MitEinstellung(salon([['Physiotherapie']]))
|
||||
assert.equal(gesundheit, 'Leistungsname enthält „Physiotherapie“')
|
||||
assert.ok(mail.html.includes('Gesundheitsbereich prüfen: Leistungsname enthält „Physiotherapie“'), mail.html)
|
||||
assert.ok(doc.ticketId && aw.docs('workorders').get(doc.ticketId).details.includes('Gesundheitsbereich prüfen:'), 'das Ticket nennt es ebenfalls')
|
||||
// die uebrigen Bloecke sind unveraendert: Ticket, Knopf zur Anleitung, kein kein_abo
|
||||
assert.ok(mail.text.includes('Im Ticketsystem öffnen:'))
|
||||
assert.ok(!mail.text.includes('Kein laufendes Wartungs-Abo'))
|
||||
})
|
||||
|
||||
test('T16 Termine ohne Treffer oder ohne Einstellungen: kein Gesundheitsblock', async () => {
|
||||
const ohneTreffer = await t16MitEinstellung(salon([['Herrenschnitt', 'Schneiden und Föhnen']]))
|
||||
assert.equal(ohneTreffer.gesundheit, null)
|
||||
assert.ok(!ohneTreffer.mail.text.includes('Gesundheitsbereich'))
|
||||
const ohneConfig = await t16MitEinstellung(null)
|
||||
assert.equal(ohneConfig.gesundheit, null)
|
||||
assert.ok(!ohneConfig.mail.text.includes('Gesundheitsbereich') && !ohneConfig.mail.html.includes('Gesundheitsbereich'))
|
||||
})
|
||||
|
||||
test('T16 Termine: scheitert das Lesen der Einstellung, geht T16 trotzdem ohne Block raus (nie Geld ohne Spur)', async () => {
|
||||
const protokoll = []
|
||||
const alt = console.error
|
||||
console.error = (...args) => protokoll.push(args.join(' '))
|
||||
try {
|
||||
const { mail, doc, gesundheit } = await t16MitEinstellung(salon([['Physiotherapie']]), { fehlerBeimLesen: true })
|
||||
assert.equal(gesundheit, null)
|
||||
assert.ok(mail.text.includes('Neue Funktion bestellt: Terminplaner') && mail.text.includes('Im Ticketsystem öffnen:'))
|
||||
assert.ok(doc.ticketId, 'das Ticket entstand')
|
||||
assert.equal(protokoll.filter((z) => /Gesundheitshinweis/.test(z)).length, 1, protokoll.join('\n'))
|
||||
assert.ok(!protokoll.join('\n').includes('Physio'), 'das Log nennt keine Daten des Betriebs')
|
||||
} finally {
|
||||
console.error = alt
|
||||
}
|
||||
})
|
||||
|
||||
test('T16 Termine: Namen des Kunden werden als Text eingesetzt (HTML maskiert, nichts Halbes), bei vielen Treffern hoechstens 500 Zeichen', async () => {
|
||||
const boese = salon([
|
||||
[`<img src=x onerror=alert(1)> \uD83D${'x'.repeat(5)}`, 'Physiotherapie'],
|
||||
['Zeile1\n## Zeile2', 'Therapie'],
|
||||
])
|
||||
const eins = await t16MitEinstellung(boese)
|
||||
assert.ok(eins.gesundheit.includes('<img src=x onerror=alert(1)>'), eins.gesundheit)
|
||||
assert.ok(!eins.mail.html.includes('<img src=x'), 'im HTML maskiert')
|
||||
assert.ok(eins.mail.html.includes('<img src=x onerror=alert(1)>'))
|
||||
assert.ok(eins.mail.text.isWellFormed() && eins.mail.html.isWellFormed())
|
||||
assert.ok(!eins.gesundheit.includes('\n'))
|
||||
|
||||
// 10 Treffer: gekuerzt auf ganze Meldungen und "und N weitere", hoechstens 500 Zeichen
|
||||
const viele = salon(Array.from({ length: 4 }, (_, i) => [`${String(i)} ${'N'.repeat(36)}`, 'Physiotherapie']))
|
||||
viele.leistungen.push(...Array.from({ length: 7 }, (_, i) => ({ ...viele.leistungen[0], id: `s_v${i}`, name: `${String(i + 4)} ${'M'.repeat(36)}`, beschreibung: 'Physiotherapie' })))
|
||||
const zehn = await t16MitEinstellung(viele)
|
||||
assert.ok(zehn.gesundheit.length <= 500, String(zehn.gesundheit.length))
|
||||
assert.match(zehn.gesundheit, / … und \d+ weitere$/)
|
||||
assert.ok(zehn.gesundheit.startsWith('Beschreibung der Leistung „0 NNN'), zehn.gesundheit)
|
||||
})
|
||||
|
||||
test('T16 Termine, Zahlung fehlgeschlagen: kein Gesundheitsblock (es wurde nichts bestellt), auch mit Treffer in den Einstellungen', async () => {
|
||||
const { projectId, sessionId } = await bestelle('termine', 'Salon Haarlinie')
|
||||
const doc = await sf.getFeatureDoc('c1', projectId, 'termine')
|
||||
aw.seed('termineConfig', { $id: doc.$id, customerId: 'c1', projectId, einstellung: JSON.stringify(salon([['Physiotherapie']])), version: 1, updatedAt: '2026-10-03T08:15:00.000Z', updatedBy: '' })
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(failedEvent(sessionId, projectId, 'termine'))
|
||||
const t16 = t16Seit(vor)
|
||||
assert.equal(t16.length, 1)
|
||||
assert.match(t16[0].text, /Zahlung fehlgeschlagen/)
|
||||
assert.ok(!t16[0].text.includes('Gesundheitsbereich') && !t16[0].html.includes('Gesundheitsbereich'))
|
||||
})
|
||||
|
||||
test('T16 News: nie ein Gesundheitsblock, auch wenn unter der ID der News-Funktion ein Einstellungsdokument mit Treffer laege', async () => {
|
||||
const { projectId, sessionId } = await bestelle('news', 'Bäckerei Physiotherapie')
|
||||
const doc = await sf.getFeatureDoc('c1', projectId, 'news')
|
||||
aw.seed('termineConfig', { $id: doc.$id, customerId: 'c1', projectId, einstellung: JSON.stringify(salon([['Physiotherapie']])), version: 1, updatedAt: '2026-10-03T08:15:00.000Z', updatedBy: '' })
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, 'news'))
|
||||
const t16 = t16Seit(vor)
|
||||
assert.equal(t16.length, 1)
|
||||
assert.ok(!t16[0].text.includes('Gesundheitsbereich') && !t16[0].html.includes('Gesundheitsbereich'))
|
||||
})
|
||||
|
||||
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'))
|
||||
|
||||
Reference in new issue
Block a user