Rechtstexte und K15: Prüfbefunde F5–F8 (AVV genauer, PDF-Prüfsumme, Unterlagen der vereinbarten Fassung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c059cb3d6
commit
90098726b9
13 files changed
+345
-63
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
|
||||
import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, copyFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
@@ -376,11 +376,153 @@ test('k15_anhang_nicht_lesbar: nicht lesbare PDFs => K15 ohne Anhang, T16 mit Bl
|
||||
} finally {
|
||||
log2.mock.restore()
|
||||
}
|
||||
// Kunde ohne E-Mail-Adresse: keine K15, aber T16 sagt es dem Team
|
||||
// Kunde ohne E-Mail-Adresse: keine K15, T16 sagt es dem Team, und das Log nennt den Grund (T16: "Der Grund steht im Log des Portals")
|
||||
aw.seed('customers', { $id: 'c9', name: 'Ohne Adresse', email: '' })
|
||||
const ohneAdresse = await bezahleTermine({ customerId: 'c9' })
|
||||
assert.equal(ohneAdresse.k15Anzahl, 0)
|
||||
assert.ok(ohneAdresse.t16.text.includes('Unterlagen von Hand nachsenden'))
|
||||
const log3 = stumm()
|
||||
try {
|
||||
const ohneAdresse = await bezahleTermine({ customerId: 'c9' })
|
||||
assert.equal(ohneAdresse.k15Anzahl, 0)
|
||||
assert.ok(ohneAdresse.t16.text.includes('Unterlagen von Hand nachsenden'))
|
||||
const zeilen = log3.mock.calls.map((c) => c.arguments.join(' '))
|
||||
assert.ok(zeilen.some((z) => z.includes(`K15 (Funktion ${ohneAdresse.doc.$id}) nicht gesendet: beim Kunden ist keine E-Mail-Adresse hinterlegt`)), zeilen.join('\n'))
|
||||
assert.doesNotMatch(zeilen.join('\n'), /Ohne Adresse/, 'keine Personendaten im Log')
|
||||
} finally {
|
||||
log3.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
/** Verzeichnis mit Kopien der ausgelieferten Dateien unter dem Namen einer anderen Fassung (HTML und PDF je Dokument). */
|
||||
function dateienDerFassung(fassung) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'k15-fassung-'))
|
||||
mkdirSync(join(dir, 'rechtliches'))
|
||||
const echt = legalPdfFiles('termine', V)
|
||||
for (const f of Object.values(echt)) {
|
||||
const slug = f.path.split('/').at(-1).replace(`-${V}.pdf`, '')
|
||||
copyFileSync(f.path, join(dir, 'rechtliches', `${slug}-${fassung}.pdf`))
|
||||
copyFileSync(f.path.replace(/\.pdf$/, '.html'), join(dir, 'rechtliches', `${slug}-${fassung}.html`))
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
test('k15_vereinbarte_fassung: wurde die Fassung zwischen Klick und Zahlung angehoben, gehen PDFs und Links der vereinbarten Fassung raus, wenn es sie gibt', async () => {
|
||||
const alt = '2026-10-01'
|
||||
const dir = dateienDerFassung(alt)
|
||||
const log = stumm()
|
||||
try {
|
||||
setLegalDirForTests(dir)
|
||||
const vereinbart = Object.fromEntries(Object.keys(consents.zustimmungsfassungen(T)).map((k) => [k, alt]))
|
||||
const { k15, t16, doc } = await bezahleTermine({ fassungen: vereinbart })
|
||||
assert.deepEqual(k15.attachments.map((a) => a.filename), [`Leistungsbeschreibung-Terminplaner-${alt}.pdf`, `AVV-Terminplaner-${alt}.pdf`, `Widerrufsbelehrung-Muster-Widerrufsformular-${alt}.pdf`])
|
||||
for (const a of k15.attachments) assert.equal(Buffer.from(a.content, 'base64').subarray(0, 5).toString('latin1'), '%PDF-')
|
||||
for (const slug of ['leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'widerrufsbelehrung-funktionen']) {
|
||||
assert.ok(k15.text.includes(`${BASE}/rechtliches/${slug}-${alt}.html`), `${slug}: Link der vereinbarten Fassung`)
|
||||
assert.ok(!k15.text.includes(`${slug}-${V}.html`), `${slug}: kein Link der aktuellen Fassung`)
|
||||
}
|
||||
assert.ok(k15.text.includes(`Fassung ${alt}: Leistungsbeschreibung`))
|
||||
assert.match(k15.text, /Alle drei hängen als PDF an dieser E-Mail\./)
|
||||
assert.doesNotMatch(t16.text, /Unterlagen von Hand nachsenden/)
|
||||
assert.equal(doc.state, 'setup')
|
||||
assert.ok(log.mock.calls.some((c) => String(c.arguments[0]).includes(`zugestimmt, aktuell ist ${V}`)), 'das Log nennt den Fassungswechsel')
|
||||
} finally {
|
||||
setLegalDirForTests(null)
|
||||
log.mock.restore()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
// gibt es die Dateien der vereinbarten Fassung nicht, geht K15 ohne PDFs und ohne Links raus (Dateien der aktuellen Fassung bleiben ungenutzt)
|
||||
const log2 = stumm()
|
||||
try {
|
||||
const fehlend = await bezahleTermine({ fassungen: { ...consents.zustimmungsfassungen(T), zustimmung: '2026-09-30' } })
|
||||
assert.ok(!fehlend.k15.attachments || fehlend.k15.attachments.length === 0)
|
||||
assert.doesNotMatch(fehlend.k15.text, /Deine Vertragsunterlagen|\/rechtliches\//)
|
||||
assert.match(fehlend.k15.text, /Deine Zustimmungen/)
|
||||
assert.ok(fehlend.t16.text.includes('Unterlagen von Hand nachsenden'))
|
||||
} finally {
|
||||
log2.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('k15_nachweis_nicht_lesbar: ohne lesbaren Nachweis ist die vereinbarte Fassung unbekannt: keine PDFs, keine Links, T16 anhang_fehlt, Log', async () => {
|
||||
// (a) Lesefehler beim Nachweis
|
||||
zaehler += 1
|
||||
const projectId = `ptermine${zaehler}`
|
||||
const sessionId = `cs_ptermine${zaehler}`
|
||||
aw.seed('websiteProjects', { $id: projectId, customerId: 'c1', projectName: `Salon ${zaehler}` })
|
||||
await sf.markOrdered({ customerId: 'c1', projectId, key: 'termine', sessionId, consent: {} })
|
||||
await consents.speichereZustimmung({
|
||||
customerId: 'c1', projectId, featureKey: 'termine', stripeSessionId: sessionId, fassungen: consents.zustimmungsfassungen(T),
|
||||
texte: consents.nachweisTexte(T, { belehrung: widerrufsbelehrungFunktion() }), at: KLICK,
|
||||
})
|
||||
const log = stumm()
|
||||
try {
|
||||
aw.failNext({ collection: 'featureConsents', method: 'GET', status: 500, message: 'GEHEIMER-ROHTEXT' })
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, 'termine'))
|
||||
const neu = mails.sent.slice(vor)
|
||||
const k15 = neu.find((m) => m.subject.startsWith('Deine Bestellung'))
|
||||
const t16 = neu.find((m) => m.subject.startsWith('Neue Funktion bestellt'))
|
||||
assert.ok(k15, 'die Bestätigung geht trotzdem raus')
|
||||
assert.ok(!k15.attachments || k15.attachments.length === 0, 'nie die aktuelle Fassung ungeprüft anhängen')
|
||||
assert.doesNotMatch(k15.text, /Deine Vertragsunterlagen|Deine Zustimmungen|\/rechtliches\//)
|
||||
assert.ok(t16.text.includes('Unterlagen von Hand nachsenden'))
|
||||
assert.equal((await sf.getFeatureDoc('c1', projectId, 'termine')).state, 'setup')
|
||||
assert.ok(log.mock.calls.some((c) => String(c.arguments[0]).includes('die vereinbarte Fassung ist unbekannt')), 'das Log nennt den Grund')
|
||||
} finally {
|
||||
log.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('k15_nachweis_ohne_fassung: ein Nachweis ohne lesbare Fassung zählt wie ein unlesbarer', async () => {
|
||||
zaehler += 1
|
||||
const projectId = `ptermine${zaehler}`
|
||||
const sessionId = `cs_ptermine${zaehler}`
|
||||
aw.seed('websiteProjects', { $id: projectId, customerId: 'c1', projectName: `Salon ${zaehler}` })
|
||||
await sf.markOrdered({ customerId: 'c1', projectId, key: 'termine', sessionId, consent: {} })
|
||||
aw.seed('featureConsents', {
|
||||
$id: `fc_kaputt_${zaehler}`, customerId: 'c1', projectId, featureKey: 'termine', stripeSessionId: sessionId, acceptedAt: KLICK,
|
||||
fassungen: 'kein json', texte: JSON.stringify(consents.nachweisTexte(T, { belehrung: widerrufsbelehrungFunktion() })), textHash: 'x',
|
||||
})
|
||||
const log = stumm()
|
||||
try {
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, 'termine'))
|
||||
const neu = mails.sent.slice(vor)
|
||||
const k15 = neu.find((m) => m.subject.startsWith('Deine Bestellung'))
|
||||
assert.ok(!k15.attachments || k15.attachments.length === 0)
|
||||
assert.doesNotMatch(k15.text, /Deine Vertragsunterlagen|\/rechtliches\//)
|
||||
assert.match(k15.text, /Deine Zustimmungen/, 'der Wortlaut der Zustimmungen steht trotzdem in der Mail')
|
||||
assert.ok(neu.find((m) => m.subject.startsWith('Neue Funktion bestellt')).text.includes('Unterlagen von Hand nachsenden'))
|
||||
assert.ok(log.mock.calls.some((c) => String(c.arguments[0]).includes('Fassung im Zustimmungsnachweis nicht bestimmbar')))
|
||||
} finally {
|
||||
log.mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('k15_frisches_array: der Mailer bekommt für attachments immer ein eigenes, nicht eingefrorenes Array', async () => {
|
||||
const roh = []
|
||||
setMailTransportForTests({
|
||||
async sendMail(mail) {
|
||||
roh.push(mail)
|
||||
return mails.sendMail(mail)
|
||||
},
|
||||
})
|
||||
try {
|
||||
aw.seed('websiteProjects', { $id: 'pnews12', customerId: 'c1', projectName: 'Frisch' })
|
||||
await sf.markOrdered({ customerId: 'c1', projectId: 'pnews12', key: 'news', sessionId: 'cs_news12', consent: {} })
|
||||
await sf.handleFeatureCheckoutEvent(paidEvent('cs_news12', 'pnews12'))
|
||||
await bezahleTermine()
|
||||
} finally {
|
||||
setMailTransportForTests(mails)
|
||||
}
|
||||
const k15 = roh.filter((m) => String(m.subject).startsWith('Deine Bestellung'))
|
||||
assert.equal(k15.length, 2)
|
||||
for (const mail of k15) {
|
||||
assert.ok(Array.isArray(mail.attachments))
|
||||
assert.equal(Object.isFrozen(mail.attachments), false)
|
||||
}
|
||||
assert.notEqual(k15[0].attachments, k15[1].attachments)
|
||||
k15[0].attachments.push('x') // ein Mailer darf das Array verändern, ohne das gemeinsame Muster zu treffen
|
||||
const nochmal = await bezahleTermine()
|
||||
assert.equal(nochmal.k15.attachments.length, 3)
|
||||
})
|
||||
|
||||
test('checkout_503_ohne_unterlagen: fehlen die PDFs der aktuellen Fassung, 503 unterlagen_fehlen vor jeder Stripe-Sitzung, ohne ordered-Dokument', async () => {
|
||||
|
||||
Reference in new issue
Block a user