# Kundenansicht neu, Paket P8: Rechtsrunde Fassung 6 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Die Rechtstexte der Kundenansicht neu liegen als Entwurf zur Freigabe vor (Abo-Bedingungen Fassung 6, Leistungsbeschreibungen und AVV je Funktion ohne Preise, Datenschutz-Prüfpunkte, Entscheidungsliste mit Kanzlei-Fragen). Der Server unterscheidet die Fassungen je Sitzung (Fassungstabelle, `aktuelleAboFassung`, Archiv v5, Wortlaute für die Bestellseiten), und zwei Skripte bauen die PDFs und laden die Fassungen nach der Freigabe in WOMS „Rechtliches“. Für Kunden mit alter Ansicht ändert sich nichts. **Architecture:** Die Texte sind Entwürfe: Markdown unter `docs/rechtliches/*-v6.md`, daraus erzeugt der HTML-Generator (neuer Satz `funktionen6`, neuer Stil) die Seiten unter `public/neu/rechtliches/`. Die Abo-Bedingungen sind wie Fassung 5 handgepflegtes HTML im neuen Stil, mit gleichen Paragrafen und Ankern. `products.js` bekommt die Fassungstabelle, die Wortlaute und die Unterlagen-Tabelle der Funktionen. Alle Tests prüfen Texte über feste Pflichtsätze, die in diesem Plan stehen; PDF-Bau und Hochladen laufen nie in diesem Paket. **Tech Stack:** Node 20 (Host 24), `node --test`, der vorhandene HTML-Generator `scripts/build-legal-html.mjs`, `scripts/build-legal-pdfs.sh` (headless Chrome, nur beschrieben), Appwrite-REST für den Upload (nur geschrieben, nie ausgeführt). **Spec:** `docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md` (5.9 komplett; E6, E7, E8, E9, E10, E17, E20, E21; C4, C5, C6, C8, C10, C11, C12, C14, C15; 5.5 und 5.6 für die Wortlaute) **Übersicht/Verträge:** `/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht/docs/superpowers/plans/2026-10-16-kundenansicht-00-uebersicht.md` (gilt wörtlich; dieses Paket liefert V12 Teil P8 und die Wortlaute für V5) **Worktree und Branch:** `/home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht`, Branch `kn/recht` (von Tag `kn-p0`). Am Ende Tag `kn-p8`. Alle Pfade unten sind relativ zu diesem Worktree. ## Global Constraints Es gelten **alle** Global Constraints der Übersicht wörtlich (Schalter, Aussehen, du-Anrede in Oberflächentexten, keine Funktionspreise, Hashes, Kundenart nur vom Server, Nachweise, Testbefehle, Commits, Sicherheitsblock, Prod-Daten). Paketeigene Zusätze: - **Vorbereitung (einmalig, kein Commit):** `git -C /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht log -1 --format=%D` nennt `kn-p0` und `kn/recht`; `ls server/services/kundenansicht/schalter.js node_modules` existiert (Symlink `ln -s ../build/node_modules node_modules`, nie `git add -A`); `free -m` zeigt mindestens 2000 MB verfügbar. - **Eingefroren (C8), nur lesen:** `public/rechtliches/*`, `public/vertragsunterlagen/v3/*`, `public/vertragsunterlagen/v4/*`, `public/vertragsunterlagen/*.html`, `public/abo-bedingungen.html`, `public/WEBklar-Abo-Vertragsunterlagen.pdf`, `public/datenschutz.html`, die bestehenden `docs/rechtliches/*.md` ohne `-v6`. Neue Fassungen entstehen nur als neue Dateien. Die Kopie unter `public/vertragsunterlagen/v5/` entsteht mit `cp` einzeln, nie mit Wildcards. - **Entwurfsstatus:** Jede neue Rechtsdatei trägt `Entwurf – Prüfung durch Kanzlei offen` (Markdown-Dateien in der Fassungszeile, die Abo-Seite in einem Absatz unter der Kopfzeile). In den Test-Dateien steht dafür die Konstante `ENTWURF = true`; der Ablauf nach der Freigabe (Task 11) setzt sie auf `false` und entfernt die Marken. - **Fassungen:** Abo `2026-10-16-v6` (`config.abo.termsVersionNeu`), Funktionsunterlagen `2026-10-16` (`FUNKTIONEN_FASSUNG_V6`). Das Datum ist der Entwurfstag; bei der Freigabe wird es auf den Freigabetag gesetzt (Task 11, Ablauf). - **Pfade der Entwürfe (Vertragsfrage 1):** Seite `public/neu/rechtliches/abo-bedingungen.html` (Adresse `/neu/rechtliches/abo-bedingungen.html`), PDF `public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf`, Funktionsunterlagen `public/neu/rechtliches/-2026-10-16.html|pdf`. - **Keine Funktionspreise** in `*-v6.md` und in den Funktionsseiten: kein `€`. Die Abo-Seite behält die Abo-Preise (20,00 €, 50,00 €, 1,00 €, 100 € pro Stunde) unverändert. - **Anrede der Rechtsdokumente:** wie bisher (3. Person „der Kunde“, in der Übersichtsbox der Abo-Seite „Ihr“); nur Wortlaute für die Oberfläche (Task 3) sind „du“. Entscheidung R12. - **Nie ausführen in diesem Paket:** `scripts/build-legal-pdfs.sh` (nur `bash -n`), `scripts/rechtliches-hochladen.mjs` (auch nicht mit `--probe`, `--karten-anlegen` oder `--pruefen`; die Tests rufen `main` nur mit Nachbau-`deps` auf), Chrome, Netzwerk. Erlaubt: `node scripts/build-legal-html.mjs funktionen6 …` (schreibt nur unter `public/neu/rechtliches/`). - **Pins, die unverändert bleiben und in Task 1, 2 und 12 mitlaufen:** `test/products.test.js:117-137` und `:187-200`, `test/mailTemplates.test.js:551-562`, `test/legalTexts.test.js:104-122` (darin `:120`, die `all`-Zeile von `build-legal-pdfs.sh`), `test/buildLegalHtml.test.js:336-368` (die Usage-Zeile `:367`) und `:418-448` (die Usage-Meldung von `build-legal-html.mjs`, `:436`). Angepasst wird keine bestehende Zeile außer dem Import-Block `test/products.test.js:6-17`. - **Testbefehle:** wie in der Übersicht. Mehrere Dateien nur mit `--test-concurrency=1`. Die ganze Suite läuft erst in Phase E. ## Review Focus Die fünf wahrscheinlichsten Fehlerfälle dieses Pakets, häufigster zuerst: 1. **Die Fassungsweiche trifft die falschen Kunden:** `-v6` zählt nur als Endung (nicht `-v60`, `-v66`, `v6-…`), Zustimmungen v3 bis v5 behalten Seite, PDF und `endParagraph` (Mails K02/K03/T05/T07), und v6 gilt nur bei neuer Ansicht (`off` mit `*`, leere Liste, E-Mail mit Großschreibung und Leerzeichen, Mitarbeiter-Kundenansicht). Tests: Task 1. 2. **Eingefrorene Fassungen werden verändert oder das Archiv weicht ab (C8):** Archiv-Kopie nicht bytegleich, PDF-Link doppelt, Originale angefasst. Tests: Task 2 (Pins), Task 14 (`git diff` gegen `kn-p0`). 3. **Preis, Alt-Wortlaut oder Lücke bei der Vorschuss-Regel in den neuen Texten:** `€`, „per Haken“, „Zahlungspflichtig“, „Hosting-Abo nicht enthalten“ bei der Analyse, „Vertrag über den Betrieb der Website“ bei Webshop und Buchungskalender, ein Dokument ohne „Vorschuss“ und ohne „zugunsten“ (E17), verschobene Nummern, auf die die AVV verweisen. Tests: Tasks 5, 7, 8, 9, 10. 4. **Wortlaut-Drift zwischen den Rechtstexten, den Konstanten und den Paketen P5/P7:** Platzhalter `{fassung}` ungefüllt, P5 zeigt noch den Entwurfstext oder verlinkt die Alt-Unterlagen mit Preisen. Tests: Task 3 (inklusive Verdrahtungstest, der nach dem Merge von P5 rot wird, bis verdrahtet ist). 5. **Hochladen ohne Freigabe, mehrere Dokumente in einem Aufruf, ein nicht committeter Stand oder eine fehlende WOMS-Karte:** ohne `--freigabe=JJJJ-MM-TT` und ohne genau einen `documentKey` bricht das Skript ab, ebenso beim Hochladen ohne Karte; `--probe` meldet eine fehlende Karte als Stopp (drei der elf Dokumente haben heute keine); Import des Moduls liest weder Schlüssel noch Netz. Tests: Task 13. ## Dateiübersicht | Datei | Aufgabe | Task | |---|---|---| | `server/services/products.js` | `FASSUNGEN`, `aktuelleAboFassung`, `isTermsV4OrNewer`, `termsDocFor`; `WORTLAUT_V6`, `mitFassung`, `FUNKTIONEN_FASSUNG_V6`, `FUNKTIONEN_UNTERLAGEN_V6` | 1, 3 | | `server/config.js` (nur `abo.termsVersionNeu`, Zeile bei `:855`) | Fassung 6 | 1 | | `public/vertragsunterlagen/v5/abo-bedingungen.html`, `…/WEBklar-Abo-Vertragsunterlagen.pdf` (neu) | Archiv Fassung 5, erst P10 aktiv | 2 | | `docs/rechtliches/abo-bedingungen-v6.md` (neu) | Entwurf: alle Änderungen gegenüber Fassung 5 im Wortlaut | 4 | | `public/neu/rechtliches/abo-bedingungen.html` (neu) | Abo-Bedingungen Fassung 6, neuer Stil | 5 | | `scripts/build-legal-html.mjs` | `stil: 'neu'`, Satz `funktionen6`, `--nur=` | 6 | | `docs/rechtliches/leistungsbeschreibung-{terminplaner,news,zeiten,webshop,unterkunft,website-analyse}-v6.md` (neu) | Leistungsbeschreibungen ohne Preise | 7, 8, 9 | | `docs/rechtliches/avv-{website-analyse,terminplaner,webshop,unterkunft}-v6.md` (neu) | AVV mit neuer Vorbemerkung | 9, 10 | | `public/neu/rechtliches/-2026-10-16.html` (10 Seiten, erzeugt) | Funktionsunterlagen | 7 bis 10 | | `docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md`, `docs/rechtliches/kundenansicht-entscheidungen.md` (neu) | Prüfpunkte, Entscheidungen R1 bis R15, Kanzlei-Fragen KQ1 bis KQ9, Ablauf nach der Freigabe | 11 | | `scripts/build-legal-pdfs.sh` | Modi `abo6`, `funktionen6` | 12 | | `scripts/rechtliches-hochladen.mjs` (neu) | Upload nach WOMS „Rechtliches“, fehlende Karten anlegen, Nachkontrolle | 13 | | `test/products.test.js` (Import `:6-17`, neue Tests nach `:137`), `test/legalTexts.test.js` (neuer Test nach `:448`) | Fassungstabelle, Archiv v5 | 1, 2 | | `test/aboWortlautV6.test.js`, `test/aboBedingungenV6.test.js`, `test/legalV6Build.test.js`, `test/funktionenRechtV6.test.js`, `test/kundenansichtEntscheidungen.test.js`, `test/rechtlichesHochladen.test.js` (alle neu) | je Task | 3, 4/5, 6/12, 7 bis 10, 11, 13 | --- ### Task 1: Fassungstabelle, Weiche und `termsVersionNeu` **Files:** - Modify: `server/services/products.js:28-68` (`isCurrentTerms`, `isTermsV4OrNewer`, `termsDocFor`) und der Import `neueAnsicht` am Dateianfang - Modify: `server/config.js:853-857` (eine Zeile im Abschnitt `abo`; Kommentar: gilt nur für Sitzungen mit neuer Ansicht, Datum bei Freigabe setzen) - Test: `test/products.test.js` (Import-Block `:6-17` um `FASSUNGEN, aktuelleAboFassung, isTermsV4OrNewer` erweitern; neue Tests nach `:137`) **Interfaces:** - Consumes: `neueAnsicht(req)` aus `server/services/kundenansicht/schalter.js` (V1); `config.kundenansichtNeu` = `{ mode, pilotEmails }`. - Produces (V12): ```js export const FASSUNGEN // Object.freeze([{ muster: /-v6$/, nummer: 6, url, pdfFile, endParagraph }, { muster: /-v5$/, nummer: 5, …, archiv: { url, pdfFile } }, { muster: /-v4$/, nummer: 4, … }, { sonst: true, nummer: 3, … }]) export function aktuelleAboFassung(req) // string: neueAnsicht(req) ? config.abo.termsVersionNeu : config.abo.termsVersion export function isTermsV4OrNewer(termsVersion) // v4, v5, v6 export function termsDocFor(termsVersion) // Form unverändert: { version, url, pdfFile, endParagraph } ``` `config.abo.termsVersionNeu = '2026-10-16-v6'`; `config.abo.termsVersion` bleibt `'2026-09-30-v5'`. - [ ] **Step 1: Failing tests** (an `test/products.test.js` anhängen) ```js 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', 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']) { assert.equal(termsDocFor(v).url, '/vertragsunterlagen/v3/abo-bedingungen.html', v) } const a = contractAttachmentFor('2026-10-16-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'))) }) test('Fassung 5 bleibt bis P10 an den heutigen Adressen, das Archiv ist vorbereitet', () => { const v5 = FASSUNGEN.find((f) => f.muster?.source === '-v5$') assert.equal(termsDocFor('2026-09-30-v5').url, '/abo-bedingungen.html') assert.deepEqual(Object.keys(termsDocFor('2026-09-30-v5')).sort(), ['endParagraph', 'pdfFile', 'url', 'version']) 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)) }) test('aktuelleAboFassung: v6 nur mit neuer Ansicht', () => { const k = (email, extra = {}) => ({ session: { customerId: 'c1', email, ...extra } }) const vorher = { ...config.kundenansichtNeu } try { Object.assign(config.kundenansichtNeu, { mode: 'off', pilotEmails: ['*'] }) assert.equal(aktuelleAboFassung(k('a@example.invalid')), '2026-09-30-v5', 'off schlägt die Liste') 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('b@example.invalid')), '2026-09-30-v5') assert.equal(aktuelleAboFassung(k('b@example.invalid', { viewAs: true, viewId: 'v1' })), '2026-10-16-v6', 'Mitarbeiter-Kundenansicht') Object.assign(config.kundenansichtNeu, { mode: 'on', pilotEmails: [] }) assert.equal(aktuelleAboFassung(k('b@example.invalid')), '2026-10-16-v6') } finally { Object.assign(config.kundenansichtNeu, vorher) } assert.deepEqual([config.abo.termsVersion, config.abo.termsVersionNeu], ['2026-09-30-v5', '2026-10-16-v6']) }) ``` - [ ] **Step 2: Rot** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/products.test.js` Expected: FAIL (`FASSUNGEN` und `aktuelleAboFassung` fehlen); die bisherigen Tests der Datei laufen grün. - [ ] **Step 3: Umsetzen** `config.abo.termsVersionNeu` ergänzen. In `products.js` die Tabelle `FASSUNGEN` einführen (Werte: v6 `url '/neu/rechtliches/abo-bedingungen.html'`, `pdfFile 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf'`, `endParagraph '§ 11'`; v5 und v4 mit den heutigen Werten aus `products.js:51-60`; `sonst` mit den Werten von v3 aus `:62-66`). `termsDocFor` sucht den ersten Eintrag, dessen `muster` auf einen String passt, sonst den Eintrag `sonst`; `version` ist die Eingabe, bei leerer oder fehlender Eingabe `TERMS_VERSION_V3`; die Rückgabe enthält nur die vier Felder (kein `archiv`, `muster`, `nummer`). `isTermsV4OrNewer` ist wahr, wenn ein Eintrag mit `muster` passt. `isCurrentTerms` bleibt unverändert exportiert. - [ ] **Step 4: Grün, alte Pins laufen mit** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/products.test.js test/mailTemplates.test.js test/legalTexts.test.js` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add server/services/products.js server/config.js test/products.test.js git commit -m "Abo-Fassung 6: Fassungstabelle, aktuelleAboFassung, termsVersionNeu" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 2: Archiv Fassung 5 vorbereiten **Files:** - Create: `public/vertragsunterlagen/v5/abo-bedingungen.html` (Kopie von `public/abo-bedingungen.html`, nur der PDF-Link zeigt aufs Archiv-PDF) - Create: `public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf` (bytegleiche Kopie von `public/WEBklar-Abo-Vertragsunterlagen.pdf`) - Test: `test/legalTexts.test.js` (neuer Test nach `:448`, Muster wie der Test zu Fassung 4 bei `:439-448`) **Interfaces:** - Consumes: `FASSUNGEN[v5].archiv` (Task 1). - Produces: das Archiv, das P10 aktiviert; die Originale bleiben unberührt. - [ ] **Step 1: Failing test** ```js // = `git show 7ae0ffd:public/abo-bedingungen.html | sha256sum` bzw. des PDFs (Fassung 5, Stand 30.09.2026) const V5 = 'public/vertragsunterlagen/v5/abo-bedingungen.html' const V5_SHA256 = '3b570eb2daed76d20fc5b8b9aea36a447f0481b21ac950452a82e454db45291e' const V5_PDF_SHA256 = 'a8344e83c4d8f5ed2610048a5ea9ca6b8d2082b5648e706ea1ef01c10087e37b' const V5_PDF_LINK = 'href="/WEBklar-Abo-Vertragsunterlagen.pdf"' const V5_ARCHIVE_PDF_LINK = 'href="/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf"' test('Fassung 5 archiviert (vorbereitet für P10): HTML bis auf den PDF-Link bytegleich, PDF bytegleich', () => { const archived = read(V5) assert.equal(archived.split(V5_ARCHIVE_PDF_LINK).length, 2, 'genau ein Archiv-Link') assert.ok(!archived.includes(V5_PDF_LINK)) assert.equal(createHash('sha256').update(archived.split(V5_ARCHIVE_PDF_LINK).join(V5_PDF_LINK)).digest('hex'), V5_SHA256) assert.equal(sha256('public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf'), V5_PDF_SHA256) assert.ok(archived.includes('Version 5 · Stand 30.09.2026')) }) ``` - [ ] **Step 2: Rot** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/legalTexts.test.js` Expected: FAIL (`ENOENT` auf `v5/abo-bedingungen.html`). - [ ] **Step 3: Archiv anlegen** `mkdir -p public/vertragsunterlagen/v5`; beide Dateien einzeln mit `cp` kopieren; in der **Kopie** die eine Stelle `href="/WEBklar-Abo-Vertragsunterlagen.pdf"` (Zeile 62) mit dem Edit-Werkzeug auf `V5_ARCHIVE_PDF_LINK` ändern. Die Originale nicht anfassen. - [ ] **Step 4: Grün** Run: wie Step 2. Expected: PASS (der neue Test und alle bisherigen Tests der Datei). - [ ] **Step 5: Commit** ```bash git add test/legalTexts.test.js public/vertragsunterlagen/v5/abo-bedingungen.html public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf git commit -m "Abo-Bedingungen Fassung 5: Archiv vorbereitet (erst mit P10 aktiv)" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 3: Wortlaute und Unterlagen-Tabelle für die Bestellseiten **Files:** - Modify: `server/services/products.js` (anhängen) - Test: `test/aboWortlautV6.test.js` (neu; Kopf wie `test/products.test.js:1-18`) **Interfaces:** - Consumes: nichts. - Produces (für P5, P7, P3): ```js export const FUNKTIONEN_FASSUNG_V6 // '2026-10-16' export const WORTLAUT_V6 // Object.freeze, tief // .linkzeile { bestellen, einschaltenMitAvv, einschaltenOhneAvv } Text mit genau einem Platzhalter {fassung}; bestellen gilt für Abo, Wechsel und Zusatzspeicher // .beginn { abo, wechsel, einrichtung } das Häkchen nur für privat in der Frist // .hinweis { shop, unterkunft, termine } Pflichten aus den alten Häkchen, ein Satz auf der Einschalt-Seite export function mitFassung(text, fassung) // ersetzt {fassung}; wirft bei leerer Fassung oder fehlendem Platzhalter export const FUNKTIONEN_UNTERLAGEN_V6 // { [key]: { lb: Dok, avv: Dok | null } }, Reihenfolge news, termine, shop, unterkunft, zeiten, analyse // Dok = { version, url, pdfFile, anhangName } ``` - [ ] **Step 1: Failing tests** (Kopf wie `test/products.test.js:1-5`: `test`, `assert`, `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.throws(() => mitFassung(WORTLAUT_V6.linkzeile.bestellen, ''), /Fassung/) assert.throws(() => mitFassung('ohne Platzhalter', '2026-10-16'), /Platzhalter/) }) test('Häkchen beginn: Wortlaut je Fall (nur privat in der Frist)', () => { assert.deepEqual(WORTLAUT_V6.beginn, { abo: 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.', wechsel: 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.', einrichtung: '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.', }) }) test('Pflichten aus den alten Häkchen verantwortung, stripe, gesundheit', () => { assert.deepEqual(WORTLAUT_V6.hinweis, { shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.', unterkunft: 'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.', termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.', }) }) test('alle Wortlaute: du-Form, kein Preis, nur {fassung} als Platzhalter, tief eingefroren', () => { const alle = JSON.stringify(WORTLAUT_V6) assert.doesNotMatch(alle, /€|\bSie\b|\bIhr(e[nmrs]?)?\b/) assert.deepEqual([...alle.matchAll(/\{[a-z]+\}/g)].map((m) => m[0]).filter((x) => x !== '{fassung}'), []) for (const t of Object.values(WORTLAUT_V6.linkzeile)) assert.equal((t.match(/\{fassung\}/g) ?? []).length, 1) 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.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(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']) }) 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') assert.equal(config.aboFunktionen.fassung, FUNKTIONEN_FASSUNG_V6) assert.equal(config.aboFunktionen.texte.beginn, WORTLAUT_V6.beginn.einrichtung) for (const key of ['termine', 'shop', 'unterkunft']) assert.equal(reg.consentAbo(key).items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, FUNKTIONEN_FASSUNG_V6), key) for (const key of ['news', 'zeiten']) assert.equal(reg.consentAbo(key).items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, FUNKTIONEN_FASSUNG_V6), key) }) ``` - [ ] **Step 2: Rot** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/aboWortlautV6.test.js` Expected: FAIL (Exporte fehlen). - [ ] **Step 3: Umsetzen** Konstanten in `products.js` wörtlich wie in den Tests; `WORTLAUT_V6.linkzeile.bestellen` ist der Satz aus Test 1 mit `{fassung}` statt der Fassung. `FUNKTIONEN_UNTERLAGEN_V6` über einen lokalen Helfer `dok(slug, anhang)`: `url = /neu/rechtliches/-.html`, `pdfFile = neu/rechtliches/-.pdf`, `anhangName = -.pdf`. Alles tief mit `Object.freeze`. - [ ] **Step 4: Grün** Run: wie Step 2. Expected: PASS; der Verdrahtungstest erscheint als übersprungen. - [ ] **Step 5: Commit** ```bash git add server/services/products.js test/aboWortlautV6.test.js git commit -m "Abo-Fassung 6: Wortlaute für Linkzeile und Häkchen, Unterlagen-Tabelle der Funktionen" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 4: Abo-Bedingungen Fassung 6, Entwurf der Änderungen **Files:** - Create: `docs/rechtliches/abo-bedingungen-v6.md` - Test: `test/aboBedingungenV6.test.js` (neu) **Interfaces:** - Consumes: nichts. Basis ist die Fassung 5 (`public/vertragsunterlagen/v5/abo-bedingungen.html`). Die Paragrafenfolge § 1 bis § 16 und alle Anker bleiben (Querverweise in Mails, `endParagraph '§ 11'`). - Produces: die Pflichtsätze (Konstante `PFLICHT` im Test), die Task 5 in die Seite übernimmt. Aufbau der Datei: `# Abo-Bedingungen Fassung 6 (Entwurf)`, eine Kopfzeile (Stand, „Entwurf zur Freigabe, keine Rechtsberatung“, Basis Fassung 5), je Stelle ein `## ` mit dem Pflichtsatz als Absatz und einer Zeile `**Begründung.**` mit Spec- oder Entscheidungsnummer, zuletzt `## Was gleich bleibt` (Preise, Laufzeiten, Paragrafenfolge, Widerrufsbelehrung und Muster-Formular, § 10, Anlagen 1 und 2). - [ ] **Step 1: Failing test** (Kopf: `import { test } from 'node:test'`, `assert`, `readFileSync`; `const { config } = await import('../server/config.js')`; `const ENTWURF = true`; `const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8')`) ```js const PFLICHT = [ // stelle = Überschrift im Entwurf; von/bis = Anker-Ids der Seite (null = Dokumentanfang bzw. -ende) { stelle: 'Das Wichtigste auf einen Blick', von: null, bis: 'p1', texte: ['Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).'] }, { stelle: '§ 1 Abs. 2', von: 'p1', bis: 'p2', texte: ['Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.'] }, { stelle: '§ 2 Abs. 2 c', von: 'p2', bis: 'p3', texte: ['Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.'] }, { stelle: '§ 2 Abs. 6', von: 'p2', bis: 'p3', texte: ['Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks.'] }, { stelle: '§ 3 Abs. 8', von: 'p3', bis: 'p4', texte: [ 'Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.', 'Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.'] }, { stelle: '§ 4 Abs. 2', von: 'p4', bis: 'p5', texte: ['Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.'] }, { stelle: '§ 4 Abs. 8', von: 'p4', bis: 'p5', texte: ['Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.'] }, { stelle: '§ 4 Abs. 9', von: 'p4', bis: 'p5', texte: ['Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.'] }, { stelle: '§ 6 Abs. 6', von: 'p6', bis: 'p7', texte: ['Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.'] }, { stelle: '§ 9 Abs. 6', von: 'wechsel', bis: 'zusatzspeicher', texte: ['Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.'] }, { stelle: '§ 11 Abs. 4', von: 'p11', bis: 'p12', texte: ['Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.'] }, { stelle: '§ 14 Abs. 2', von: 'p14', bis: 'p15', texte: ['Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.'] }, { stelle: '§ 15', von: 'p15', bis: 'p16', texte: ['Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.'] }, { stelle: 'Leistungsbeschreibung A Nr. 1', von: 'hosting', bis: 'wartung', texte: ['Website-Analyse im Kundenportal (§ 3 Abs. 8)'] }, { stelle: 'Leistungsbeschreibung B Nr. 1', von: 'wartung', bis: 'widerruf', texte: ['Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)'] }, { stelle: 'Widerruf: Hinweise', von: 'widerruf-hinweise', bis: 'avv', texte: ['Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).'] }, { stelle: 'Anlage 3', von: 'sub', bis: null, texte: ['Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)', 'Deutschland (Rechenzentrum Falkenstein)'] }, ] 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.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) { const teil = md.slice(md.indexOf(`## ${p.stelle}\n`)).split(/\n## /)[0] for (const t of p.texte) assert.ok(teil.includes(t), `${p.stelle}: ${t.slice(0, 50)}`) assert.match(teil, /\*\*Begründung\.\*\*/, p.stelle) } assert.doesNotMatch(md.slice(0, md.indexOf('## Was gleich bleibt')), /€/, 'die Pflichtsätze nennen keinen Preis') }) ``` - [ ] **Step 2: Rot** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/aboBedingungenV6.test.js` Expected: FAIL (`ENOENT` auf den Entwurf). - [ ] **Step 3: Entwurf schreiben** `docs/rechtliches/abo-bedingungen-v6.md` nach dem Aufbau oben. Die Pflichtsätze wörtlich aus `PFLICHT`; die Begründung nennt Spec-Abschnitt oder Entscheidung (E6, E9, E10, E17, E21, C4, C5, C10, C11, C12) und bei den offenen Punkten die Nummer aus der Entscheidungsliste (R1 bis R15, Task 11). - [ ] **Step 4: Grün** Run: wie Step 2. Expected: PASS. - [ ] **Step 5: Commit** ```bash git add docs/rechtliches/abo-bedingungen-v6.md test/aboBedingungenV6.test.js git commit -m "Abo-Bedingungen Fassung 6: Entwurf der Änderungen im Wortlaut" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 5: Abo-Bedingungen Fassung 6 als Seite im neuen Stil **Files:** - Create: `public/neu/rechtliches/abo-bedingungen.html` (Basis: `cp public/abo-bedingungen.html …`, das Original bleibt unberührt) - Test: `test/aboBedingungenV6.test.js` (erweitern) **Interfaces:** - Consumes: `PFLICHT` (Task 4), `FASSUNGEN` (Task 1: Adresse und PDF-Pfad der v6), das Archiv `public/vertragsunterlagen/v5/abo-bedingungen.html` (Task 2) als Vergleichsbasis. - Produces: die Seite, die `termsDocFor('2026-10-16-v6').url` liefert. Neue Absätze stehen als `
  • …
  • ` (vier Leerzeichen, eine Zeile); der neue Absatz (8) in § 3 als `

    (8) Funktionen

    ` mit zwei `

    `; die Übersichtszeile als `Funktionen…` nach der Zeile Wartungs-Abo. - [ ] **Step 1: Failing tests** (an die Datei aus Task 4 anhängen; Hilfen `strip` und `between` aus `test/legalTexts.test.js:17-36` kopieren, kein Import möglich) ```js const V6 = 'public/neu/rechtliches/abo-bedingungen.html', V5A = 'public/vertragsunterlagen/v5/abo-bedingungen.html' const abschnitt = (html, von, bis) => { const a = von ? html.indexOf(`id="${von}"`) : 0 assert.ok(a >= 0, `Anker #${von}`) const b = bis ? html.indexOf(`id="${bis}"`, a + 1) : html.length assert.ok(b > a, `Anker #${bis}`) return strip(html.slice(a, b)) } test('v6-Seite: gleiche Anker wie Fassung 5, Absatzzahlen der geänderten Paragrafen', () => { const h = lies(V6), ids = (x) => [...x.matchAll(/ id="([^"]+)"/g)].map((m) => m[1]) assert.deepEqual(ids(h), ids(lies(V5A))) const n = (von, bis) => [...h.slice(h.indexOf(`id="${von}"`), h.indexOf(`id="${bis}"`)).matchAll(/^

  • ([\s\S]*?)<\/li>$/gm)].length assert.deepEqual([['p1', 'p2'], ['p2', 'p3'], ['p4', 'p5'], ['p6', 'p7'], ['wechsel', 'zusatzspeicher'], ['p11', 'p12'], ['p14', 'p15']].map(([a, b]) => n(a, b)), [4, 6, 9, 6, 6, 4, 2]) assert.equal((h.slice(h.indexOf('id="p3"'), h.indexOf('id="p4"')).match(/

    \(\d\)/g) ?? []).length, 8) }) test('v6-Seite: Version, Stand, Entwurfsmarke und PDF-Link passen zu termsVersionNeu', () => { const m = /^(\d{4})-(\d{2})-(\d{2})-v6$/.exec(config.abo.termsVersionNeu); assert.ok(m) const h = lies(V6), [, y, mo, d] = m assert.ok(strip(h).includes(`Version 6 · Stand ${d}.${mo}.${y}`)) assert.ok(h.includes(`content: "Version 6 · Stand ${d}.${mo}.${y}"`), 'Kopfzeile im Druck') assert.ok(!strip(h).includes('Version 5')) assert.equal(strip(h).includes('Entwurf – Prüfung durch Kanzlei offen'), ENTWURF) assert.ok(h.includes('href="/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf"') && !h.includes('href="/WEBklar-Abo-Vertragsunterlagen.pdf"')) }) test('v6-Seite: jede Pflichtstelle steht im Wortlaut an ihrem Platz', () => { const h = lies(V6) for (const p of PFLICHT) for (const t of p.texte) assert.ok(abschnitt(h, p.von, p.bis).includes(t), `${p.stelle}: ${t.slice(0, 50)}`) assert.ok(strip(h).includes(`Funktionen ${PFLICHT[0].texte[0]}`), 'Zeile „Funktionen“ in der Übersichtstabelle') }) test('v6-Seite: Widerrufsbelehrung und Muster-Formular wortgleich mit Fassung 5, Abo-Preise unverändert, Funktionen ohne Preis', () => { const h = lies(V6), v5 = lies(V5A) const muster = (x) => between(abschnitt(x, 'widerruf', 'formular'), 'Widerrufsrecht Sie haben das Recht', 'Ende der Widerrufsbelehrung') assert.equal(muster(h), muster(v5)) const formular = (x) => between(abschnitt(x, 'formular', 'widerruf-hinweise'), 'Muster-Widerrufsformular', '(*) Unzutreffendes streichen.') assert.equal(formular(h), formular(v5)) for (const s of ['20,00 €', '50,00 €', '1,00 €', '100 € pro Stunde', 'zwölf Monaten']) assert.ok(strip(h).includes(s), s) assert.doesNotMatch(abschnitt(h, 'p3', 'p4').split('(8) Funktionen')[1], /€/) }) test('v6-Seite: neues Aussehen, keine alten Farben, Druckkopf', () => { const style = lies(V6).match(/