Files
Webklar-Kundenbereich/test/featureConsentTexte.test.js
ClaudeandClaude Opus 5.5 0e74f1669c Webshop A3: Register-Eintrag shop, Haken, Rechtstext-Satz und Ticket-Details
Registereintrag config.features.shop (Bestelltexte mit Fassung config.shop.fassung, Rechtsunterlagen-Getter,
Mailtexte, Ticket), STANDARD_HAKEN.shop (Schalter, Bestellpruefung ueber shop/anfrage.js, Unterlagen, Belehrung),
legalDocs-Satz shop, shopTicketDetails sowie Rumpf von anfrageOffenePunkte. Tests angepasst und um Shop-Faelle erweitert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:35:28 +00:00

347 lines
22 KiB
JavaScript
Raw Permalink 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, beforeEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
// Bestelltexte des Terminplaners (Task F7): die vier Zustimmungen (Haken), der Knopf, die Fassung, die Widerrufsbelehrung im
// Register und der Zustimmungsnachweis beim Checkout (Review Focus 5: genau die gezeigten Texte, Fassungen und der Zeitpunkt
// sind gespeichert, bevor die URL an den Browser geht).
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
featureConsents: schemaKeys('featureConsents'),
termineConfig: schemaKeys('termineConfig'),
termineBookings: schemaKeys('termineBookings'),
termineClaims: schemaKeys('termineClaims'),
termineOutbox: schemaKeys('termineOutbox'),
contractEvents: schemaKeys('contractEvents'),
customerViewLog: schemaKeys('customerViewLog'),
},
datetimes: TERMINE_DATETIMES,
defaultLimit: 25,
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, post, send } = await import('./support/testApp.js')
const { customerCookie, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
const { default: termineRoutes } = await import('../server/routes/termine.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { featureConfig, featureHaken, oeffentlicheAnsicht } = await import('../server/services/featureRegistry.js')
const { nachweisTexte, zustimmungsfassungen } = await import('../server/services/featureConsents.js')
const { widerrufsbelehrungFunktion } = await import('../server/services/termine/rechtstexte.js')
const { legalPaths } = await import('../server/services/legalDocs.js')
const app = await startTestApp({ '/api/funktionen': funktionenRoutes, '/api/termine': termineRoutes })
after(() => Promise.all([app.close(), aw.close(), st.close()]))
const T = config.features.termine
const NEWS = config.features.news
const V = config.termine.avvVersion
const STANDARD = { mode: config.termine.mode, pilotEmails: [...config.termine.pilotEmails] }
const sha256 = (text) => createHash('sha256').update(text).digest('hex')
beforeEach(async () => {
for (const col of ['subscriptions', 'portalFeatures', 'featureConsents', 'termineConfig', 'contractEvents']) {
for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
seedFixtures(aw)
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Salon' })
aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc }))
st.checkoutSessions.clear()
config.termine.mode = 'pilot'
config.termine.pilotEmails = ['*']
delete T.bestellpruefung
})
after(() => {
config.termine.mode = STANDARD.mode
config.termine.pilotEmails = STANDARD.pilotEmails
})
// ------------------------------------------------------------------------------------------------ die Texte
test('termine_haken_woertlich: vier Haken in fester Reihenfolge mit dem Wortlaut aus dem Brief', () => {
assert.deepEqual(T.consent.items.map((i) => i.id), ['unterlagen', 'gesundheit', 'beginn', 'information'])
assert.deepEqual(T.consent.items.map((i) => i.haken), [true, true, true, true])
assert.deepEqual(T.consent.items.map((i) => i.text), [
`Ich stimme der Leistungsbeschreibung Terminplaner und dem Auftragsverarbeitungsvertrag Terminplaner in der Fassung ${V} zu.`,
'Mein Angebot liegt nicht im Gesundheitsbereich (zum Beispiel keine Arzt-, Zahnarzt-, Psychotherapie-, Heilpraktiker- oder Physiotherapiepraxis).',
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Terminplaners beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).',
'Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung (Einrichtung des Terminplaners für diese Website: ein Widget an einer Stelle, gestaltet nach dem Fragebogen, mit Einbau und Datenschutz-Absatz), Gesamtpreis 149 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG), voraussichtliche Dauer meist innerhalb von 1–2 Wochen, der Terminplaner endet mit dem Wartungs-Abo. Die Widerrufsbelehrung mit Muster-Widerrufsformular habe ich gelesen.',
])
assert.equal(T.consent.knopf, 'Zahlungspflichtig bestellen · 149 €')
for (const item of T.consent.items) assert.deepEqual(Object.keys(item).sort(), ['haken', 'id', 'text'], 'ohne url')
// die Bestellseite bekommt dieselben Texte
const ansicht = oeffentlicheAnsicht('termine').consent
assert.deepEqual(ansicht.items, T.consent.items.map((i) => ({ id: i.id, text: i.text, haken: true })))
assert.equal(ansicht.knopf, T.consent.knopf)
// das Zitat für das Erlöschen des Widerrufsrechts bei der Einrichtung (Dienstleistung): § 356 Abs. 5 Nr. 2 BGB, nicht Abs. 4
assert.match(T.consent.items[2].text, /\(§ 356 Abs\. 5 Nr\. 2 BGB\)/)
assert.doesNotMatch(T.consent.items[2].text, /Abs\. 4/)
})
test('termine_version_gleich_dokumente: Fassung des Registers = Fassung aller Dokumente und im Haken 1', () => {
assert.equal(T.consent.version, config.termine.avvVersion)
assert.ok(T.consent.items[0].text.includes(`Fassung ${T.consent.version} zu.`))
const adressen = legalPaths('termine', V)
assert.deepEqual(T.leistungsbeschreibung, { version: V, url: adressen.lb })
assert.deepEqual(T.avv, { version: V, url: adressen.avv })
assert.deepEqual(T.widerruf, { version: V, url: adressen.widerruf })
assert.equal(T.widerruf.url, `/rechtliches/widerrufsbelehrung-funktionen-${V}.html`)
assert.deepEqual(zustimmungsfassungen(T), { zustimmung: V, leistungsbeschreibung: V, avv: V, widerruf: V })
// News behält ihre Leistungsbeschreibung, hat aber keine Widerrufsbelehrung der Funktionen
assert.equal(NEWS.widerruf, null)
assert.equal(NEWS.consent.version, V)
// folgt die Konstante, folgen Fassung und Text (die Haken werden je Fassung gebaut)
config.termine.avvVersion = '2099-01-01'
try {
assert.equal(T.consent.version, '2099-01-01')
assert.ok(T.consent.items[0].text.includes('in der Fassung 2099-01-01 zu.'))
assert.equal(T.widerruf.version, '2099-01-01')
assert.equal(T.widerruf.url, '/rechtliches/widerrufsbelehrung-funktionen-2099-01-01.html')
} finally {
config.termine.avvVersion = V
}
assert.ok(T.consent.items[0].text.includes(`in der Fassung ${V} zu.`))
// Tests dürfen einzelne Texte ändern und zurückstellen: dasselbe Objekt, solange die Fassung gleich bleibt
const text = T.consent.items[1].text
T.consent.items[1].text = 'anders'
assert.equal(T.consent.items[1].text, 'anders')
T.consent.items[1].text = text
})
test('information_nennt_register_werte: Preis, Umsatzsteuer-Hinweis und Dauer kommen aus dem Register', () => {
const info = T.consent.items.find((i) => i.id === 'information').text
assert.ok(info.includes(`Gesamtpreis ${T.priceLabel} einmalig (${config.ustHinweis})`))
assert.ok(info.includes('149 €'))
assert.ok(info.includes('ohne Umsatzsteuer gemäß § 19 UStG'))
assert.ok(info.includes(`voraussichtliche Dauer ${T.setupEstimate}`))
assert.ok(info.includes('der Terminplaner endet mit dem Wartungs-Abo'))
assert.ok(info.endsWith('Die Widerrufsbelehrung mit Muster-Widerrufsformular habe ich gelesen.'))
// Kleinunternehmer: nie "inkl. MwSt." oder "brutto"
for (const text of [...T.consent.items.map((i) => i.text), T.consent.knopf]) assert.doesNotMatch(text, /brutto|inkl\.|inklusive|Mehrwertsteuer|MwSt|zzgl/i)
assert.equal(T.consent.knopf, `Zahlungspflichtig bestellen · ${T.priceLabel}`)
})
test('keine_urls_im_text: kein Link, keine Adresse und kein Markup in den Haken und im Knopf', () => {
for (const text of [...T.consent.items.map((i) => i.text), T.consent.knopf, ...NEWS.consent.items.map((i) => i.text), NEWS.consent.knopf]) {
assert.doesNotMatch(text, /https?:\/\/|www\.|\/rechtliches|\.html|\.pdf|<|>|ENTWURF|Entwurf|Prüfpunkt|⚖/, text.slice(0, 40))
}
})
/**
* Pin auf die Bestelltexte beider Funktionen (Haken und Knopf in Registerreihenfolge, durch eine Leerzeile verbunden).
* Bei Textänderung neues Fassungsdatum (config.termine.avvVersion) UND neuen Hash: eine Textänderung ohne Fassungswechsel
* fällt hier auf, denn gespeicherte Nachweise tragen Text und Fassung.
*/
const TEXTSTAND = { fassung: '2026-10-05', sha256: 'f7c3adf183030d817e2a23f26e8e01dbcd90e35a375b58e2c3b3a33b6bf99b6c' }
test('textstand_pin: Hash über die verbundenen Bestelltexte beider Funktionen steht im Test', () => {
const texte = [...NEWS.consent.items.map((i) => i.text), NEWS.consent.knopf, ...T.consent.items.map((i) => i.text), T.consent.knopf].join('\n\n')
assert.equal(config.termine.avvVersion, TEXTSTAND.fassung, 'neue Fassung: Pin und Hash hier, in featureRegistry.test.js und in den Rechtstext-Tests nachziehen')
assert.equal(sha256(texte), TEXTSTAND.sha256, 'Bestelltexte geändert: neues Fassungsdatum und neuen Hash eintragen')
})
/**
* Pin auf die Bestelltexte des Webshops (Haken und Knopf, durch eine Leerzeile verbunden), gebunden an config.shop.fassung.
* Bei Textänderung neues Fassungsdatum (config.shop.fassung) UND neuen Hash: gespeicherte Nachweise tragen Text und Fassung.
*/
const SHOP_TEXTSTAND = { fassung: '2026-10-05', sha256: '09553de56c794602ca462d980de8548015df7b03d90a5ba1227ce83a58a6608a' }
test('shop_haken_woertlich: fünf Haken in fester Reihenfolge mit dem Wortlaut aus dem Plan', () => {
const S = config.shop.fassung
const SHOP = config.features.shop
assert.deepEqual(SHOP.consent.items.map((i) => i.id), ['unterlagen', 'verantwortung', 'stripe', 'beginn', 'information'])
assert.deepEqual(SHOP.consent.items.map((i) => i.haken), [true, true, true, true, true])
assert.deepEqual(SHOP.consent.items.map((i) => i.text), [
`Ich stimme der Leistungsbeschreibung Webshop und dem Auftragsverarbeitungsvertrag Webshop in der Fassung ${S} zu.`,
'Ich verkaufe als Unternehmer in eigenem Namen. Für meine Produkte, Preise, Produktangaben und Rechtstexte (AGB, Widerrufsbelehrung, Impressum, Datenschutzerklärung) bin ich selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Ich verkaufe keine der in der Leistungsbeschreibung ausgeschlossenen Waren.',
'Die Zahlungen meiner Käufer laufen über mein eigenes Stripe-Konto zu den Bedingungen von Stripe. WEBklar erhält und verwahrt kein Geld meiner Käufer und erhebt keine Umsatzbeteiligung.',
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Webshops beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).',
'Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung (Einrichtung des Webshops für diese Website: Einbau des Shop-Widgets, Verwaltung im Portal, Muster-Rechtstexte), Gesamtpreis 299 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG), voraussichtliche Dauer meist innerhalb von 2–3 Wochen, keine laufende Gebühr von WEBklar; der Webshop läuft, solange WEBklar die Website betreibt. Die Widerrufsbelehrung mit Muster-Widerrufsformular habe ich gelesen.',
])
assert.equal(SHOP.consent.knopf, 'Zahlungspflichtig bestellen · 299 €')
for (const item of SHOP.consent.items) assert.deepEqual(Object.keys(item).sort(), ['haken', 'id', 'text'], 'ohne url')
assert.equal(SHOP.consent.version, S)
})
test('shop_textstand_pin: Hash über die verbundenen Bestelltexte des Webshops steht im Test', () => {
const SHOP = config.features.shop
const texte = [...SHOP.consent.items.map((i) => i.text), SHOP.consent.knopf].join('\n\n')
assert.equal(config.shop.fassung, SHOP_TEXTSTAND.fassung, 'neue Fassung: Pin und Hash hier und in den Rechtstext-Tests nachziehen')
assert.equal(sha256(texte), SHOP_TEXTSTAND.sha256, 'Bestelltexte geändert: neues Fassungsdatum und neuen Hash eintragen')
})
test('shop_belehrung_haken: dieselbe Belehrung wie beim Terminplaner, ohne ENTWURF', async () => {
const belehrung = await featureHaken('shop').belehrung()
assert.equal(belehrung, widerrufsbelehrungFunktion())
assert.equal(belehrung, await featureHaken('termine').belehrung())
assert.deepEqual(config.features.shop.widerruf, { version: config.shop.fassung, url: legalPaths('shop', config.shop.fassung).widerruf })
assert.doesNotMatch(JSON.stringify([config.features.shop.consent.items, belehrung]), /ENTWURF/)
})
// ------------------------------------------------------------------------------------------------ Belehrung im Haken
test('belehrung_haken: der Standard-Haken liefert die Widerrufsbelehrung der Funktionen, News hat keinen', async () => {
const belehrung = await featureHaken('termine').belehrung()
assert.equal(belehrung, widerrufsbelehrungFunktion())
assert.doesNotMatch(belehrung, /ENTWURF|Block F/)
assert.match(belehrung, /Muster-Widerrufsformular/)
assert.match(belehrung, /An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler/)
assert.equal(featureHaken('news').belehrung, undefined)
// der Nachweis führt Belehrung und Knopf hinter den Haken
const texte = nachweisTexte(T, { belehrung })
assert.deepEqual(texte.map((t) => t.id), ['unterlagen', 'gesundheit', 'beginn', 'information', 'belehrung', 'knopf'])
assert.deepEqual(texte.slice(-2), [
{ id: 'belehrung', text: belehrung, haken: false },
{ id: 'knopf', text: T.consent.knopf, haken: false },
])
})
// ------------------------------------------------------------------------------------------------ Checkout und Nachweis
const c1 = customerCookie('c1')
const checkout = (body = {}) =>
post('/api/funktionen/termine/checkout', { projectId: 'p1', zustimmung: { version: T.consent.version, haken: T.consent.items.map((i) => i.id) }, ...body }, { cookie: c1 })
const bereit = () => {
T.bestellpruefung = async () => []
}
const nachweise = () => [...aw.docs('featureConsents').values()]
const sitzungen = () => [...st.checkoutSessions.values()]
const termineDoc = () => [...aw.docs('portalFeatures').values()].find((d) => d.featureKey === 'termine')
test('nachweis_vor_umleitung: Stripe-Sitzung, dann Nachweis mit Texten, Fassungen und Klickzeit, dann ordered, dann erst die URL', async () => {
bereit()
// Reihenfolge der Schreibzugriffe auf featureConsents und portalFeatures und der Antwort an den Browser
const ereignisse = []
const echt = globalThis.fetch
globalThis.fetch = async (url, init) => {
const adresse = String(url?.url ?? url)
const methode = init?.method ?? 'GET'
const antwort = await echt(url, init)
const sammlung = /\/collections\/(featureConsents|portalFeatures)\/documents/.exec(adresse)?.[1]
if (sammlung && methode !== 'GET') ereignisse.push(`${methode} ${sammlung}`)
else if (adresse === `${app.url}/api/funktionen/termine/checkout`) ereignisse.push('antwort')
return antwort
}
const vor = new Date().toISOString()
let res
try {
res = await checkout()
} finally {
globalThis.fetch = echt
}
const nach = new Date().toISOString()
assert.equal(res.status, 200, JSON.stringify(res.body))
assert.deepEqual(Object.keys(res.body), ['url'])
// genau eine Sitzung (zuerst angelegt, ihre ID steht im Nachweis) und genau ein Nachweis
assert.equal(sitzungen().length, 1)
const [session] = sitzungen()
assert.equal(res.body.url, session.url)
assert.equal(nachweise().length, 1)
const [n] = nachweise()
assert.equal(n.stripeSessionId, session.id, 'der Nachweis trägt die ID der Stripe-Sitzung')
// erst der Nachweis, dann jede Änderung am Funktionsdokument (ordered), dann die Antwort mit der URL
const nachweisPosition = ereignisse.indexOf('POST featureConsents')
assert.ok(nachweisPosition >= 0, `Nachweis nicht geschrieben: ${ereignisse}`)
assert.equal(ereignisse.filter((e) => e === 'POST featureConsents').length, 1)
assert.equal(ereignisse.at(-1), 'antwort', 'die Antwort kommt zuletzt')
const dokumentSchreiben = ereignisse.map((e, i) => (e.endsWith('portalFeatures') ? i : -1)).filter((i) => i >= 0)
assert.ok(dokumentSchreiben.length >= 1)
for (const i of dokumentSchreiben) assert.ok(i > nachweisPosition, `portalFeatures wird vor dem Nachweis geschrieben: ${ereignisse}`)
assert.ok(nachweisPosition < ereignisse.indexOf('antwort'))
// gespeichert: genau die Registertexte (Haken), dann Belehrung, dann Knopf; die Fassungen mit widerruf; Hash; Klickzeit
assert.deepEqual(JSON.parse(n.texte), [...T.consent.items.map((i) => ({ id: i.id, text: i.text, haken: true })), { id: 'belehrung', text: widerrufsbelehrungFunktion(), haken: false }, { id: 'knopf', text: T.consent.knopf, haken: false }])
assert.deepEqual(JSON.parse(n.fassungen), { zustimmung: V, leistungsbeschreibung: V, avv: V, widerruf: V })
assert.equal(n.textHash, sha256(n.texte))
assert.equal(n.acceptedAt, session.metadata.consentAt, 'acceptedAt = Klickzeit = consentAt der Stripe-Sitzung')
assert.ok(vor <= n.acceptedAt && n.acceptedAt <= nach, `${vor} <= ${n.acceptedAt} <= ${nach}`)
assert.match(n.acceptedAt, /Z$/)
const doc = termineDoc()
assert.equal(doc.state, 'ordered')
assert.equal(doc.consentAt, n.acceptedAt, 'das Dokument trägt dieselbe Klickzeit (Anker der Widerrufsfrist)')
})
test('nachweis_vor_umleitung: schlägt das Speichern fehl, gibt es 500 ohne url und kein ordered; die Sitzung läuft nach 24 h ab', async () => {
bereit()
const log = mock.method(console, 'error', () => {})
try {
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500, message: 'GEHEIMER-ROHTEXT' })
const res = await checkout()
assert.equal(res.status, 500)
assert.equal(res.body.url, undefined)
assert.doesNotMatch(JSON.stringify(res.body), /GEHEIMER-ROHTEXT|https?:/)
assert.equal(nachweise().length, 0)
const doc = termineDoc()
assert.ok(!doc || doc.state === 'off', 'nicht ordered')
assert.equal(sitzungen().length, 1, 'die Stripe-Sitzung entstand zuerst, ihre URL ging nie an den Browser')
assert.equal(sitzungen()[0].status, 'open')
} finally {
log.mock.restore()
}
// die verwaiste Sitzung läuft nach 24 h ab (Stripe: expires_at beim Anlegen)
const quelle = readFileSync(new URL('../server/services/stripeBilling.js', import.meta.url), 'utf8')
assert.match(quelle, /expires_at: Math\.floor\(Date\.now\(\) \/ 1000\) \+ 24 \* 60 \* 60 - 5 \* 60/)
})
test('belehrung_gezeigt_gleich_gespeichert: gespeicherter Text = bestellung.belehrung der Bestellseite = widerrufsbelehrungFunktion()', async () => {
// gezeigt: GET /api/termine/einstellungen (Bestellseite liest bestellung.belehrung)
const einstellungen = await send('GET', '/api/termine/einstellungen?projectId=p1', { cookie: c1 })
assert.equal(einstellungen.status, 200, JSON.stringify(einstellungen.body))
const gezeigt = einstellungen.body.bestellung.belehrung
assert.equal(typeof gezeigt, 'string')
assert.equal(gezeigt, widerrufsbelehrungFunktion())
assert.deepEqual(einstellungen.body.bestellung.dokumente.map((d) => [d.id, d.version, d.url]), [
['leistungsbeschreibung', V, legalPaths('termine', V).lb],
['avv', V, legalPaths('termine', V).avv],
['widerruf', V, legalPaths('termine', V).widerruf],
])
// gespeichert: der Nachweis des Checkouts
bereit()
assert.equal((await checkout()).status, 200)
const gespeichert = JSON.parse(nachweise()[0].texte).find((t) => t.id === 'belehrung')
assert.equal(gespeichert.text, gezeigt, 'gezeigter und gespeicherter Text sind dieselbe Zeichenkette')
})
test('knopf_gleich_register_und_nachweis: bestellung.consent.knopf = Registertext = gespeicherter knopf-Eintrag', async () => {
const einstellungen = await send('GET', '/api/termine/einstellungen?projectId=p1', { cookie: c1 })
const knopf = einstellungen.body.bestellung.consent.knopf
assert.equal(knopf, T.consent.knopf)
assert.equal(knopf, featureConfig('termine').consent.knopf)
assert.equal(knopf, 'Zahlungspflichtig bestellen · 149 €')
bereit()
assert.equal((await checkout()).status, 200)
const gespeichert = JSON.parse(nachweise()[0].texte).find((t) => t.id === 'knopf')
assert.deepEqual(gespeichert, { id: 'knopf', text: knopf, haken: false })
// die Haken der Bestellseite sind die Haken des Nachweises
assert.deepEqual(einstellungen.body.bestellung.consent.items.map((i) => i.text), JSON.parse(nachweise()[0].texte).filter((t) => t.haken).map((t) => t.text))
})
test('alte_fassung_409: ein Body mit alter Fassung wird mit der neuen Fassung und allen Texten abgewiesen, ohne Spur', async () => {
bereit()
const res = await checkout({ zustimmung: { version: '2026-10-02', haken: T.consent.items.map((i) => i.id) } })
assert.equal(res.status, 409)
assert.equal(res.body.code, 'fassung_veraltet')
assert.equal(res.body.consent.version, V)
assert.deepEqual(res.body.consent.items.map((i) => i.id), ['unterlagen', 'gesundheit', 'beginn', 'information'])
assert.equal(res.body.consent.items[0].text, T.consent.items[0].text)
assert.equal(res.body.consent.knopf, T.consent.knopf)
assert.equal(sitzungen().length, 0)
assert.equal(nachweise().length, 0)
assert.equal(termineDoc(), undefined)
// fehlt ein Haken, ist es 400 (die Fassung ist die aktuelle)
const zuWenig = await checkout({ zustimmung: { version: V, haken: ['unterlagen', 'gesundheit', 'beginn'] } })
assert.equal(zuWenig.status, 400)
assert.equal(zuWenig.body.code, 'zustimmung_fehlt')
})