Rechtsrunde Fassung 6 freigegeben (28.10.2026): Datum, Entwurfsmarken, R8b

Der Inhaber hat am 28.10.2026 alle Vorschläge R1 bis R18 angenommen (R10 = Option A)
und zusätzlich R8b entschieden: Webshop und Buchungskalender kehren nach einem neuen
Wartungs-Abo in der Frist automatisch zurück, wie der Server es schon tut.

- Fassungen: Abo 2026-10-28-v6, Funktionsunterlagen 2026-10-28 (Seiten neu gebaut)
- Entwurfsmarken und meta robots noindex entfernt, ENTWURF_V6 = false
- WORTLAUT_STATUS = 'freigegeben'
- Nr. 10 Abs. 5 Webshop/Buchungskalender ohne "auf Wunsch in Textform" (R8b)
- Entscheidungsliste: Abschnitt "Freigabe"; Prüfpunkte D1 bis D8 auf die aktuelle
  Datenschutzerklärung (Fassung 22.10.2026, eine Zeile tiefer) umgestellt
- Tests auf das neue Datum und den Status freigegeben gezogen

Live ändert sich für Kunden nichts: die Seiten liegen unter /neu/ und gelten nur in der
neuen Ansicht (Pilot). Umschalten (P10) folgt erst auf ausdrückliches "umschalten".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-28 14:46:53 +00:00
1 parent 2873638b1c
commit 64f1a3b26b
36 files changed
+129 -131

No files matched your search

+2 -2
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const { config } = await import('../server/config.js')
const ENTWURF = true
const ENTWURF = false
const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8')
const PFLICHT = [ // stelle = Überschrift im Entwurf; von/bis = Anker-Ids der Seite (null = Dokumentanfang bzw. -ende)
@@ -29,7 +29,7 @@ const PFLICHT = [ // stelle = Überschrift im Entwurf; von/bis = Anker-Ids der
]
test('Entwurf Fassung 6: Stellen in Reihenfolge, jeder Pflichtsatz im Wortlaut unter seiner Stelle', () => {
const md = lies('docs/rechtliches/abo-bedingungen-v6.md')
assert.match(md, /^# Abo-Bedingungen Fassung 6 \(Entwurf\)\n/)
assert.match(md, /^# Abo-Bedingungen Fassung 6\n/)
assert.deepEqual([...md.matchAll(/^## (.+)$/gm)].map((m) => m[1]), [...PFLICHT.map((p) => p.stelle), 'Was gleich bleibt'])
assert.equal(md.includes('Entwurf zur Freigabe, keine Rechtsberatung'), ENTWURF)
for (const p of PFLICHT) {
+1 -1
View File
@@ -50,7 +50,7 @@ test('consentAbo: Linkzeile mit Fassung, Hinweis (termine, shop, unterkunft), be
if (key in HINWEIS) assert.equal(c.items[1].text, HINWEIS[key], key)
}
assert.equal(config.aboFunktionen.texte.beginn, 'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.')
assert.equal(V, '2026-10-16')
assert.equal(V, '2026-10-28')
assert.deepEqual(HINWEIS, { ...WORTLAUT_V6.hinweis }, 'die Hinweise sind die Sätze aus WORTLAUT_V6')
assert.equal(V, FUNKTIONEN_FASSUNG_V6)
assert.equal(reg.consentAbo('termine').items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, FUNKTIONEN_FASSUNG_V6))
+8 -8
View File
@@ -5,11 +5,11 @@ const { config } = await import('../server/config.js')
const { WORTLAUT_V6, FUNKTIONEN_FASSUNG_V6, FUNKTIONEN_UNTERLAGEN_V6, mitFassung } = await import('../server/services/products.js')
test('Linkzeilen: Wortlaut, Platzhalter wird gefüllt', () => {
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.bestellen, '2026-10-16-v6'), 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung 2026-10-16-v6). Alles kommt danach auch als PDF per Mail.')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, '2026-10-16'), 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16).')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, '2026-10-16'), 'Es gilt die Leistungsbeschreibung (Fassung 2026-10-16).')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.bestellen, '2026-10-28-v6'), 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung 2026-10-28-v6). Alles kommt danach auch als PDF per Mail.')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, '2026-10-28'), 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-28).')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, '2026-10-28'), 'Es gilt die Leistungsbeschreibung (Fassung 2026-10-28).')
assert.throws(() => mitFassung(WORTLAUT_V6.linkzeile.bestellen, ''), /Fassung/)
assert.throws(() => mitFassung('ohne Platzhalter', '2026-10-16'), /Platzhalter/)
assert.throws(() => mitFassung('ohne Platzhalter', '2026-10-28'), /Platzhalter/)
})
test('Häkchen beginn: Wortlaut je Fall (nur privat in der Frist)', () => {
assert.deepEqual(WORTLAUT_V6.beginn, {
@@ -33,13 +33,13 @@ test('alle Wortlaute: du-Form, kein Preis, nur {fassung} als Platzhalter, tief e
for (const o of [WORTLAUT_V6, WORTLAUT_V6.linkzeile, WORTLAUT_V6.beginn, WORTLAUT_V6.hinweis]) assert.ok(Object.isFrozen(o))
})
test('Funktionsunterlagen Fassung 6: Reihenfolge, Vorschaupfad, AVV nur wo Daten im Auftrag verarbeitet werden', () => {
assert.equal(FUNKTIONEN_FASSUNG_V6, '2026-10-16')
assert.equal(FUNKTIONEN_FASSUNG_V6, '2026-10-28')
assert.deepEqual(Object.keys(FUNKTIONEN_UNTERLAGEN_V6), ['news', 'termine', 'shop', 'unterkunft', 'zeiten', 'analyse'])
assert.deepEqual(FUNKTIONEN_UNTERLAGEN_V6.termine.lb, { version: '2026-10-16', url: '/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html', pdfFile: 'neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.pdf', anhangName: 'Leistungsbeschreibung-Terminplaner-2026-10-16.pdf' })
const slugs = Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.url.replace(/^\/neu\/rechtliches\/|-2026-10-16\.html$/g, ''))
assert.deepEqual(FUNKTIONEN_UNTERLAGEN_V6.termine.lb, { version: '2026-10-28', url: '/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-28.html', pdfFile: 'neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-28.pdf', anhangName: 'Leistungsbeschreibung-Terminplaner-2026-10-28.pdf' })
const slugs = Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.url.replace(/^\/neu\/rechtliches\/|-2026-10-28\.html$/g, ''))
assert.deepEqual(slugs, ['leistungsbeschreibung-news', 'leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'leistungsbeschreibung-webshop', 'avv-webshop', 'leistungsbeschreibung-unterkunft', 'avv-unterkunft', 'leistungsbeschreibung-zeiten', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse'])
assert.deepEqual([FUNKTIONEN_UNTERLAGEN_V6.news.avv, FUNKTIONEN_UNTERLAGEN_V6.zeiten.avv], [null, null])
assert.deepEqual(Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.anhangName.replace('-2026-10-16.pdf', '')), ['Leistungsbeschreibung-News-Pinnwand', 'Leistungsbeschreibung-Terminplaner', 'AVV-Terminplaner', 'Leistungsbeschreibung-Webshop', 'AVV-Webshop', 'Leistungsbeschreibung-Buchungskalender', 'AVV-Buchungskalender', 'Leistungsbeschreibung-Oeffnungszeiten-live', 'Leistungsbeschreibung-Website-Analyse', 'AVV-Website-Analyse'])
assert.deepEqual(Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.anhangName.replace('-2026-10-28.pdf', '')), ['Leistungsbeschreibung-News-Pinnwand', 'Leistungsbeschreibung-Terminplaner', 'AVV-Terminplaner', 'Leistungsbeschreibung-Webshop', 'AVV-Webshop', 'Leistungsbeschreibung-Buchungskalender', 'AVV-Buchungskalender', 'Leistungsbeschreibung-Oeffnungszeiten-live', 'Leistungsbeschreibung-Website-Analyse', 'AVV-Website-Analyse'])
})
test('Verdrahtung mit P5 (läuft erst nach dem Merge von kn/server; rot, bis der Controller verdrahtet hat)', { skip: config.aboFunktionen === undefined && 'config.aboFunktionen fehlt: P5 noch nicht gemerged' }, async () => {
const reg = await import('../server/services/featureRegistry.js')
+8 -8
View File
@@ -13,7 +13,7 @@ const { render, funktionen6Dokumente } = await import('../scripts/build-legal-ht
const ROOT = fileURLToPath(new URL('..', import.meta.url))
const lies = (rel) => readFileSync(join(ROOT, rel), 'utf8')
const ENTWURF = true
const ENTWURF = false
const B = { // Bausteine; {N} = Nominativ, {G} = Genitiv der Partei (Kunde/Kunden, Händler/Händlers, Vermieter/Vermieters)
geltung: 'Diese Leistungsbeschreibung gilt, wenn der {N} die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der {N} die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat.',
@@ -49,7 +49,7 @@ const abschnitte = (md) => md.split(/^## /m).slice(1).map((s) => ({ kopf: s.spli
test('Leistungsbeschreibungen v6: Fassungszeile, Bausteine, Pflichtsatz, Frist, keine Preise', () => {
for (const key of LISTE) {
const e = LB[key], md = lies(`docs/rechtliches/${e.slug}-v6.md`)
assert.match(md.split('\n')[2], new RegExp(`^\\*\\*Fassung 2026-10-16\\*\\*${ENTWURF ? ' · Entwurf – Prüfung durch Kanzlei offen' : ''} · Zusatzbedingungen zum Wartungs-Abo$`), key)
assert.match(md.split('\n')[2], new RegExp(`^\\*\\*Fassung 2026-10-28\\*\\*${ENTWURF ? ' · Entwurf – Prüfung durch Kanzlei offen' : ''} · Zusatzbedingungen zum Wartungs-Abo$`), key)
assert.doesNotMatch(md, VERBOTEN, key)
for (const t of ['geltung', 'voraussetzung', 'einrichtung', 'projekt', 'mehraufwand', key === 'zeiten' ? 'endeZeiten' : 'ende']) assert.ok(md.includes(bau(B[t], e.partei)), `${key}: Baustein ${t}`)
if (key === 'zeiten') assert.doesNotMatch(md, /Karenz|Behalten|spätestens am folgenden Tag/, 'Zeiten: 30 Tage Weiterlauf wie das bestehende Modul, kein Behalten, nicht die Abschaltung am Folgetag')
@@ -67,13 +67,13 @@ test('Terminplaner v6: Überschriften wie Fassung 5 (Nr. 5 heißt „Einrichtung
})
test('Seiten der Gruppe sind genau render(Markdown)', () => {
for (const key of LISTE) {
const d = funktionen6Dokumente().find((x) => x.html.includes(`/${LB[key].slug}-2026-10-16.html`))
const d = funktionen6Dokumente().find((x) => x.html.includes(`/${LB[key].slug}-2026-10-28.html`))
assert.equal(lies(d.html), render(lies(d.md), d.meta), `${d.html}: node scripts/build-legal-html.mjs funktionen6 --nur=${d.doc}`)
}
})
test('Entwurfsseiten der Gruppe: Entwurfsmarke in der Quelle und meta robots noindex in der Seite (N45)', () => {
for (const key of LISTE) {
const d = funktionen6Dokumente().find((x) => x.html.includes(`/${LB[key].slug}-2026-10-16.html`))
const d = funktionen6Dokumente().find((x) => x.html.includes(`/${LB[key].slug}-2026-10-28.html`))
assert.equal(lies(d.md).includes('Entwurf – Prüfung durch Kanzlei offen'), ENTWURF, d.md)
assert.equal(lies(d.html).includes('<meta name="robots" content="noindex">'), ENTWURF, d.html)
}
@@ -98,7 +98,7 @@ const BA = {
}
test('Website-Analyse v6 (Leistungsbeschreibung): in beiden Abos, Einrichtung kostenlos, Ende mit dem Abo, Zielgruppenregel bleibt', () => {
const md = lies('docs/rechtliches/leistungsbeschreibung-website-analyse-v6.md')
assert.match(md.split('\n')[2], /^\*\*Fassung 2026-10-16\*\* · (Entwurf – Prüfung durch Kanzlei offen · )?Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo$/)
assert.match(md.split('\n')[2], /^\*\*Fassung 2026-10-28\*\* · (Entwurf – Prüfung durch Kanzlei offen · )?Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo$/)
assert.doesNotMatch(md, /€|per Haken|[a-zäöüß,;:] Sie\b|Im Hosting-Abo ist die Website-Analyse nicht enthalten|Sie ist im Wartungs-Abo ohne Aufpreis|im Kundenportal bestellt/)
assert.ok(md.includes(B.geltung.replaceAll('{N}', 'Kunde')))
for (const t of Object.values(BA)) assert.ok(md.includes(t), t.slice(0, 40))
@@ -114,16 +114,16 @@ const AVV = { // [alt, neu] je Datei; der Rest ab "## 1." ist sonst wortgleich
['(Ausschalten oder Ende des Wartungs-Abos)', '(Ausschalten oder Ende des Abos, das sie enthält)'],
['Das Wartungs-Abo bleibt davon unberührt.', 'Das Abo bleibt davon unberührt.']],
}
const KOPF_AVV = ENTWURF ? '**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen' : '**Fassung 2026-10-16**'
const KOPF_AVV = ENTWURF ? '**Fassung 2026-10-28** · Entwurf – Prüfung durch Kanzlei offen' : '**Fassung 2026-10-28**'
const AVV_FUNKTION = { termine: 'Terminplaner', shop: 'Webshop', unterkunft: 'Buchungskalender', analyse: 'Website-Analyse' }
const avvVorbemerkung = (funktion, fassung) => `Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „${funktion}“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung ${fassung})“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „${funktion}“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.`
function pruefeAvv(key, slug) {
const v5 = lies(`docs/rechtliches/${slug}.md`), v6 = lies(`docs/rechtliches/${slug}-v6.md`)
assert.equal(v6.split('\n')[2], KOPF_AVV)
assert.ok(v6.includes(`(1) ${avvVorbemerkung(AVV_FUNKTION[key], '2026-10-16')}`))
assert.ok(v6.includes(`(1) ${avvVorbemerkung(AVV_FUNKTION[key], '2026-10-28')}`))
assert.doesNotMatch(v6, /per Haken|Mit der Bestellung weist|Vertrag über den Betrieb der Website|keine Bestellung geworden/)
let erwartet = v5.replace(/^\*\*Fassung \d{4}-\d{2}-\d{2}\*\*.*$/m, KOPF_AVV)
erwartet = erwartet.replace(/^\(1\) Der Kunde schließt diesen Vertrag[^\n]*$/m, `(1) ${avvVorbemerkung(AVV_FUNKTION[key], '2026-10-16')}`)
erwartet = erwartet.replace(/^\(1\) Der Kunde schließt diesen Vertrag[^\n]*$/m, `(1) ${avvVorbemerkung(AVV_FUNKTION[key], '2026-10-28')}`)
for (const [alt, neu] of AVV[key]) { assert.ok(erwartet.includes(alt), `Bestand enthält: ${alt.slice(0, 40)}`); erwartet = erwartet.replaceAll(alt, neu) }
assert.equal(v6, erwartet, `${slug}-v6.md = Bestandsfassung mit genau diesen Ersetzungen`)
}
+4 -4
View File
@@ -19,7 +19,7 @@ const BESTELLEN = mitFassung(WORTLAUT_V6.linkzeile.bestellen, FASSUNG) // der Sa
const T = bestelltexteFuerClient({ fassung: FASSUNG, termsUrl: URL_V6 })
test('Status, Knopf und feste Fehlertexte', () => {
assert.deepEqual([WORTLAUT_STATUS, KNOPF], ['entwurf', 'Zahlungspflichtig bestellen'])
assert.deepEqual([WORTLAUT_STATUS, KNOPF], ['freigegeben', 'Zahlungspflichtig bestellen'])
assert.equal(WECHSEL_GESPERRT, 'Der Wechsel gilt sofort. Ohne dein Häkchen können wir ihn nicht ausführen.')
assert.equal(FEHLER_NACHWEIS, 'Das hat gerade nicht geklappt. Bitte versuche es gleich noch einmal.')
assert.equal(FEHLER_BEDINGUNGEN, 'Die Bedingungen wurden aktualisiert. Bitte lade die Seite neu.')
@@ -32,7 +32,7 @@ test('klartext und Fassungsangabe', () => {
})
test('Nutzlast: Linkzeile und Häkchen sind der Wortlaut aus P8 (WORTLAUT_V6), dazu Sperrzeile, Warnzeile, Satz, Kundenart-Zeile (Entwurf)', () => {
assert.deepEqual([T.status, T.fassung, T.knopf], ['entwurf', FASSUNG, 'Zahlungspflichtig bestellen'])
assert.deepEqual([T.status, T.fassung, T.knopf], ['freigegeben', FASSUNG, 'Zahlungspflichtig bestellen'])
assert.equal(klartext(T.abo.linkzeile.text), `${BESTELLEN} Datenschutz`)
assert.deepEqual(T.abo.linkzeile.text.match(/\[[^\]]+\]/g), ['[Vertragsbedingungen]', '[Datenschutz]'])
assert.deepEqual(T.abo.linkzeile.urls, [URL_V6, '/datenschutz.html'])
@@ -98,9 +98,9 @@ test('nachweisTexte: Reihenfolge, Häkchen nur wenn gezeigt und gesetzt, Klartex
assert.equal(nachweisTexte('abo', { kundenart: 'privat', fassung }).some((t) => t.id === 'bestellung'), false, 'ohne Zeile keine Zeile')
})
test('Quelle: keine Imports, du-Form, als Entwurf gekennzeichnet', () => {
test('Quelle: keine Imports, du-Form, als freigegeben gekennzeichnet', () => {
const q = readFileSync(new URL('../server/services/bestelltexte.js', import.meta.url), 'utf8')
assert.doesNotMatch(q, /^import /m)
assert.doesNotMatch(q, /\b(Sie|Ihr|Ihre|Ihnen)\b/)
assert.match(q, /ENTWURF/)
assert.match(q, /Freigegeben vom Inhaber am 28\.10\.2026/)
})
+2 -2
View File
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs'
// Entscheidungsliste, Kanzlei-Fragen, Ablauf nach der Freigabe und Datenschutz-Prüfpunkte der Rechtsrunde Fassung 6 (Task 11).
const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8')
const ENTWURF = true
const ENTWURF = false
const md = lies('docs/rechtliches/kundenansicht-entscheidungen.md'), dsg = lies('docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md')
test('Entscheidungsliste: R1 bis R18, je Punkt Vorschlag, Alternative und Kanzlei-Frage', () => {
const z = md.split('\n').filter((l) => /^\| R\d+ \|/.test(l)).map((l) => l.split('|').slice(1, -1).map((s) => s.trim()))
@@ -39,5 +39,5 @@ test('Datenschutz-Prüfpunkte D1 bis D8 mit gültiger Fundstelle in public/daten
const zeilen = lies('public/datenschutz.html').split('\n')
const refs = [...dsg.matchAll(/public\/datenschutz\.html:(\d+)/g)].map((m) => Number(m[1]))
assert.ok(refs.length >= 8 && refs.every((n) => n >= 1 && n <= zeilen.length))
assert.ok(zeilen[126].includes('Zahlungspflichtig bestellen') && zeilen[125].includes('Leistungsbeginn') && zeilen[101].includes('Wartungs-Abo'), 'D1, D2, D5 zitieren die richtigen Zeilen')
assert.ok(zeilen[127].includes('Zahlungspflichtig bestellen') && zeilen[126].includes('Leistungsbeginn') && zeilen[102].includes('Wartungs-Abo'), 'D1, D2, D5 zitieren die richtigen Zeilen')
})
+2 -2
View File
@@ -14,7 +14,7 @@ useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1'
const { render, lauf, funktionen6Dokumente } = await import('../scripts/build-legal-html.mjs')
const ROOT = fileURLToPath(new URL('..', import.meta.url))
const F = '2026-10-16'
const F = '2026-10-28'
const SLUGS = ['leistungsbeschreibung-news', 'leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'leistungsbeschreibung-webshop', 'avv-webshop', 'leistungsbeschreibung-unterkunft', 'avv-unterkunft', 'leistungsbeschreibung-zeiten', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse']
test('funktionen6Dokumente: zehn Seiten unter public/neu/rechtliches, Quellen *-v6.md, Navigation im Vorschaupfad', () => {
const d = funktionen6Dokumente()
@@ -68,7 +68,7 @@ test('build-legal-pdfs.sh: Modi abo6 und funktionen6, all und erste Usage-Zeile
assert.equal(spawnSync('bash', ['-n', join(ROOT, 'scripts/build-legal-pdfs.sh')], { encoding: 'utf8' }).status, 0)
})
test('PDFs der Fassung 6: erst nach der Freigabe vorhanden; wenn vorhanden, PDF, Prüfsumme der Seite daneben', { skip: !existsSync(join(ROOT, 'public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf')) && 'PDFs entstehen erst nach der Freigabe (Task 11, Ablauf)' }, () => {
for (const p of ['abo-bedingungen.html', ...SLUGS.map((s) => `${s}-2026-10-16.html`)]) {
for (const p of ['abo-bedingungen.html', ...SLUGS.map((s) => `${s}-2026-10-28.html`)]) {
const pdf = join(ROOT, 'public/neu/rechtliches', p === 'abo-bedingungen.html' ? 'WEBklar-Abo-Vertragsunterlagen.pdf' : p.replace('.html', '.pdf'))
assert.equal(readFileSync(pdf).subarray(0, 5).toString(), '%PDF-', pdf)
assert.equal(readFileSync(`${pdf}.sha256`, 'utf8').trim(), createHash('sha256').update(readFileSync(join(ROOT, 'public/neu/rechtliches', p))).digest('hex'), p)
+10 -10
View File
@@ -141,14 +141,14 @@ test('Vertragsunterlagen je Version', () => {
test('Fassungstabelle: Reihenfolge, Muster nur am Ende, v6 im Vorschaupfad', () => {
assert.deepEqual(FASSUNGEN.map((f) => f.muster?.source ?? 'sonst'), ['-v6$', '-v5$', '-v4$', 'sonst'])
assert.deepEqual(termsDocFor('2026-10-16-v6'), {
version: '2026-10-16-v6', url: '/neu/rechtliches/abo-bedingungen.html',
assert.deepEqual(termsDocFor('2026-10-28-v6'), {
version: '2026-10-28-v6', url: '/neu/rechtliches/abo-bedingungen.html',
pdfFile: 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11',
})
for (const v of ['2026-10-16-v60', '2026-10-16-v6x', 'v6-2026-10-16', '2026-10-16-v66']) {
for (const v of ['2026-10-28-v60', '2026-10-28-v6x', 'v6-2026-10-28', '2026-10-28-v66']) {
assert.equal(termsDocFor(v).url, '/vertragsunterlagen/v3/abo-bedingungen.html', v)
}
const a = contractAttachmentFor('2026-10-16-v6')
const a = contractAttachmentFor('2026-10-28-v6')
assert.equal(a.filename, 'WEBklar-Abo-Vertragsunterlagen.pdf')
assert.ok(path.isAbsolute(a.path) && a.path.endsWith(path.join('public', 'neu', 'rechtliches', 'WEBklar-Abo-Vertragsunterlagen.pdf')))
})
@@ -159,8 +159,8 @@ test('Fassung 5 bleibt bis P10 an den heutigen Adressen, das Archiv ist vorberei
assert.deepEqual(v5.archiv, { url: '/vertragsunterlagen/v5/abo-bedingungen.html', pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf' })
})
test('isTermsV4OrNewer: v4, v5, v6 ja; v3, fehlende und fremde Endungen nein', () => {
for (const v of ['2026-09-30-v4', '2026-09-30-v5', '2026-10-16-v6']) assert.equal(isTermsV4OrNewer(v), true, v)
for (const v of ['2026-09-25-v3', '2026-09-25', null, undefined, '', '2026-10-16-v60', 7]) assert.equal(isTermsV4OrNewer(v), false, String(v))
for (const v of ['2026-09-30-v4', '2026-09-30-v5', '2026-10-28-v6']) assert.equal(isTermsV4OrNewer(v), true, v)
for (const v of ['2026-09-25-v3', '2026-09-25', null, undefined, '', '2026-10-28-v60', 7]) assert.equal(isTermsV4OrNewer(v), false, String(v))
})
test('aktuelleAboFassung: v6 nur mit neuer Ansicht', () => {
// appwriteUserId gehört zur gültigen Kunden-Sitzung (N1, schalter.js istKundenSitzung)
@@ -172,13 +172,13 @@ test('aktuelleAboFassung: v6 nur mit neuer Ansicht', () => {
Object.assign(config.kundenansichtNeu, { mode: 'pilot', pilotEmails: [] })
assert.equal(aktuelleAboFassung(k('a@example.invalid')), '2026-09-30-v5', 'leere Liste lässt niemanden durch')
config.kundenansichtNeu.pilotEmails = ['a@example.invalid']
assert.equal(aktuelleAboFassung(k(' A@Example.INVALID ')), '2026-10-16-v6', 'Großschreibung und Leerraum')
assert.equal(aktuelleAboFassung(k(' A@Example.INVALID ')), '2026-10-28-v6', 'Großschreibung und Leerraum')
assert.equal(aktuelleAboFassung(k('b@example.invalid')), '2026-09-30-v5')
assert.equal(aktuelleAboFassung(k('b@example.invalid', { viewAs: true, viewId: 'v1' })), '2026-10-16-v6', 'Mitarbeiter-Kundenansicht')
assert.equal(aktuelleAboFassung(k('b@example.invalid', { viewAs: true, viewId: 'v1' })), '2026-10-28-v6', 'Mitarbeiter-Kundenansicht')
Object.assign(config.kundenansichtNeu, { mode: 'on', pilotEmails: [] })
assert.equal(aktuelleAboFassung(k('b@example.invalid')), '2026-10-16-v6')
assert.equal(aktuelleAboFassung(k('b@example.invalid')), '2026-10-28-v6')
} finally { Object.assign(config.kundenansichtNeu, vorher) }
assert.deepEqual([config.abo.termsVersion, config.abo.termsVersionNeu], ['2026-09-30-v5', '2026-10-16-v6'])
assert.deepEqual([config.abo.termsVersion, config.abo.termsVersionNeu], ['2026-09-30-v5', '2026-10-28-v6'])
})
test('Produktansicht enthält Leistungen ohne 1-Postfach-Regel bei Wartung', () => {