# Rechtstexte – Umsetzungsplan (Strang L7) > **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 des Vorhabens „Aufträge und Rechnungen“ liegen in Endfassung zur Freigabe vor: die Wortlaute der Auftragsbestätigung (Textbausteine, Bestätigungs-Häkchen, Frühbeginn-Erklärung, Widerrufsbelehrung mit Muster-Widerrufsformular) in `server/services/auftraege/texte.js`, die AGB als wortgleiche Kopie der Website mit Seite und PDF, die Datenschutzerklärung mit neuem Abschnitt 9a und ergänztem Widerrufsabschnitt, die Druck-Skripte und ein Freigabe-Dokument für den Nutzer. Das Dokument zeigt außerdem die rechtlich gefärbten Sätze der Kundenseiten (Strang L6) und der Mails K31, K32, T22 sowie des Blocks `projekt` in K04 und T09 (Strang L5) im Wortlaut der Dateien. **Architecture:** `texte.js` bleibt die einzige Quelle der Wortlaute (Exportnamen und Formen aus V7 unverändert). `scripts/build-legal-html.mjs` bekommt den Satz `auftraege`: Die Widerrufsbelehrung wird aus `texte.js` zu Markdown zusammengesetzt, die AGB kommen aus `docs/rechtliches/agb.md`; beide laufen durch denselben `render()` wie die Seiten der anderen Sätze. `scripts/build-legal-pdfs.sh auftraege` druckt beide Seiten mit Chrome und legt neben jedes PDF die SHA-256 seiner HTML-Seite. Die Datenschutzerklärung bleibt handgepflegtes HTML; ihr PDF druckt `build-legal-pdfs.sh datenschutz`. Alle Prüfungen stehen in der neuen Testdatei `test/auftraegeRechtstexte.test.js` und in einer geänderten Zeile von `test/legalTexts.test.js`. Das Freigabe-Dokument erzeugt ein Skript außerhalb des Repos aus den Dateien (Abschnitte 1 bis 6 aus diesem Branch, Abschnitt 7 mit `git show` aus den Ständen von L5 und L6); es lässt sich beliebig oft wiederholen. **Tech Stack:** Node 20/24 (ESM, `node --test`), Bash, Chrome headless (nur für den PDF-Druck), die Markdown-Teilmenge des vorhandenen Generators. **Spec:** `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege/docs/superpowers/specs/2026-10-06-auftraege-und-rechnungen-design.md` (Abschnitt 8 Rechtsrunde, Abschnitt 2 Verbraucher) **Übersicht/Verträge:** `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege/docs/superpowers/plans/2026-10-06-auftraege-00-uebersicht.md` (V7, Dateizuständigkeit, Global Constraints, Livegang) **Worktree und Branch:** `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7`, Branch `auftraege/l7-rechtstexte` (aus `feature/auftraege` nach L0). Die Plan-Dateien und die Übersicht liegen im Basis-Worktree `dev-auftraege` und werden dort nur gelesen. --- ## Vorab geprüft (06.10.2026, vor dem Schreiben dieses Plans) 1. **`AGB_FASSUNG = '2026-09-30'` ist richtig.** `git -C /home/webklar/apps/webklar.com/build log -- src/pages/AGB.tsx` nennt als letzte Änderung des Vertragstexts `72f278b` vom 30.09.2026 („AGB § 9: Zeiterfassung in 15-Minuten-Schritten und Unterstützung“). Die späteren Commits (`77672b5` Seitentitel, `80099cb` und `b46527e` Schreibweise der Marke im Seitenkopf, `1c8cfa5` Relaunch-Layout vom 01.10.2026) ändern nur Markup, Kopf und Titel: Der Klartext von Titelblock und Nr. 1 bis 14 ist in `72f278b` und im heutigen Stand Zeichen für Zeichen gleich. `konstanten.js` bleibt unverändert; dieser Strang ändert dort nichts. 2. **Norm der Frühbeginn-Erklärung: § 356 Abs. 5 Nr. 2 BGB und § 357a Abs. 2 BGB.** Die frühere Zählung „§ 356 Abs. 4 BGB“ (sie stand bis zur Korrektur vom 05.10.2026 in den Texten der News-Pinnwand) ist überholt. Nach dem aktuellen Text auf gesetze-im-internet.de (abgerufen am 06.10.2026; Auszug in `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt`) regelt Abs. 4 die Höchstfrist von zwölf Monaten und 14 Tagen. Das Erlöschen bei Dienstleistungen steht in Abs. 5: Nr. 1 gilt für Verträge ohne Preis, Nr. 2 für Verträge mit Preis (ausdrückliche Zustimmung vor Beginn, bei außerhalb von Geschäftsräumen geschlossenen Verträgen auf einem dauerhaften Datenträger, Bestätigung der Kenntnis). Ein Projektauftrag ist eine entgeltliche Dienst- bzw. Werkleistung: Nr. 2 passt, Nr. 1 nicht. Abs. 6 betrifft die Bereitstellung digitaler Inhalte und passt nicht zu einem Auftrag, bei dem WEBklar eine Leistung erbringt (als Kanzlei-Frage E9 a im Freigabe-Dokument vermerkt). Den Wertersatz regelt § 357a Abs. 2 BGB (Verlangen als Voraussetzung, Berechnung nach dem vereinbarten Gesamtpreis). Die Erklärungen der Funktionen (`server/config.js`, gepinnt in `test/funktionenRecht.test.js`) nennen dieselbe Zählung; die Erklärung für Projektaufträge übernimmt ihren Satzbau. 3. **§ 649 BGB ist der Kostenanschlag, nicht § 650 (Planrunde, zweiter Abruf am 06.10.2026).** Der Satz „Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.“ (`AB_TEXTE.aufwand`) stützt sich auf § 649 Abs. 2 BGB. Die amtliche Seite gesetze-im-internet.de nennt § 649 „Kostenanschlag“; Abs. 2 lautet: „Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.“ § 650 BGB ist dagegen der „Werklieferungsvertrag; Verbrauchervertrag über die Herstellung digitaler Produkte“ (sein Abs. 2 verdrängt für Verbraucherverträge über digitale Inhalte die §§ 633 bis 640, siehe Kanzlei-Frage E9 a und d). Die Annahme im Plan Z (dessen Vertragsfrage 21), der Kostenanschlag stehe in § 650, trifft nach dem heutigen Stand der Seite nicht zu. Auszüge und Zitat stehen in `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt`; der Test `normen_kostenanschlag` hält das fest. 4. **Das Muster der Belehrung ist aktuell.** `docs/rechtliches/quellen/anlage-1.txt` und `anlage-2.txt` stimmen mit dem Text auf gesetze-im-internet.de vom 06.10.2026 überein (Satzvergleich; nur die Formatierung der Zeile „Gestaltungshinweise“ weicht ab). Die Belehrung für Projektaufträge hat denselben Unternehmer, dieselbe Art Vertrag (Dienstleistung) und dieselbe Widerrufsseite wie die der Funktionen und ist deshalb mit `widerrufsbelehrungFunktion()` wortgleich; ein Test hält das fest. 5. **Probelauf der Entwürfe.** Alle Entwürfe dieses Plans (Wortlaute, Tests, Edit-Schritte, erzeugte Seiten, Skript, Datenschutz-Änderungen, Freigabe-Dokument) wurden in Kopien des Repos im Scratchpad geprüft, nicht im Repo und ohne Chrome (für Teil 4 der Tests dienten Platzhalter-PDFs mit passender Prüfsumme): Jeder `old_string` der Edit-Schritte kommt in seiner Datei genau einmal vor. `test/auftraegeRechtstexte.test.js` ist mit 32 Tests grün (`skipped 0`). Das Freigabe-Skript lief gegen ein Test-Repository mit Attrappen der Stränge L5, L6 und L7: Stränge nicht gemergt und gemergt ergeben Ende-Code 0, ein fehlender Branch von L6 Code 3, ein K32 mit dem alten Satz Code 4, ein unsauberer Worktree bricht ab; sein Selbsttest vergleicht die eigene Darstellung der Mails mit `renderMail` an den Vorlagen K16, K15, K04 und T09. Grün sind auch `legalTexts` (bis auf den PDF-Pin, der erst nach dem Neudruck in Task 6 passt), `buildLegalHtml`, `legalDocs`, `zeitenRecht`, `termineKanzleiPaket`, `termineVertragsunterlagen`, `termineRechtstexte`, `termineRechtstexteArchiv`, `avvPage`, `analyseLegal` und `mailTemplates`. Kommt ein Anker der Edit-Schritte nicht genau einmal vor, ist der Ausgangsstand ein anderer als beim Probelauf (andere Zeilenstände nach einem Merge): Datei lesen, die Stelle sinngemäß anpassen, den Wortlaut der Texte nie ändern und den Fall im Bericht nennen. ## Global Constraints Alle Punkte der Übersicht gelten. Zusätzlich für diesen Strang: - **Nur die Dateien der Tabelle „Dateien dieses Strangs“.** Unverändert bleiben `server/services/auftraege/konstanten.js` (die eine erlaubte Zeile `AGB_FASSUNG` bleibt `'2026-09-30'`), `test/buildLegalHtml.test.js`, `server/services/legalDocs.js` und alle vorhandenen Dateien unter `public/rechtliches/`. Neue Dateien unter `public/rechtliches/` entstehen nur durch die Skripte. - **Wortlaute exakt wie in diesem Plan.** Rechtstexte werden nicht „verbessert“, gekürzt oder umgestellt; Zweifel gehören in den Bericht (Vertragsfragen), nicht in den Text. Eigene Wortlaute stehen in der Sie-Form und ohne Gedankenstriche im Fließtext (die Gedankenstriche in den Überschriften der Website-AGB und die Aufzählungszeichen des gesetzlichen Formulars bleiben, weil sie Vorlage sind). - **`texte.js` behält genau die Exporte und Formen aus V7.** Keine neuen Exporte. Die einzigen Ergänzungen sind das optionale Feld `privat` bei `AB_TEXTE.termine` und die Prüfung der Pflichtwerte (TypeError statt „undefined“ im Text). - **PDF-Druck (Chrome):** nur hinter dem flock der Übersicht, vorher `free -m` (unter 1500 MB „available“ warten), mit `nice -n 10`, die PDFs nacheinander. Das Skript begrenzt jeden Chrome-Aufruf auf 120 Sekunden (`timeout 120`); bricht ein Druck ab, nicht von Hand wiederholen, sondern im Bericht melden. Nie ein Ersatz-PDF einchecken. Gedruckt wird nur in Task 5 (zwei PDFs) und Task 6 (Datenschutz). - **Suchen** mit `command grep -a …`: In dieser Shell ist `grep` eine Funktion, die Dateien still auslässt. - **Tests nur gezielt:** `timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/`. Die volle Suite läuft nur in Z1. - **Ausgelieferte Fassungen werden nie überschrieben, alte AGB-PDF-Fassungen bleiben liegen.** Dieser Strang erzeugt nur die Fassungen 2026-10-06 (Texte) und 2026-09-30 (AGB). Jede gesendete Auftragsbestätigung verweist über `agbFassung` auf ihre AGB-Fassung (Anhang der Mail K31, Link der Bestätigungsseite): Eine spätere neue Fassung kommt als neue Datei dazu, eine ältere wird nie gelöscht, umbenannt oder neu gedruckt. - **Zeichenvorrat der Texte:** In `texte.js` stehen nur Zeichen, die die PDF-Schrift Figtree (Regular und SemiBold) hat. Der Test `zeichenvorrat_figtree` hält die geprüfte Liste fest (Basic Latin, Umlaute, ß, §, € und die typografischen Zeichen „ “ ‚ ‘ ’ ” – — … − • →, geprüft am 06.10.2026 mit fontkit); schmale Leerzeichen (U+202F, U+2009), Häkchen, Emoji und andere Schriften fehlen in Figtree. Nach dem Merge prüft der Wächter-Test von L4 (`test/auftraegePdfDaten.test.js`) gegen die Schrift selbst. - **Schreibweise im Code:** nie ein Unicode-Escape mit genau vier Hexziffern (Übersicht, Global Constraints); die Entwürfe dieses Plans benutzen echte Zeichen, `\u{…}` oder `String.fromCodePoint`. - **Außerhalb des Worktrees** werden nur geschrieben: das Freigabe-Dokument `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md` und sein Werkzeug im Ordner `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/` (Vorlage, Skript und Hilfsmodul, nicht eingecheckt) sowie der Scratchpad für Hilfsdateien. Kein Upload in WOMS → Rechtliches: Das geschieht nach der Freigabe durch den Orchestrator. - **Git:** nur im Worktree `dev-auftraege-l7`, Commits mit einzeln genannten Dateien, Nachricht deutsch, Schlusszeile `Co-Authored-By: Claude Opus 5.5 `; kein Push, nie `--force`, nie `git add -A` (der Symlink `node_modules` ist unversioniert). - **Der Datenschutz-Commit (Task 6) ist der letzte Commit des Strangs** (entschieden, Übersicht „Nachträge“ und Livegang). `public/datenschutz.html` und das PDF sind sofort öffentlich, sobald `main` gepusht wird; deshalb führt Z1 nur `auftraege/l7-rechtstexte~1` zusammen, und der Commit kommt erst zum Livegang der Auftragsbestätigung nach der Freigabe dazu. Sein Betreff beginnt mit „Datenschutz: Abschnitt 9a“ (Z1 findet ihn mit `git log --grep='^Datenschutz: Abschnitt 9a'`). Die Tasks 7 und 8 committen nichts; eine Korrektur aus Task 8 wird **vor** diesen Commit eingefügt (Task 8, Step 6), nie dahinter. ## Review Focus 1. **Die Widerrufsbelehrung weicht vom gesetzlichen Muster ab.** Prüfung in Task 2: `belehrung_muster`, `belehrung_widerrufsfunktion`, `belehrung_wertersatz` und `formular_muster` vergleichen Absatz für Absatz mit `anlage-1.txt` und `anlage-2.txt`; `belehrung_gleich_funktionen` verlangt Wortgleichheit mit `widerrufsbelehrungFunktion()`. 2. **Die Frühbeginn-Erklärung nennt die falsche Norm oder lässt ein Element weg** (Verlangen, Kenntnis vom Erlöschen, Wertersatz). Prüfung in Task 2: `fruehbeginn_text` (drei Sätze, Wortlaut gepinnt, kein „Abs. 4“, dasselbe Satzende wie die Erklärung der Funktionen) und `normen_zaehlung` (die Zählung gegen den Gesetzestext der Quelle). 3. **Die AGB-Kopie weicht von der Website ab, oder die Verweise der Auftragsbestätigung auf AGB Nr. 7, 8 und 9 treffen nicht.** Prüfung in Task 3 und 4: `agb_text_gleich_website` (Klartext gegen `AGB.tsx`, auf dem Server nicht übersprungen), `agb_text_pin` (SHA-256 des Klartexts), `agb_md_aufbau`, `auftraege_html_text_gleich_quelle`. 4. **Datenschutz-PDF und HTML laufen auseinander, oder die Änderung bricht gepinnte Sätze und Zählungen anderer Tests.** Prüfung in Task 6: `pdf_pruefsumme` in `legalTexts.test.js` (erst nach dem Neudruck grün), `ds_9a_stelle`, `ds_9a_inhalt`, `ds_widerruf_projektauftrag` und die Läufe der Tests, die `public/datenschutz.html` lesen (`legalTexts`, `zeitenRecht`, `termineKanzleiPaket`, `termineVertragsunterlagen`). 5. **Seite und PDF gehören nicht zusammen (HTML geändert, PDF nicht neu gedruckt) oder das Druck-Skript bricht bestehende Modi.** Prüfung in Task 4 und 5: `auftraege_html_gleich_generator` (Seite = `render(Quelle)`, deterministisch), `auftraege_pdf_dateien` (SHA-256 der HTML-Seite neben dem PDF), `auftraege_script_modus` und der unveränderte Lauf von `test/buildLegalHtml.test.js`. 6. **Das Freigabe-Dokument zeigt nicht, was Kunden wirklich lesen** (Mailtexte oder Seitentexte von Hand kopiert, veraltet, vom Vertrag V6 abweichend). Prüfung in Task 7: Das Skript liest die Dateien aus Git statt abzutippen, rendert die Mails mit denselben Schritten wie `renderMail` (Selbsttest gegen `renderMail` bei jedem Lauf), prüft die Zusagen der Planrunde (K32 gibt `FRUEHBEGINN_TEXT` wörtlich wieder und enthält den Satz „Widerrufsrecht bleibt unberührt“ nicht, die Kopie der K31 für das Team hat keinen Link, T22 trägt „[Aufträge]“) und meldet fehlende Quellen (Ende-Code 3) und Befunde (Ende-Code 4) im Dokument selbst. ## Dateien dieses Strangs Alle Dateien stehen in der Zuständigkeitstabelle der Übersicht; die Zusatzdateien `docs/rechtliches/agb.md`, `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt` und die Stand-Zeile in `test/legalTexts.test.js` sind dort seit der Planrunde eingetragen („Nachträge“, L7). | Datei | Aktion | Task | |---|---|---| | `server/services/auftraege/texte.js` | Wortlaute ersetzen (Exporte und Formen aus V7 bleiben) | 2 | | `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt` | neu: Auszüge der Normen mit Quelle | 2 | | `test/auftraegeRechtstexte.test.js` | neu, wächst in den Tasks 2 bis 6 | 2 bis 6 | | `docs/rechtliches/agb.md` | neu: Quelle der AGB-Kopie | 3 | | `scripts/build-legal-html.mjs` | Satz `auftraege` | 4 | | `public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.html`, `public/rechtliches/agb-2026-09-30.html` | neu, vom Generator erzeugt | 4 | | `scripts/build-legal-pdfs.sh` | Modus `auftraege` | 5 | | `public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.pdf` und `.pdf.sha256`, `public/rechtliches/agb-2026-09-30.pdf` und `.pdf.sha256` | neu, vom Skript erzeugt | 5 | | `public/datenschutz.html` | Abschnitt 9a, Widerrufsabschnitt, Abschnitt 6, Stand | 6 | | `docs/WEBklar-Datenschutzerklaerung.pdf` und `.pdf.sha256` | neu gedruckt | 6 | | `test/legalTexts.test.js` | eine Zeile (Pin des Stands) | 6 | | `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md` | Freigabe-Dokument, außerhalb des Repos, erzeugt | 7 | | `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/` mit `rechtsrunde-vorlage.md`, `rechtsrunde-erzeugen.mjs`, `rechtsrunde-quellen.mjs` | Vorlage, Skript und Hilfsmodul des Freigabe-Dokuments, außerhalb des Repos, nicht eingecheckt | 7 | --- ### Task 1: Worktree einrichten und Ausgangslage prüfen **Files:** keine Änderung (nur Worktree). **Interfaces:** keine. - [ ] **Step 1: Voraussetzung prüfen.** L0 muss fertig sein: `texte.js` und `konstanten.js` liegen in `feature/auftraege`. ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege git log --oneline -5 git status --short ls server/services/auftraege/texte.js server/services/auftraege/konstanten.js command grep -a -n "AGB_FASSUNG" server/services/auftraege/konstanten.js ``` Erwartet: beide Dateien vorhanden, `AGB_FASSUNG = '2026-09-30'`. Fehlt eine Datei, ist L0 nicht fertig: nicht weitermachen, im Bericht melden. - [ ] **Step 2: Worktree und Branch anlegen** (Befehle aus der Übersicht). ```bash git -C /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege worktree add ../dev-auftraege-l7 -b auftraege/l7-rechtstexte feature/auftraege cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 ln -s ../dev-auftraege/node_modules node_modules git status --short ``` Erwartet: nur `?? node_modules`. - [ ] **Step 3: Lesen.** Spec Abschnitt 2 (Verbraucher) und Abschnitt 8 (Rechtsrunde), Übersicht (V7, Dateizuständigkeit, Global Constraints) und die vorläufige `server/services/auftraege/texte.js` von L0. Die Exporte der vorläufigen Datei festhalten: ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 node --input-type=module -e "import * as t from './server/services/auftraege/texte.js'; console.log(Object.keys(t).join(' '))" ``` Erwartet: die acht Namen aus V7 (`ABSENDER AB_TEXTE BESTAETIGUNG_TEXT FRUEHBEGINN_FASSUNG FRUEHBEGINN_TEXT TEXTE_FASSUNG WIDERRUFSBELEHRUNG_PROJEKT WIDERRUFSFORMULAR`). Hat L0 weitere Exporte angelegt, ist das ein Vertragsbruch: nicht übernehmen, im Bericht melden. - [ ] **Step 4: Ausgangslage der Tests** (alle sollten grün sein; ein roter Test im Ausgangsstand wird notiert und nicht behoben). ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 for f in legalTexts buildLegalHtml legalDocs zeitenRecht termineKanzleiPaket termineVertragsunterlagen; do echo "== $f"; timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/$f.test.js 2>&1 | command grep -a -E "^(✖|ℹ (tests|pass|fail|skipped))" done ``` - [ ] **Step 5: Hilfsordner prüfen.** ```bash ls -d /root/reports/auftraege-rechnungen-2026-10-06 /tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad ``` Beide Ordner müssen existieren (der Scratchpad ist der aus der Übersicht für die Sperrdatei `auftraege-schwer.lock`). --- ### Task 2: Wortlaute in `texte.js` und Nachweis der Normen **Files:** - Create: `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt` - Create: `test/auftraegeRechtstexte.test.js` - Modify (Inhalt ersetzen): `server/services/auftraege/texte.js` **Interfaces:** `texte.js` exportiert genau `TEXTE_FASSUNG`, `ABSENDER`, `AB_TEXTE`, `BESTAETIGUNG_TEXT`, `FRUEHBEGINN_FASSUNG`, `FRUEHBEGINN_TEXT`, `WIDERRUFSBELEHRUNG_PROJEKT`, `WIDERRUFSFORMULAR` in den Formen aus V7. Verhalten der Funktionen (die Eingaben liefern `auftragSicht.js` bzw. `pdf.js` in `euroDe`-Form): | Funktion | Eingabe | Ausgabe | |---|---|---| | `AB_TEXTE.einleitung({ datum, weg })` | `datum` `'TT.MM.JJJJ'`; `weg` Klartext (`'telefonisch'`) oder Schlüssel aus `AUFTRAG_WEGE` (`'telefon'`), leer erlaubt | Satz mit Datum und, wenn vorhanden, `(Weg)`; ohne `datum` TypeError | | `AB_TEXTE.aufwand({ satz, schaetzung })` | `satz` Euro-Text (`'100,00 €'`); `schaetzung` Stundentext (`'10'`, `'7,5'`) oder `null` | Absatz zur Aufwandsregel; Schätzung und Zusage „melden wir uns vorher“ nur mit `schaetzung`; ohne `satz` TypeError | | `AB_TEXTE.zahlungsplan.nach_abnahme/anzahlung/monatlich/frei` | `ziel` Zahl der Tage (0 = sofort; fehlend oder ungültig = 14); `anzahlung`: `prozent` Zahl, `anzahlung` und `rest` Euro-Text; `frei`: `text` | ein Satz; fehlende Pflichtwerte TypeError | | `AB_TEXTE.termine({ start, fertig, privat })` | `start` und `fertig` `'TT.MM.JJJJ'` oder leer; `privat` optional (Boolean, Vorgabe `false`) | Satz oder `null`; mit `privat` kommt der Satz zum Beginn nach Ablauf der Widerrufsfrist dazu (dann nie `null`) | | `AB_TEXTE.agb({ fassung })`, `AB_TEXTE.ersetzt({ nummer, datum })` | `fassung` und `datum` `'TT.MM.JJJJ'` | je ein Satz; Pflichtwerte TypeError | - [ ] **Step 1: Nachweis der Normen anlegen.** Neue Datei `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt` mit genau diesem Inhalt (Auszüge von gesetze-im-internet.de, abgerufen am 06.10.2026; der Kopf hält das Ergebnis der zweiten Prüfung von § 649 und § 650 BGB mit Zitat fest; die Tests in Step 2 lesen die Datei): ```text Normen, auf die sich die Texte der Auftragsbestätigung, der Kundenseiten und der Mails stützen (Auszüge, unverändert, abgerufen 2026-10-06 von gesetze-im-internet.de). Die Datei dient als Nachweis der Zählung; test/auftraegeRechtstexte.test.js gleicht die Zitate in den Texten mit ihr ab. Prüfung vom 06.10.2026 (zweiter Abruf von § 649 und § 650 BGB): Der Kostenanschlag steht in § 649 BGB (amtliche Überschrift „Kostenanschlag“). Absatz 2 lautet: „Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.“ § 650 BGB regelt dagegen den Werklieferungsvertrag und Verbraucherverträge über die Herstellung digitaler Produkte; ein Verweis auf „§ 650“ für den Kostenanschlag ist falsch. Der Satz „Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.“ (AB_TEXTE.aufwand) stützt sich auf § 649 Absatz 2. Quelle: https://www.gesetze-im-internet.de/bgb/__312f.html (Auszug: § 312f Absatz 1 und 2 BGB) (1) Bei außerhalb von Geschäftsräumen geschlossenen Verträgen ist der Unternehmer verpflichtet, dem Verbraucher alsbald auf Papier zur Verfügung zu stellen 1. eine Abschrift eines Vertragsdokuments, das von den Vertragsschließenden so unterzeichnet wurde, dass ihre Identität erkennbar ist, oder 2. eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist. Wenn der Verbraucher zustimmt, kann für die Abschrift oder die Bestätigung des Vertrags auch ein anderer dauerhafter Datenträger verwendet werden. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben nur enthalten, wenn der Unternehmer dem Verbraucher diese Informationen nicht bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt hat. (2) Bei Fernabsatzverträgen ist der Unternehmer verpflichtet, dem Verbraucher eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist, innerhalb einer angemessenen Frist nach Vertragsschluss, spätestens jedoch bei der Lieferung der Ware oder bevor mit der Ausführung der Dienstleistung begonnen wird, auf einem dauerhaften Datenträger zur Verfügung zu stellen. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben enthalten, es sei denn, der Unternehmer hat dem Verbraucher diese Informationen bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt. Quelle: https://www.gesetze-im-internet.de/bgb/__355.html (Auszug: § 355 Absatz 1 BGB) (1) Wird einem Verbraucher durch Gesetz ein Widerrufsrecht nach dieser Vorschrift eingeräumt, so sind der Verbraucher und der Unternehmer an ihre auf den Abschluss des Vertrags gerichteten Willenserklärungen nicht mehr gebunden, wenn der Verbraucher seine Willenserklärung fristgerecht widerrufen hat. Der Widerruf erfolgt durch Erklärung gegenüber dem Unternehmer. Aus der Erklärung muss der Entschluss des Verbrauchers zum Widerruf des Vertrags eindeutig hervorgehen. Der Widerruf muss keine Begründung enthalten. Zur Fristwahrung genügt die rechtzeitige Absendung des Widerrufs. Quelle: https://www.gesetze-im-internet.de/bgb/__356.html (Auszug: § 356 Absatz 4 bis 6 BGB) (4) Das Widerrufsrecht erlischt spätestens zwölf Monate und 14 Tage nach dem in Absatz 2 oder § 355 Absatz 2 Satz 2 genannten Zeitpunkt. Bei Verträgen über Finanzdienstleistungen gilt Satz 1 nicht, wenn der Verbraucher nicht nach Artikel 246b § 1 Absatz 1 Nummer 16 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche über sein Widerrufsrecht belehrt wurde. (5) Das Widerrufsrecht erlischt bei Verträgen über die Erbringung von Dienstleistungen auch unter folgenden Voraussetzungen: 1. bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer die Dienstleistung vollständig erbracht hat, 2. bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher vor Beginn der Erbringung a) ausdrücklich zugestimmt hat, dass der Unternehmer mit der Erbringung der Dienstleistung vor Ablauf der Widerrufsfrist beginnt, b) bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag die Zustimmung nach Buchstabe a auf einem dauerhaften Datenträger übermittelt hat und c) seine Kenntnis davon bestätigt hat, dass sein Widerrufsrecht mit vollständiger Vertragserfüllung durch den Unternehmer erlischt, 3. bei einem Vertrag, bei dem der Verbraucher den Unternehmer ausdrücklich aufgefordert hat, ihn aufzusuchen, um Reparaturarbeiten auszuführen, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher die in Nummer 2 Buchstabe a und b genannten Voraussetzungen erfüllt hat, 4. bei einem Vertrag über die Erbringung von Finanzdienstleistungen, wenn der Vertrag von beiden Seiten auf ausdrücklichen Wunsch des Verbrauchers vollständig erfüllt ist, bevor der Verbraucher sein Widerrufsrecht ausübt. (6) Das Widerrufsrecht erlischt bei Verträgen über die Bereitstellung von nicht auf einem körperlichen Datenträger befindlichen digitalen Inhalten auch unter folgenden Voraussetzungen: 1. bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer mit der Vertragserfüllung begonnen hat, 2. bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, wenn a) der Unternehmer mit der Vertragserfüllung begonnen hat, b) der Verbraucher ausdrücklich zugestimmt hat, dass der Unternehmer mit der Vertragserfüllung vor Ablauf der Widerrufsfrist beginnt, c) der Verbraucher seine Kenntnis davon bestätigt hat, dass durch seine Zustimmung nach Buchstabe b mit Beginn der Vertragserfüllung sein Widerrufsrecht erlischt, und d) der Unternehmer dem Verbraucher eine Bestätigung gemäß § 312f zur Verfügung gestellt hat. Quelle: https://www.gesetze-im-internet.de/bgb/__356a.html (Auszug: § 356a Absatz 2 und 4 BGB) (2) Die Widerrufsfunktion muss dem Verbraucher ermöglichen, eine Widerrufserklärung an den Unternehmer zu übermitteln und dem Unternehmer in oder mit der Widerrufserklärung ohne Weiteres folgende Informationen bereitzustellen oder zu bestätigen: 1. den Namen des Verbrauchers, 2. Angaben zur Identifizierung des Vertrags oder des Teils des Vertrags, den der Verbraucher widerrufen möchte, 3. Angaben zum elektronischen Kommunikationsmittel, mit welchem dem Verbraucher eine Eingangsbestätigung für den Widerruf zu übermitteln ist. (4) Der Unternehmer hat dem Verbraucher, wenn dieser die Bestätigungsfunktion aktiviert hat, auf einem dauerhaften Datenträger unverzüglich eine Eingangsbestätigung zu übermitteln, die zumindest den Inhalt der Widerrufserklärung nach Absatz 2 sowie das Datum und die Uhrzeit ihres Eingangs enthält. Quelle: https://www.gesetze-im-internet.de/bgb/__357a.html (Auszug: § 357a Absatz 2 BGB) (2) Der Verbraucher hat Wertersatz für die bis zum Widerruf erbrachten Dienstleistungen, für die der Vertrag die Zahlung eines Preises vorsieht, oder die bis zum Widerruf erfolgte Lieferung von Wasser, Gas oder Strom in nicht bestimmten Mengen oder nicht begrenztem Volumen oder von Fernwärme zu leisten, wenn 1. der Verbraucher von dem Unternehmer ausdrücklich verlangt hat, dass mit der Leistung vor Ablauf der Widerrufsfrist begonnen werden soll, 2. bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag der Verbraucher das Verlangen nach Nummer 1 auf einem dauerhaften Datenträger übermittelt hat und 3. der Unternehmer den Verbraucher nach Artikel 246a § 1 Absatz 2 Satz 1 Nummer 1 und 3 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche ordnungsgemäß informiert hat. Bei der Berechnung des Wertersatzes ist der vereinbarte Gesamtpreis zu Grunde zu legen. Ist der vereinbarte Gesamtpreis unverhältnismäßig hoch, so ist der Wertersatz auf der Grundlage des Marktwerts der erbrachten Leistung zu berechnen. Quelle: https://www.gesetze-im-internet.de/bgbeg/art_246a__1.html (Auszug: Artikel 246a § 1 Absatz 3 EGBGB) (3) Der Unternehmer hat den Verbraucher auch zu informieren, wenn 1. dem Verbraucher nach § 312g Absatz 2 Nummer 1, 2, 5 und 7 bis 13 des Bürgerlichen Gesetzbuchs ein Widerrufsrecht nicht zusteht, dass der Verbraucher seine Willenserklärung nicht widerrufen kann, oder 2. das Widerrufsrecht des Verbrauchers nach § 312g Absatz 2 Nummer 3, 4 und 6 sowie § 356 Absatz 5 und 6 des Bürgerlichen Gesetzbuchs vorzeitig erlöschen kann, über die Umstände, unter denen der Verbraucher ein zunächst bestehendes Widerrufsrecht verliert. Quelle: https://www.gesetze-im-internet.de/bgb/__640.html (Auszug: § 640 Absatz 2 BGB) (2) Als abgenommen gilt ein Werk auch, wenn der Unternehmer dem Besteller nach Fertigstellung des Werks eine angemessene Frist zur Abnahme gesetzt hat und der Besteller die Abnahme nicht innerhalb dieser Frist unter Angabe mindestens eines Mangels verweigert hat. Ist der Besteller ein Verbraucher, so treten die Rechtsfolgen des Satzes 1 nur dann ein, wenn der Unternehmer den Besteller zusammen mit der Aufforderung zur Abnahme auf die Folgen einer nicht erklärten oder ohne Angabe von Mängeln verweigerten Abnahme hingewiesen hat; der Hinweis muss in Textform erfolgen. Quelle: https://www.gesetze-im-internet.de/bgb/__649.html (§ 649 BGB, erneut abgerufen 2026-10-06; die amtliche Überschrift lautet „Kostenanschlag“) § 649 Kostenanschlag (1) Ist dem Vertrag ein Kostenanschlag zugrunde gelegt worden, ohne dass der Unternehmer die Gewähr für die Richtigkeit des Anschlags übernommen hat, und ergibt sich, dass das Werk nicht ohne eine wesentliche Überschreitung des Anschlags ausführbar ist, so steht dem Unternehmer, wenn der Besteller den Vertrag aus diesem Grund kündigt, nur der im § 645 Abs. 1 bestimmte Anspruch zu. (2) Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen. Quelle: https://www.gesetze-im-internet.de/bgb/__650.html (Auszug: § 650 Absatz 2 BGB, zur Abgrenzung: § 650 ist nicht der Kostenanschlag; er regelt den Werklieferungsvertrag und Verbraucherverträge über digitale Produkte) § 650 Werklieferungsvertrag; Verbrauchervertrag über die Herstellung digitaler Produkte (2) Auf einen Verbrauchervertrag, bei dem der Unternehmer sich verpflichtet, 1. digitale Inhalte herzustellen, 2. einen Erfolg durch eine digitale Dienstleistung herbeizuführen oder 3. einen körperlichen Datenträger herzustellen, der ausschließlich als Träger digitaler Inhalte dient, sind die §§ 633 bis 639 über die Rechte bei Mängeln sowie § 640 über die Abnahme nicht anzuwenden. An die Stelle der nach Satz 1 nicht anzuwendenden Vorschriften treten die Vorschriften des Abschnitts 3 Titel 2a. Die §§ 641, 644 und 645 sind mit der Maßgabe anzuwenden, dass an die Stelle der Abnahme die Bereitstellung des digitalen Produkts (§ 327b Absatz 3 bis 5) tritt. Quelle: https://www.gesetze-im-internet.de/ao_1977/__147.html (Auszug: § 147 Absatz 1 Nummer 2 bis 4, Absatz 3 Satz 1 und Absatz 4 AO) 2. die empfangenen Handels- oder Geschäftsbriefe, 3. Wiedergaben der abgesandten Handels- oder Geschäftsbriefe, 4. Buchungsbelege, (3) Die in Absatz 1 Nummer 1 und 4a aufgeführten Unterlagen sind zehn Jahre, die in Absatz 1 Nummer 4 aufgeführten Unterlagen acht Jahre und die sonstigen in Absatz 1 aufgeführten Unterlagen sechs Jahre aufzubewahren, sofern nicht in anderen Steuergesetzen kürzere Aufbewahrungsfristen zugelassen sind. (4) Die Aufbewahrungsfrist beginnt mit dem Schluss des Kalenderjahrs, in dem die letzte Eintragung in das Buch gemacht, das Inventar, die Eröffnungsbilanz, der Jahresabschluss oder der Lagebericht aufgestellt, der Handels- oder Geschäftsbrief empfangen oder abgesandt worden oder der Buchungsbeleg entstanden ist, ferner die Aufzeichnung vorgenommen worden ist oder die sonstigen Unterlagen entstanden sind. ``` - [ ] **Step 2: Test zuerst.** Neue Datei `test/auftraegeRechtstexte.test.js` mit genau diesem Inhalt (Teil 1; die Tasks 3 bis 6 hängen weitere Blöcke an das Ende an): ```js import { test } from 'node:test' import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { spawnSync } from 'node:child_process' // Rechtstexte der Aufträge und Rechnungen (Strang L7; Spec Abschnitt 8 „Rechtsrunde“, Übersicht V7). // Geprüft werden die Wortlaute in server/services/auftraege/texte.js gegen die gesetzlichen Muster (docs/rechtliches/quellen/), die Seiten // und PDFs unter public/rechtliches/ (Satz „auftraege“ von scripts/build-legal-html.mjs und build-legal-pdfs.sh), die AGB-Kopie gegen die // Website und die Abschnitte der Datenschutzerklärung. Es wird nur gelesen, geschrieben wird nur in temporären Verzeichnissen. const ROOT = fileURLToPath(new URL('..', import.meta.url)) const lies = (rel) => readFileSync(join(ROOT, rel), 'utf8') const sha256 = (daten) => createHash('sha256').update(daten).digest('hex') const norm = (text) => String(text).replace(/\s+/g, ' ').trim() const { config } = await import('../server/config.js') const texte = await import('../server/services/auftraege/texte.js') const { AGB_FASSUNG, AUFTRAG_WEGE } = await import('../server/services/auftraege/konstanten.js') const { widerrufsbelehrungFunktion } = await import('../server/services/termine/rechtstexte.js') const gen = await import('../scripts/build-legal-html.mjs') const T = texte.AB_TEXTE // ---------------------------------------------------------------------------------------------------------- Exporte und Formen (V7) const V7_EXPORTE = ['ABSENDER', 'AB_TEXTE', 'BESTAETIGUNG_TEXT', 'FRUEHBEGINN_FASSUNG', 'FRUEHBEGINN_TEXT', 'TEXTE_FASSUNG', 'WIDERRUFSBELEHRUNG_PROJEKT', 'WIDERRUFSFORMULAR'] test('texte_exporte: genau die Exporte und Formen aus V7 der Übersicht, Fassungen im Format JJJJ-MM-TT', () => { assert.deepEqual(Object.keys(texte).sort(), [...V7_EXPORTE].sort()) for (const fassung of [texte.TEXTE_FASSUNG, texte.FRUEHBEGINN_FASSUNG]) { assert.match(fassung, /^\d{4}-\d{2}-\d{2}$/) assert.ok(fassung >= '2026-10-06', `Fassung ${fassung} liegt vor der Rechtsrunde`) } assert.deepEqual(texte.ABSENDER, { name: 'WEBklar GbR', strasse: 'Am Schwimmbad 10', plzOrt: '67722 Winnweiler', telefon: '0176 23726355', email: 'support@webklar.com', web: 'webklar.com', vertretenDurch: 'Kenso Grimm und Justin Klein', }) assert.deepEqual(Object.keys(T).sort(), ['agb', 'aufwand', 'einleitung', 'ersetzt', 'gruss', 'kleinunternehmer', 'mitwirkung', 'termine', 'titel', 'zahlungsplan']) assert.deepEqual(Object.keys(T.zahlungsplan).sort(), ['anzahlung', 'frei', 'monatlich', 'nach_abnahme']) for (const name of ['einleitung', 'aufwand', 'termine', 'agb', 'ersetzt']) assert.equal(typeof T[name], 'function', name) for (const name of ['anzahlung', 'frei', 'monatlich', 'nach_abnahme']) assert.equal(typeof T.zahlungsplan[name], 'function', name) for (const name of ['titel', 'kleinunternehmer', 'mitwirkung', 'gruss']) assert.equal(typeof T[name], 'string', name) assert.equal(typeof texte.BESTAETIGUNG_TEXT, 'string') assert.equal(typeof texte.FRUEHBEGINN_TEXT, 'string') assert.deepEqual(Object.keys(texte.WIDERRUFSBELEHRUNG_PROJEKT), ['titel', 'abschnitte']) for (const abschnitt of texte.WIDERRUFSBELEHRUNG_PROJEKT.abschnitte) { assert.deepEqual(Object.keys(abschnitt), ['ueberschrift', 'absaetze']) assert.ok(abschnitt.absaetze.length > 0 && abschnitt.absaetze.every((s) => typeof s === 'string' && s !== '')) } assert.deepEqual(Object.keys(texte.WIDERRUFSFORMULAR), ['titel', 'einleitung', 'zeilen']) }) // ---------------------------------------------------------------------------------------------------------- Textbausteine der AB test('ab_texte_einleitung: Datum und Weg der Absprache; Schlüssel aus AUFTRAG_WEGE wird zum Klartext; Pflichtwert fehlt = Fehler', () => { assert.equal(T.titel, 'Auftragsbestätigung') assert.equal( T.einleitung({ datum: '05.10.2026', weg: 'telefonisch' }), 'Vielen Dank für Ihren Auftrag vom 05.10.2026 (telefonisch). Wir bestätigen ihn wie folgt:', ) for (const [schluessel, klartext] of Object.entries(AUFTRAG_WEGE)) { const erwartet = `Vielen Dank für Ihren Auftrag vom 05.10.2026 (${klartext}). Wir bestätigen ihn wie folgt:` assert.equal(T.einleitung({ datum: '05.10.2026', weg: schluessel }), erwartet, schluessel) assert.equal(T.einleitung({ datum: '05.10.2026', weg: klartext }), erwartet, klartext) } assert.equal(T.einleitung({ datum: '05.10.2026', weg: '' }), 'Vielen Dank für Ihren Auftrag vom 05.10.2026. Wir bestätigen ihn wie folgt:') assert.equal(T.einleitung({ datum: '05.10.2026', weg: 'constructor' }), 'Vielen Dank für Ihren Auftrag vom 05.10.2026 (constructor). Wir bestätigen ihn wie folgt:', 'kein Treffer in der Prototypkette') assert.throws(() => T.einleitung({ weg: 'telefon' }), TypeError) assert.throws(() => T.einleitung(), TypeError) }) test('ab_texte_aufwand: Stundensatz, 15-Minuten-Schritte, Unterstützung (AGB Nr. 9), unverbindliche Schätzung und Zusage nach § 649 BGB (Kostenanschlag) nur mit Schätzung', () => { const regel = 'Wir rechnen nach Aufwand ab: 100,00 € pro Stunde, erfasst je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. ' + 'Wirkt eine weitere Person unterstützend mit, berechnen wir für sie die Hälfte der Arbeitszeit (AGB Nr. 9).' const schaetzung = (stunden, wort) => `${regel} Der geschätzte Umfang beträgt etwa ${stunden} ${wort}; die Schätzung ist unverbindlich. Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.` assert.equal(T.aufwand({ satz: '100,00 €', schaetzung: '10' }), schaetzung('10', 'Stunden')) assert.equal(T.aufwand({ satz: '100,00 €', schaetzung: '1' }), schaetzung('1', 'Stunde')) assert.equal(T.aufwand({ satz: '100,00 €', schaetzung: '7,5' }), schaetzung('7,5', 'Stunden')) // ohne Schätzung gibt es nichts zu überschreiten: weder Schätzung noch Zusage for (const leer of [null, undefined, '']) assert.equal(T.aufwand({ satz: '100,00 €', schaetzung: leer }), regel) assert.throws(() => T.aufwand({ schaetzung: '10' }), TypeError) }) test('ab_texte_kleinunternehmer_mitwirkung_agb_ersetzt_gruss: Wortlaut', () => { assert.equal(T.kleinunternehmer, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.') assert.equal( T.mitwirkung, 'Mitwirkung und Abnahme richten sich nach Nr. 7 und Nr. 8 unserer Allgemeinen Geschäftsbedingungen. ' + 'Bitte stellen Sie uns die benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.', ) assert.equal( T.agb({ fassung: '30.09.2026' }), 'Es gelten unsere Allgemeinen Geschäftsbedingungen in der Fassung vom 30.09.2026, die dieser Bestätigung beiliegen.', ) assert.equal(T.ersetzt({ nummer: 'AB-2026-0001', datum: '05.10.2026' }), 'Diese Auftragsbestätigung ersetzt AB-2026-0001 vom 05.10.2026.') assert.equal(T.gruss, 'Mit freundlichen Grüßen') assert.throws(() => T.agb({}), TypeError) assert.throws(() => T.ersetzt({ nummer: 'AB-2026-0001' }), TypeError) }) test('ab_texte_zahlungsplan: vier Pläne, Zahlungsziel als „innerhalb von n Tagen nach Rechnungsdatum“', () => { const Z = T.zahlungsplan const ziel14 = 'innerhalb von 14 Tagen nach Rechnungsdatum' assert.equal(Z.nach_abnahme({ ziel: 14 }), `Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar ${ziel14}.`) assert.equal( Z.anzahlung({ prozent: 50, anzahlung: '400,00 €', rest: '400,00 €', ziel: 14 }), `Anzahlung 50 % (400,00 €) nach Auftragsbestätigung, Rest (400,00 €) nach Abnahme; zahlbar jeweils ${ziel14}.`, ) assert.equal(Z.anzahlung({ prozent: 33.5, anzahlung: '335,00 €', rest: '665,00 €', ziel: 7 }), 'Anzahlung 33,5 % (335,00 €) nach Auftragsbestätigung, Rest (665,00 €) nach Abnahme; zahlbar jeweils innerhalb von 7 Tagen nach Rechnungsdatum.') assert.equal(Z.monatlich({ ziel: 14 }), `Wir stellen den angefallenen Aufwand monatlich in Rechnung; zahlbar ${ziel14}.`) assert.equal( Z.frei({ text: 'Je ein Drittel bei Auftrag, zur Halbzeit und nach Abnahme', ziel: 7 }), 'Je ein Drittel bei Auftrag, zur Halbzeit und nach Abnahme. Zahlbar jeweils innerhalb von 7 Tagen nach Rechnungsdatum.', ) assert.equal(Z.frei({ text: 'Zahlung in zwei Raten.', ziel: 14 }), `Zahlung in zwei Raten. Zahlbar jeweils ${ziel14}.`, 'kein doppelter Punkt') // Zahlungsziel: 0 = sofort, 1 = Einzahl, Strings und Zahlen, ungültig oder fehlend = 14 Tage assert.equal(Z.nach_abnahme({ ziel: 0 }), 'Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar sofort nach Rechnungsstellung.') assert.equal(Z.nach_abnahme({ ziel: 1 }), 'Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar innerhalb von 1 Tag nach Rechnungsdatum.') assert.equal(Z.nach_abnahme({ ziel: '30' }), 'Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar innerhalb von 30 Tagen nach Rechnungsdatum.') for (const ungueltig of [undefined, null, '', 'bald', 7.5, -3, NaN]) { assert.equal(Z.nach_abnahme({ ziel: ungueltig }), `Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar ${ziel14}.`, String(ungueltig)) } assert.equal(Z.nach_abnahme(), `Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar ${ziel14}.`) // Pflichtwerte assert.throws(() => Z.anzahlung({ prozent: 50, anzahlung: '400,00 €', ziel: 14 }), TypeError) assert.throws(() => Z.frei({ text: ' ', ziel: 14 }), TypeError) }) test('ab_texte_termine: Beginn und voraussichtliche Fertigstellung; für Privatpersonen der Satz zum Beginn nach Ablauf der Widerrufsfrist', () => { const privat = 'Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.' assert.equal(T.termine({ start: '08.10.2026', fertig: '31.10.2026' }), 'Beginn: 08.10.2026; voraussichtliche Fertigstellung: 31.10.2026.') assert.equal(T.termine({ start: '08.10.2026', fertig: null }), 'Beginn: 08.10.2026.') assert.equal(T.termine({ start: null, fertig: '31.10.2026' }), 'Voraussichtliche Fertigstellung: 31.10.2026.') assert.equal(T.termine({ start: null, fertig: null }), null) assert.equal(T.termine({}), null) assert.equal(T.termine({ start: '08.10.2026', fertig: '31.10.2026', privat: false }), 'Beginn: 08.10.2026; voraussichtliche Fertigstellung: 31.10.2026.') assert.equal(T.termine({ start: '08.10.2026', fertig: '31.10.2026', privat: true }), `Beginn: 08.10.2026; voraussichtliche Fertigstellung: 31.10.2026. ${privat}`) assert.equal(T.termine({ start: null, fertig: null, privat: true }), privat, 'der Satz steht auch ohne Termine') }) test('ab_texte_stil: Sie-Form, keine Gedankenstriche im Fließtext, keine veraltete Zählung, kein „undefined“', () => { const alle = [ T.titel, T.einleitung({ datum: '05.10.2026', weg: 'telefon' }), T.aufwand({ satz: '100,00 €', schaetzung: '10' }), T.aufwand({ satz: '100,00 €' }), T.kleinunternehmer, T.zahlungsplan.nach_abnahme({ ziel: 14 }), T.zahlungsplan.anzahlung({ prozent: 50, anzahlung: '400,00 €', rest: '400,00 €', ziel: 14 }), T.zahlungsplan.monatlich({ ziel: 14 }), T.zahlungsplan.frei({ text: 'Je ein Drittel', ziel: 7 }), T.termine({ start: '08.10.2026', fertig: '31.10.2026', privat: true }), T.mitwirkung, T.agb({ fassung: '30.09.2026' }), T.ersetzt({ nummer: 'AB-2026-0001', datum: '05.10.2026' }), T.gruss, texte.BESTAETIGUNG_TEXT, texte.FRUEHBEGINN_TEXT, ] for (const text of alle) { assert.doesNotMatch(text, /[–—]/, `Gedankenstrich: ${text}`) assert.doesNotMatch(text, /\bdein|\bdu\b|\bdir\b|\bdich\b/i, `Du-Form: ${text}`) assert.doesNotMatch(text, /undefined|null|NaN|\{\{|\}\}/, `Platzhalter: ${text}`) assert.doesNotMatch(text, /Abs\. 4/, `veraltete Zählung: ${text}`) } }) // ---------------------------------------------------------------------------------------------------------- Bestätigung und Frühbeginn test('bestaetigung_text: Häkchen im Wortlaut der Spezifikation (Alternative mit Bezug auf die AGB ist Entscheidung E2 der Rechtsrunde)', () => { assert.equal(texte.BESTAETIGUNG_TEXT, 'Ich bestätige den Auftrag zu diesen Bedingungen.') }) const NORMEN = lies('docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt') test('normen_zaehlung: § 356 Abs. 5 Nr. 2 BGB regelt das Erlöschen bei entgeltlichen Dienstleistungen (Abs. 4 ist die Höchstfrist), § 357a Abs. 2 BGB den Wertersatz', () => { const n = norm(NORMEN) // Abs. 4 = Höchstfrist, kein Erlöschen durch Leistung assert.ok(n.includes('(4) Das Widerrufsrecht erlischt spätestens zwölf Monate und 14 Tage nach dem in Absatz 2 oder § 355 Absatz 2 Satz 2 genannten Zeitpunkt.')) // Abs. 5 Nr. 2 = Vertrag mit Preis: Zustimmung (a), bei Verträgen außerhalb von Geschäftsräumen Datenträger (b), Kenntnis (c) const abs5 = n.slice(n.indexOf('(5) Das Widerrufsrecht erlischt bei Verträgen über die Erbringung von Dienstleistungen'), n.indexOf('(6) Das Widerrufsrecht erlischt bei Verträgen über die Bereitstellung')) assert.ok(abs5.length > 500, 'Absatz 5 fehlt in der Quelle') const nr2 = abs5.slice(abs5.indexOf('2. bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet'), abs5.indexOf('3. bei einem Vertrag, bei dem der Verbraucher den Unternehmer ausdrücklich aufgefordert hat')) for (const teil of [ 'mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher vor Beginn der Erbringung', 'a) ausdrücklich zugestimmt hat, dass der Unternehmer mit der Erbringung der Dienstleistung vor Ablauf der Widerrufsfrist beginnt,', 'b) bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag die Zustimmung nach Buchstabe a auf einem dauerhaften Datenträger übermittelt hat und', 'c) seine Kenntnis davon bestätigt hat, dass sein Widerrufsrecht mit vollständiger Vertragserfüllung durch den Unternehmer erlischt,', ]) assert.ok(nr2.includes(teil), `§ 356 Abs. 5 Nr. 2: ${teil}`) // Nr. 1 betrifft unentgeltliche Verträge, Abs. 6 die Bereitstellung digitaler Inhalte: beides passt nicht zum Projektauftrag assert.ok(abs5.includes('1. bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet')) // § 357a Abs. 2: Wertersatz für Dienstleistungen mit Preis, ausdrückliches Verlangen als Voraussetzung assert.ok(n.includes('(2) Der Verbraucher hat Wertersatz für die bis zum Widerruf erbrachten Dienstleistungen, für die der Vertrag die Zahlung eines Preises vorsieht')) assert.ok(n.includes('1. der Verbraucher von dem Unternehmer ausdrücklich verlangt hat, dass mit der Leistung vor Ablauf der Widerrufsfrist begonnen werden soll,')) assert.ok(n.includes('Bei der Berechnung des Wertersatzes ist der vereinbarte Gesamtpreis zu Grunde zu legen.')) }) test('normen_kostenanschlag: § 649 BGB ist der Kostenanschlag (Abs. 2: unverzügliche Anzeige), § 650 BGB regelt etwas anderes', () => { const n = norm(NORMEN) const block = (nr) => { const a = n.indexOf(`Quelle: https://www.gesetze-im-internet.de/bgb/__${nr}.html`) assert.ok(a >= 0, `§ ${nr} fehlt im Nachweis`) const b = n.indexOf('Quelle: ', a + 10) return n.slice(a, b < 0 ? undefined : b) } const kostenanschlag = block(649) assert.ok(kostenanschlag.includes('§ 649 Kostenanschlag'), 'amtliche Überschrift von § 649') assert.ok(kostenanschlag.includes('(1) Ist dem Vertrag ein Kostenanschlag zugrunde gelegt worden')) assert.ok(kostenanschlag.includes('(2) Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.')) // § 650 trägt eine andere Überschrift; der Kostenanschlag kommt in seinem Gesetzestext nicht vor const werklieferung = block(650) assert.ok(werklieferung.includes('§ 650 Werklieferungsvertrag; Verbrauchervertrag über die Herstellung digitaler Produkte')) assert.ok(!werklieferung.slice(werklieferung.indexOf('§ 650 Werklieferungsvertrag')).includes('Kostenanschlag')) // das Ergebnis der Prüfung vom 06.10.2026 steht mit dem Zitat im Kopf der Datei assert.ok(n.includes('Der Kostenanschlag steht in § 649 BGB (amtliche Überschrift „Kostenanschlag“).')) assert.ok(n.includes('ein Verweis auf „§ 650“ für den Kostenanschlag ist falsch')) assert.doesNotMatch(lies('server/services/auftraege/texte.js'), /§ 650/, 'texte.js nennt § 650 nicht') }) test('normen_kundenseiten: Widerruf formfrei (§ 355 Abs. 1), Widerrufsfunktion und Eingangsbestätigung (§ 356a Abs. 2 und 4), digitale Produkte (§ 650 Abs. 2)', () => { const n = norm(NORMEN) assert.ok(n.includes('Der Widerruf erfolgt durch Erklärung gegenüber dem Unternehmer. Aus der Erklärung muss der Entschluss des Verbrauchers zum Widerruf des Vertrags eindeutig hervorgehen.')) assert.ok(n.includes('2. Angaben zur Identifizierung des Vertrags oder des Teils des Vertrags, den der Verbraucher widerrufen möchte,')) assert.ok(n.includes('(4) Der Unternehmer hat dem Verbraucher, wenn dieser die Bestätigungsfunktion aktiviert hat, auf einem dauerhaften Datenträger unverzüglich eine Eingangsbestätigung zu übermitteln, die zumindest den Inhalt der Widerrufserklärung nach Absatz 2 sowie das Datum und die Uhrzeit ihres Eingangs enthält.')) assert.ok(n.includes('(2) Auf einen Verbrauchervertrag, bei dem der Unternehmer sich verpflichtet, 1. digitale Inhalte herzustellen,')) assert.ok(n.includes('sind die §§ 633 bis 639 über die Rechte bei Mängeln sowie § 640 über die Abnahme nicht anzuwenden.')) }) // Zeichen, die Figtree Regular und SemiBold haben (die Schrift des PDFs, server/assets/fonts/ von Strang L4): Basic Latin, Umlaute, ß, § und die // typografischen Zeichen der Texte. Geprüft am 06.10.2026 mit fontkit (hasGlyphForCodePoint, @expo-google-fonts/figtree 0.4.1, beide Schnitte). const FIGTREE_ZEICHEN = new Set([ ...Array.from({ length: 95 }, (_, i) => String.fromCodePoint(0x20 + i)), ...'äöüÄÖÜß§€–—„“‚‘’”…−•→', String.fromCodePoint(0xa0), '\n', ]) test('zeichenvorrat_figtree: texte.js enthält nur Zeichen der PDF-Schrift Figtree (sonst ersetzt das PDF still ein Zeichen und weicht vom freigegebenen Wortlaut ab)', () => { const fremd = [...new Set(lies('server/services/auftraege/texte.js'))].filter((zeichen) => !FIGTREE_ZEICHEN.has(zeichen)) const bezeichnet = fremd.map((zeichen) => `U+${zeichen.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')} ${zeichen}`) assert.deepEqual(bezeichnet, [], 'Zeichen außerhalb des geprüften Zeichenvorrats (auch in Kommentaren): vor der Aufnahme gegen Figtree prüfen und FIGTREE_ZEICHEN ergänzen; der Wächter-Test von L4 (auftraegePdfDaten) prüft nach dem Merge gegen die Schrift selbst') }) test('fruehbeginn_text: Verlangen, Kenntnis vom Erlöschen und Wertersatz in einem Satzgefüge; zitiert § 356 Abs. 5 Nr. 2 und § 357a Abs. 2 BGB', () => { assert.equal(texte.FRUEHBEGINN_TEXT, 'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Ausführung dieses Projektauftrags beginnt. ' + 'Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung 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).') const saetze = texte.FRUEHBEGINN_TEXT.split(/(?<=\.) (?=Mir ist bekannt|Widerrufe ich)/) assert.equal(saetze.length, 3, 'drei Sätze: Verlangen, Kenntnis, Wertersatz') // Verlangen (§ 357a Abs. 2 Nr. 1) und Zustimmung (§ 356 Abs. 5 Nr. 2 Buchstabe a): ausdrücklich, vor Ablauf der Widerrufsfrist, für diesen Auftrag assert.ok(saetze[0].startsWith('Ich verlange ausdrücklich') && saetze[0].includes('vor Ablauf der Widerrufsfrist') && saetze[0].includes('dieses Projektauftrags')) // Kenntnis (§ 356 Abs. 5 Nr. 2 Buchstabe c): Erlöschen mit vollständiger Erbringung assert.ok(saetze[1].startsWith('Mir ist bekannt') && saetze[1].includes('erlischt') && saetze[1].includes('vollständigen Erbringung')) // Wertersatz (§ 357a Abs. 2): angemessener Betrag für die bis dahin erbrachte Leistung assert.ok(saetze[2].includes('angemessenen Betrag') && saetze[2].includes('bis dahin erbrachte Leistung')) assert.doesNotMatch(texte.FRUEHBEGINN_TEXT, /§ 356 Abs\. 4|§ 356 Abs\. 6|§ 357 Abs\. 8/) // dasselbe Satzende wie bei den Funktionen (News, Terminplaner, Webshop, Öffnungszeiten): gleiche Normen, gleiche Wertersatz-Formel const ende = '(§ 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).' assert.ok(texte.FRUEHBEGINN_TEXT.endsWith(ende)) assert.ok(config.funktionen.consentText.endsWith(ende), 'die Erklärung der Funktionen hat dasselbe Satzende') // beide Zitate stehen in der Quelle der Normen assert.ok(norm(NORMEN).includes('Quelle: https://www.gesetze-im-internet.de/bgb/__356.html')) assert.ok(norm(NORMEN).includes('Quelle: https://www.gesetze-im-internet.de/bgb/__357a.html')) }) // ---------------------------------------------------------------------------------------------------------- Widerrufsbelehrung und Formular const ANLAGE_1 = lies('docs/rechtliches/quellen/anlage-1.txt') const ANLAGE_2 = lies('docs/rechtliches/quellen/anlage-2.txt') const zeilenVon = (text) => text.split('\n').map((z) => z.trim()) const KONTAKT = 'WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon: 0176 23726355, E-Mail: support@webklar.com' const B = texte.WIDERRUFSBELEHRUNG_PROJEKT const F = texte.WIDERRUFSFORMULAR /** Die Belehrung als ein Text: Titel, Überschriften und Absätze in Dokumentreihenfolge. */ const belehrungText = () => [B.titel, ...B.abschnitte.flatMap((a) => [a.ueberschrift, ...a.absaetze])].join('\n') test('belehrung_muster: jeder Absatz der Anlage 1 steht wörtlich darin, nur die Lücken sind gefüllt (Frist „des Vertragsabschlusses.“, Kontaktangaben)', () => { const z = zeilenVon(ANLAGE_1) const von = z.indexOf('Widerrufsbelehrung') const bis = z.findIndex((s) => s.startsWith('Gestaltungshinweise:')) assert.ok(von >= 0 && bis > von, 'Aufbau von anlage-1.txt hat sich geändert') const muster = z.slice(von, bis).filter(Boolean) assert.equal(muster.length, 8, 'Anlage 1: Titel, Widerrufsrecht, 3 Absätze, Folgen des Widerrufs und 1 Absatz') const frist = /Dienstleistungsvertrags[^„]*„([^“]+)“/.exec(ANLAGE_1)?.[1] assert.equal(frist, 'des Vertragsabschlusses.', 'Textbaustein a) für Dienstleistungsverträge') const text = belehrungText() for (const zeile of muster) { const erwartet = zeile.replace('ab dem Tag .', `ab dem Tag ${frist}`).replace('uns ( )', `uns (${KONTAKT})`) assert.ok(text.includes(erwartet), `Absatz fehlt oder ist verändert: ${erwartet}`) } }) test('belehrung_widerrufsfunktion: Satzrahmen der Anlage wörtlich, die Lücke „…“ ist die Adresse der Widerrufsseite', () => { const m = /„(Sie können Ihr Widerrufsrecht auch online unter) … \[[^\]]+\] (ausüben\.) (Wenn Sie diese Online-Funktion nutzen, [^“]+?)\s*“/.exec(ANLAGE_1) assert.ok(m, 'Gestaltungshinweis zur Widerrufsfunktion fehlt oder hat sich geändert in anlage-1.txt') assert.ok(m[3].includes('Eingangsbestätigung') && m[3].includes('Uhrzeit ihres Eingangs')) const absatz = `${m[1]} https://project.webklar.com/widerrufen.html ${m[2]} ${m[3]}` assert.ok(belehrungText().includes(absatz), 'Absatz zur Widerrufsfunktion fehlt oder weicht vom Satzrahmen der Anlage ab') // die Widerrufsseite kennt die Vertragsart „Projektauftrag“ (L5/L6); die Adresse ist dieselbe wie bei den Funktionen assert.equal(belehrungText().split('https://project.webklar.com/widerrufen.html').length - 1, 1) }) test('belehrung_wertersatz: Wertersatz für Dienstleistungen, Wasser/Gas/Strom/Fernwärme gestrichen, keine Bausteine zu Waren', () => { const m = /Haben Sie verlangt, dass die Dienstleistungen oder Lieferung von Wasser\/Gas\/Strom\/Fernwärme \[Unzutreffendes streichen\] (während[^“]+?)\s*“/.exec(ANLAGE_1) assert.ok(m, 'Wertersatz-Absatz fehlt in anlage-1.txt') const text = belehrungText() assert.ok(text.includes(`Haben Sie verlangt, dass die Dienstleistungen ${m[1]}`), 'Wertersatz-Absatz fehlt oder weicht ab') assert.doesNotMatch(text, /Wasser|Gas\b|Strom|Fernwärme|\[Unzutreffendes|Rücksendung|in Besitz genommen|Wertverlust/) }) test('formular_muster: Muster-Widerrufsformular der Anlage 2 wörtlich, Unternehmer in der „An“-Zeile, (*)-Alternativen unverändert', () => { const z = zeilenVon(ANLAGE_2) const start = z.indexOf('Muster-Widerrufsformular') assert.ok(start >= 0, 'Aufbau von anlage-2.txt hat sich geändert') const inhalt = z.slice(start).filter((s) => s && s !== '–') assert.equal(inhalt.length, 10, 'Anlage 2: Titel, Einleitung, 7 Zeilen und der Hinweis (*)') const zeilen = [F.titel, F.einleitung, ...F.zeilen.map((s) => s.replace(/^– /, ''))] let anZeilen = 0 for (const zeile of inhalt) { if (zeile.startsWith('An [hier ist')) { anZeilen += 1 assert.ok(zeilen.includes('An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:'), '„An“-Zeile mit dem Unternehmer fehlt') } else { assert.ok(zeilen.includes(zeile), `Formularzeile fehlt oder ist verändert: ${zeile}`) } } assert.equal(anZeilen, 1) assert.equal(F.titel, 'Muster-Widerrufsformular') // Aufzählungszeichen des Musters vor jeder Formularzeile, der Hinweis (*) steht zuletzt ohne Zeichen assert.ok(F.zeilen.slice(0, -1).every((s) => s.startsWith('– '))) assert.equal(F.zeilen.at(-1), '(*) Unzutreffendes streichen.') }) test('belehrung_gleich_funktionen: Belehrung und Formular sind wörtlich die Belehrung für die Bestellung einer Funktion', () => { const absaetze = [ B.titel, ...B.abschnitte.flatMap((a) => [a.ueberschrift, ...a.absaetze]), F.titel, F.einleitung, F.zeilen.slice(0, -1).join('\n'), F.zeilen.at(-1), ] assert.equal(absaetze.join('\n\n'), widerrufsbelehrungFunktion()) assert.deepEqual(B.abschnitte.map((a) => a.ueberschrift), ['Widerrufsrecht', 'Folgen des Widerrufs']) }) ``` - [ ] **Step 3: Lauf, FAIL erwartet.** Die vorläufigen Wortlaute von L0 weichen ab. ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/auftraegeRechtstexte.test.js 2>&1 | command grep -a -E "^(✔|✖|ℹ (tests|pass|fail))" ``` Erwartet: mindestens `fruehbeginn_text` und die `ab_texte_*`-Tests rot (die vorläufigen Wortlaute von L0 weichen ab), kein Absturz beim Laden der Datei. `normen_zaehlung`, `normen_kostenanschlag` und `normen_kundenseiten` sind schon grün, weil sie nur die Quelle aus Step 1 lesen; `zeichenvorrat_figtree` prüft die vorläufige Datei von L0 und ist grün oder rot, je nachdem, welche Zeichen L0 benutzt. - [ ] **Step 4: Umsetzung.** Den gesamten Inhalt von `server/services/auftraege/texte.js` durch diesen ersetzen (Datei vorher mit `cat` lesen: Es darf nichts verloren gehen, was V7 verlangt; Zusatzexporte von L0 entfallen): ```js /** * Wortlaute der Auftragsbestätigung (AB), der Bestätigung durch den Kunden und der Texte für Privatpersonen (Frühbeginn, * Widerrufsbelehrung, Muster-Widerrufsformular). Reine Konstanten und Funktionen ohne Zugriff auf Appwrite, Stripe oder die * Konfiguration. Endfassung der Rechtsrunde vom 06.10.2026 (Strang L7); die Zusammenstellung zur Freigabe liegt in * /root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md. * * Alle Kundentexte stehen in der Sie-Form und ohne Gedankenstriche im Fließtext. Widerrufsbelehrung und Muster-Widerrufsformular sind * die Muster der Anlagen 1 und 2 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB für einen Dienstleistungsvertrag (Quellen: * docs/rechtliches/quellen/anlage-1.txt und anlage-2.txt) und gleich der Belehrung für die Bestellung einer Funktion * (widerrufsbelehrungFunktion in services/termine/rechtstexte.js); test/auftraegeRechtstexte.test.js prüft beides. * Die Frühbeginn-Erklärung folgt den Erklärungen der Funktionen: § 356 Abs. 5 Nr. 2 BGB (Erlöschen), § 357a Abs. 2 BGB (Wertersatz). * * Ändert sich ein Wortlaut nach der Auslieferung, bekommt TEXTE_FASSUNG ein neues Datum. Die Fassung steht im Nachweis jeder * Auftragsbestätigung (texteFassung); die Dateien früherer Fassungen unter public/rechtliches/ bleiben unverändert. */ import { AUFTRAG_WEGE, LIMITS } from './konstanten.js' export const TEXTE_FASSUNG = '2026-10-06' export const ABSENDER = { name: 'WEBklar GbR', strasse: 'Am Schwimmbad 10', plzOrt: '67722 Winnweiler', telefon: '0176 23726355', email: 'support@webklar.com', web: 'webklar.com', vertretenDurch: 'Kenso Grimm und Justin Klein' } // ---------------------------------------------------------------------------------------------------------- Hilfen /** Pflichtwert: fehlt er, entsteht ein Fehler statt eines Textes mit „undefined“ (eine Auftragsbestätigung darf nie unvollständig entstehen). */ function pflicht(wert, name) { if (wert === undefined || wert === null || String(wert).trim() === '') throw new TypeError(`Text der Auftragsbestätigung: ${name} fehlt.`) return String(wert).trim() } /** Klartext der Absprache: der Schlüssel aus AUFTRAG_WEGE („telefon“) wird zu „telefonisch“, ein fertiger Klartext bleibt, ohne Angabe entfällt er. */ function wegText(weg) { const w = typeof weg === 'string' ? weg.trim() : '' if (w === '') return '' return Object.hasOwn(AUFTRAG_WEGE, w) ? AUFTRAG_WEGE[w] : w } /** Zahlungsziel als Satzteil: 0 = sofort, sonst „innerhalb von n Tagen nach Rechnungsdatum“; fehlend oder ungültig = Vorgabe (14 Tage). */ function zielText(ziel) { const n = typeof ziel === 'number' ? ziel : Number.parseInt(String(ziel), 10) const tage = Number.isInteger(n) && n >= 0 ? n : LIMITS.standardZahlungszielTage if (tage === 0) return 'sofort nach Rechnungsstellung' return `innerhalb von ${tage} ${tage === 1 ? 'Tag' : 'Tagen'} nach Rechnungsdatum` } /** Zahl mit Dezimalkomma (33.5 wird „33,5“). */ const zahl = (wert) => String(wert).replace('.', ',') const PRIVAT_BEGINN = 'Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.' // ---------------------------------------------------------------------------------------------------------- Auftragsbestätigung export const AB_TEXTE = { titel: 'Auftragsbestätigung', einleitung: ({ datum, weg } = {}) => { const art = wegText(weg) return `Vielen Dank für Ihren Auftrag vom ${pflicht(datum, 'datum')}${art ? ` (${art})` : ''}. Wir bestätigen ihn wie folgt:` }, aufwand: ({ satz, schaetzung } = {}) => { const saetze = [ `Wir rechnen nach Aufwand ab: ${pflicht(satz, 'satz')} pro Stunde, erfasst je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet.`, 'Wirkt eine weitere Person unterstützend mit, berechnen wir für sie die Hälfte der Arbeitszeit (AGB Nr. 9).', ] if (schaetzung !== undefined && schaetzung !== null && String(schaetzung).trim() !== '') { const stunden = String(schaetzung).trim() saetze.push(`Der geschätzte Umfang beträgt etwa ${stunden} ${stunden === '1' ? 'Stunde' : 'Stunden'}; die Schätzung ist unverbindlich.`) saetze.push('Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.') } return saetze.join(' ') }, kleinunternehmer: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.', zahlungsplan: { nach_abnahme: ({ ziel } = {}) => `Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar ${zielText(ziel)}.`, anzahlung: ({ prozent, anzahlung, rest, ziel } = {}) => `Anzahlung ${zahl(pflicht(prozent, 'prozent'))} % (${pflicht(anzahlung, 'anzahlung')}) nach Auftragsbestätigung, ` + `Rest (${pflicht(rest, 'rest')}) nach Abnahme; zahlbar jeweils ${zielText(ziel)}.`, monatlich: ({ ziel } = {}) => `Wir stellen den angefallenen Aufwand monatlich in Rechnung; zahlbar ${zielText(ziel)}.`, frei: ({ text, ziel } = {}) => { const t = pflicht(text, 'text') return `${t}${/[.!?]$/.test(t) ? '' : '.'} Zahlbar jeweils ${zielText(ziel)}.` }, }, termine: ({ start, fertig, privat = false } = {}) => { const teile = [] if (start && fertig) teile.push(`Beginn: ${start}; voraussichtliche Fertigstellung: ${fertig}.`) else if (start) teile.push(`Beginn: ${start}.`) else if (fertig) teile.push(`Voraussichtliche Fertigstellung: ${fertig}.`) if (privat) teile.push(PRIVAT_BEGINN) return teile.length > 0 ? teile.join(' ') : null }, mitwirkung: 'Mitwirkung und Abnahme richten sich nach Nr. 7 und Nr. 8 unserer Allgemeinen Geschäftsbedingungen. ' + 'Bitte stellen Sie uns die benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.', agb: ({ fassung } = {}) => `Es gelten unsere Allgemeinen Geschäftsbedingungen in der Fassung vom ${pflicht(fassung, 'fassung')}, die dieser Bestätigung beiliegen.`, ersetzt: ({ nummer, datum } = {}) => `Diese Auftragsbestätigung ersetzt ${pflicht(nummer, 'nummer')} vom ${pflicht(datum, 'datum')}.`, gruss: 'Mit freundlichen Grüßen', } // ---------------------------------------------------------------------------------------------------------- Bestätigung und Frühbeginn /** Häkchen auf der Bestätigungsseite und im Kundenportal (Wortlaut der Spezifikation; Alternative mit Bezug auf die AGB: Entscheidung E2 im Freigabe-Dokument). */ export const BESTAETIGUNG_TEXT = 'Ich bestätige den Auftrag zu diesen Bedingungen.' /** Fassung der Frühbeginn-Erklärung; steht im Nachweis der Bestätigung (fruehbeginnFassung). */ export const FRUEHBEGINN_FASSUNG = '2026-10-06' /** Zweites, freiwilliges Häkchen für Privatpersonen: Verlangen (§ 357a Abs. 2 Nr. 1 BGB), Zustimmung und Kenntnis (§ 356 Abs. 5 Nr. 2 BGB). */ export const FRUEHBEGINN_TEXT = 'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Ausführung dieses Projektauftrags beginnt. ' + 'Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung 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).' // ---------------------------------------------------------------------------------------------------------- Widerrufsbelehrung und Formular const KONTAKT = `${ABSENDER.name}, ${ABSENDER.strasse}, ${ABSENDER.plzOrt}, Telefon: ${ABSENDER.telefon}, E-Mail: ${ABSENDER.email}` /** Muster der Anlage 1 für einen Dienstleistungsvertrag; Lücken: Frist „des Vertragsabschlusses.“, Kontaktangaben, Widerrufsfunktion, Wertersatz. */ export const WIDERRUFSBELEHRUNG_PROJEKT = { titel: 'Widerrufsbelehrung', abschnitte: [ { ueberschrift: 'Widerrufsrecht', absaetze: [ 'Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.', 'Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.', `Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (${KONTAKT}) mittels einer eindeutigen Erklärung ` + '(z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. ' + 'Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.', 'Sie können Ihr Widerrufsrecht auch online unter https://project.webklar.com/widerrufen.html ausüben. ' + 'Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) ' + 'unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.', 'Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.', ], }, { ueberschrift: 'Folgen des Widerrufs', absaetze: [ 'Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten ' + '(mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, ' + 'günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, ' + 'an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. ' + 'Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, ' + 'es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.', 'Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, ' + 'der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, ' + 'bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.', ], }, ], } /** Muster der Anlage 2; „–“ ist das Aufzählungszeichen des Musters, die letzte Zeile gehört zum Muster. */ export const WIDERRUFSFORMULAR = { titel: 'Muster-Widerrufsformular', einleitung: '(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)', zeilen: [ `– An ${ABSENDER.name}, ${ABSENDER.strasse}, ${ABSENDER.plzOrt}, E-Mail: ${ABSENDER.email}:`, '– Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)', '– Bestellt am (*)/erhalten am (*)', '– Name des/der Verbraucher(s)', '– Anschrift des/der Verbraucher(s)', '– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)', '– Datum', '(*) Unzutreffendes streichen.', ], } ``` - [ ] **Step 5: Lauf, PASS erwartet.** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/auftraegeRechtstexte.test.js 2>&1 | command grep -a -E "^(✔|✖|ℹ (tests|pass|fail|skipped))" ``` Erwartet: 18 Tests grün, `fail 0`. - [ ] **Step 6: Verbraucher von `texte.js` prüfen.** Tests anderer Stränge im Ausgangsstand dürfen die neuen Wortlaute nicht verlangen oder pinnen: ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 command grep -a -rl "auftraege/texte" server test scripts public --include=*.js --include=*.mjs | command grep -a -v "^test/auftraegeRechtstexte" ``` Jede gefundene Testdatei (Dateien unter `test/`) einzeln mit dem Lauf aus Step 5 ausführen. Schlägt eine fehl, weil sie einen alten Wortlaut festhält: **nicht ändern** (fremde Datei), im Bericht unter „Vertragsfragen“ melden (Vorschlag: L0-Tests pinnen nur Form und Exportnamen, nie Wortlaute). - [ ] **Step 7: Commit.** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 git add server/services/auftraege/texte.js docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt test/auftraegeRechtstexte.test.js git status --short git commit -m "Rechtstexte der Aufträge: Endfassung der Textbausteine, Bestätigung, Frühbeginn-Erklärung und Widerrufsbelehrung (Fassung 2026-10-06)" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 3: AGB-Kopie als Markdown-Quelle **Files:** - Create: `docs/rechtliches/agb.md` - Modify: `test/auftraegeRechtstexte.test.js` (Teil 2 anhängen) **Interfaces:** `docs/rechtliches/agb.md` ist die Quelle der AGB-Kopie für den Generator (Task 4). Aufbau: `# Allgemeine Geschäftsbedingungen`, Zeile `**Fassung 2026-09-30** · …` (Pflicht des Generators), dann der Titelblock der Website als `## Kaufvertrag – WEBklar (Modularer Projektvertrag)` mit den Parteien, dann `## 1.` bis `## 14.` mit Absätzen `(1)`, `(2)`, … (die Website zeigt sie als nummerierte Liste). Umsetzungsregeln gegenüber `AGB.tsx`: Text unverändert; Listennummern der Website werden zu „(n)“; die Gruppen 6.1 und 6.2 werden `###`-Überschriften; die Aufzählung in Nr. 10 Abs. 2 bleibt eine Liste mit „- “; der Link auf die „Vertragsbedingungen für die WEBklar-Abos“ in Nr. 5 ist im PDF Text ohne Link (der Generator kennt keine Links; die Seite verlinkt die Abo-Bedingungen in der Kopfzeile). Der Test vergleicht den Klartext ohne diese Listenzeichen mit `AGB.tsx`. - [ ] **Step 1: Test zuerst.** An das Ende von `test/auftraegeRechtstexte.test.js` eine Leerzeile und dann diesen Block anhängen (zum Beispiel mit `cat >> test/auftraegeRechtstexte.test.js <<'JSEOF'` … `JSEOF`): ```js // ---------------------------------------------------------------------------------------------------------- AGB-Kopie const AGB_MD = 'docs/rechtliches/agb.md' // Quelle der Website-AGB auf dem Server (Repository webklar.com). Fehlt die Datei (anderer Rechner, Container), entfällt nur der Vergleich mit der Website. const AGB_WEBSITE = process.env.WEBKLAR_AGB_QUELLE || '/home/webklar/apps/webklar.com/build/src/pages/AGB.tsx' // SHA-256 des normierten Klartexts der Website-AGB vom 30.09.2026 (Commit 72f278b der Website; Titelblock und Nr. 1 bis 14). Ändert sich der Text der Kopie, // braucht sie eine neue Fassung: AGB_FASSUNG hochziehen, agb.md ändern, Seite und PDF neu erzeugen, diesen Hash eintragen. const AGB_TEXT_SHA256 = 'f3374352ef97e6050bb0908c1b4abd806f759851b5b85fa9d030512d46b36659' /** Textknoten der Kopie: alles nach der Fassungszeile, ohne Überschriftszeichen, Aufzählungszeichen, Absatznummern „(1)“ und Fettdruck. */ function agbKnotenAusMarkdown(md) { const zeilen = md.replace(/\r/g, '').split('\n') const start = zeilen.findIndex((z) => z.startsWith('**Fassung ')) assert.ok(start > 0, 'agb.md: Fassungszeile fehlt') return zeilen .slice(start + 1) .filter((z) => z.trim() !== '') .map((z) => z.replace(/^#{1,3} /, '').replace(/^- /, '').replace(/^\(\d+\) /, '').replace(/\*\*/g, '').replace(/\s+/g, ' ').trim()) } /** Textknoten der Website-Seite AGB.tsx: vom Seitentitel bis zum Ende des Vertragstexts, ohne JSX-Kommentare und Markup. */ function agbKnotenAusWebsite(tsx) { const von = tsx.indexOf('

') const bis = tsx.indexOf('') assert.ok(von > 0 && bis > von, 'AGB.tsx: Aufbau hat sich geändert (Seitentitel oder article nicht gefunden)') return tsx .slice(von, bis) .replace(/\{\/\*[\s\S]*?\*\/\}/g, ' ') .replace(/\{" "\}/g, ' ') .replace(/<[^>]*>/g, '\n') .split('\n') .map((z) => z.replace(/\s+/g, ' ').trim()) .filter(Boolean) } const agbText = (knoten) => norm(knoten.join(' ')) /** Die erste Stelle, an der zwei Texte auseinanderlaufen, mit etwas Umgebung (für die Fehlermeldung). */ const ersterUnterschied = (a, b) => { let i = 0 while (i < a.length && i < b.length && a[i] === b[i]) i += 1 return `Kopie: „…${a.slice(Math.max(0, i - 40), i + 60)}…“, Website: „…${b.slice(Math.max(0, i - 40), i + 60)}…“` } test('agb_fassung: AGB_FASSUNG ist der 30.09.2026, der Tag der letzten Textänderung der Website-AGB (Commit 72f278b); die Kopie trägt dieselbe Fassung', () => { assert.equal(AGB_FASSUNG, '2026-09-30') assert.ok(lies(AGB_MD).startsWith(`# Allgemeine Geschäftsbedingungen\n\n**Fassung ${AGB_FASSUNG}**`)) }) test('agb_md_aufbau: vierzehn Abschnitte in Reihenfolge; die Verweise der Auftragsbestätigung auf Nr. 7, 8 und 9 treffen', () => { const md = lies(AGB_MD) const abschnitte = md.split('\n').filter((z) => /^## \d+\. /.test(z)).map((z) => z.replace(/^## /, '')) assert.deepEqual(abschnitte.map((z) => Number.parseInt(z, 10)), Array.from({ length: 14 }, (_, i) => i + 1)) const titel = Object.fromEntries(abschnitte.map((z) => [Number.parseInt(z, 10), z.replace(/^\d+\. /, '')])) assert.equal(titel[7], 'Mitwirkungspflichten des Kunden') assert.equal(titel[8], 'Abnahme') assert.equal(titel[9], 'Vergütung und Zahlung') assert.ok(T.mitwirkung.includes('Nr. 7 und Nr. 8')) assert.ok(T.aufwand({ satz: '100,00 €' }).includes('(AGB Nr. 9)')) const nr9 = md.slice(md.indexOf('## 9. '), md.indexOf('## 10. ')) assert.ok(nr9.includes('in Schritten von 15 Minuten') && nr9.includes('die Hälfte der Arbeitszeit'), 'Nr. 9 enthält die Regel, auf die die Auftragsbestätigung verweist') // der Titelblock der Website steht als Überschrift und Parteien, die Verweise auf die Abos bleiben Text assert.ok(md.includes('## Kaufvertrag – WEBklar (Modularer Projektvertrag)')) assert.ok(md.includes('**zwischen**') && md.includes('**und**')) }) test('agb_text_pin: der normierte Text der Kopie hat den festgehaltenen SHA-256 (keine versehentliche Änderung eines Rechtstextes)', () => { const hash = sha256(agbText(agbKnotenAusMarkdown(lies(AGB_MD)))) assert.equal(hash, AGB_TEXT_SHA256, `Der Text der AGB-Kopie hat sich geändert (Hash jetzt ${hash}). Änderungen nur mit neuer Fassung: AGB_FASSUNG hochziehen, Seite und PDF neu erzeugen.`) }) test('agb_text_gleich_website: die Kopie ist wortgleich mit der Seite AGB.tsx der Website (Titelblock und Nr. 1 bis 14)', (t) => { if (!existsSync(AGB_WEBSITE)) return t.skip(`Website-AGB nicht erreichbar: ${AGB_WEBSITE}`) const website = agbText(agbKnotenAusWebsite(readFileSync(AGB_WEBSITE, 'utf8'))) const kopie = agbText(agbKnotenAusMarkdown(lies(AGB_MD))) assert.ok(kopie === website, `Die AGB-Kopie weicht von den AGB der Website ab (${ersterUnterschied(kopie, website)}). Ist die Website neu, braucht die Kopie eine neue Fassung (AGB_FASSUNG, agb.md, Seite, PDF, Hash).`) assert.equal(sha256(website), AGB_TEXT_SHA256) }) ``` - [ ] **Step 2: Lauf, FAIL erwartet** (`docs/rechtliches/agb.md` fehlt). ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/auftraegeRechtstexte.test.js 2>&1 | command grep -a -E "^(✖|ℹ (tests|pass|fail|skipped))" ``` Erwartet: die vier neuen Tests `agb_*` rot (ENOENT), die 18 Tests aus Task 2 weiter grün. - [ ] **Step 3: Umsetzung.** Neue Datei `docs/rechtliches/agb.md` mit genau diesem Inhalt (Text der Website vom 30.09.2026, Stand `72f278b`): ```markdown # Allgemeine Geschäftsbedingungen **Fassung 2026-09-30** · Wortgleiche Kopie der Allgemeinen Geschäftsbedingungen auf webklar.com/agb ## Kaufvertrag – WEBklar (Modularer Projektvertrag) **zwischen** WEBklar (im Folgenden „Anbieter“) **und** Kunde laut Angebot (im Folgenden „Kunde“) ## 1. Vertragsgegenstand (1) Gegenstand dieses Vertrages ist die Erbringung der im Angebot definierten Leistungen. (2) Der Vertrag besteht aus diesem Grundvertrag sowie den ausgewählten Leistungsmodulen. (3) Maßgeblich ist das jeweils angenommene Angebot von WEBklar. ## 2. Leistungsart (1) Sämtliche Leistungen von WEBklar stellen Dienst- und Entwicklungsleistungen dar. (2) Ein bestimmter wirtschaftlicher, technischer oder rechtlicher Erfolg wird nicht geschuldet. (3) WEBklar erbringt keinen laufenden Betrieb, sofern dieser nicht explizit vereinbart wurde. ## 3. Leistungsmodul A – Webseite (einmalig) (aktiv, wenn im Angebot enthalten) (1) WEBklar erstellt eine individuelle Webseite gemäß Angebot. (2) Die Umsetzung erfolgt nach den vom Kunden gelieferten Inhalten und Vorgaben. (3) Zusätzliche Leistungen wie Domain, Hosting, Wartung oder SEO sind nicht Bestandteil, sofern sie nicht gesondert beauftragt wurden. (4) Der Kunde ist nicht berechtigt, Änderungen am Quellcode selbst vorzunehmen. (5) Änderungen erfolgen ausschließlich durch WEBklar gegen gesonderte Vergütung. ## 4. Leistungsmodul B – Automatisierung / Virtualisierung (einmalig) (aktiv, wenn im Angebot enthalten) (1) WEBklar entwickelt individuelle Automatisierungen, Apps oder Virtualisierungssysteme. (2) Die Leistung stellt eine reine Entwicklungsleistung dar. (3) Optional kann eine Beratungsleistung Bestandteil des Projektes sein. (4) Ein laufender Betrieb, Monitoring oder Wartung ist nicht geschuldet, außer dies wurde explizit vereinbart. (5) Der Kunde entscheidet über Inhalte, Daten und Prozesse und trägt dafür die rechtliche Verantwortung. ## 5. Leistungsmodul C – Hosting (aktiv, wenn im Angebot enthalten) (1) Hosting bietet WEBklar als Hosting-Abo an. Für das Hosting-Abo und das Wartungs-Abo gelten die Vertragsbedingungen für die WEBklar-Abos mit Leistungsbeschreibungen; sie gehen diesen AGB vor. (2) Bestehende Hosting-Verträge mit jährlicher Laufzeit behalten die bei ihrem Abschluss vereinbarten Bedingungen. Abweichend davon können Verbraucher solche Verträge nach einer automatischen Verlängerung jederzeit mit einer Frist von einem Monat kündigen. (3) WEBklar ist berechtigt, externe Anbieter (z. B. Rechenzentren) einzusetzen. ## 6. Quellcode und Eigentum ### 6.1 Webseite (1) Der Kunde erhält ein einfaches, zeitlich unbegrenztes Nutzungsrecht an der fertigen Webseite. (2) Der Quellcode der Webseite wird nur auf ausdrückliche Anfrage und nach Vereinbarung herausgegeben. (3) Ohne Vereinbarung verbleibt der Quellcode bei WEBklar und wird archiviert. ### 6.2 Apps, Automatisierungen und Backend (1) Der Quellcode von Apps, Automatisierungen und Backend-Systemen verbleibt vollständig bei WEBklar. (2) Eine Herausgabe erfolgt ausschließlich nach gesonderter schriftlicher Vereinbarung. (3) Der Kunde erhält lediglich Zugriff auf die Bedienoberfläche bzw. das Frontend. ## 7. Mitwirkungspflichten des Kunden (1) Der Kunde stellt alle benötigten Inhalte, Daten und Freigaben rechtzeitig bereit. (2) Verzögerungen durch fehlende Mitwirkung gehen nicht zu Lasten von WEBklar. (3) WEBklar ist nicht verpflichtet, rechtliche Prüfungen der Inhalte vorzunehmen. ## 8. Abnahme (1) Nach Fertigstellung wird dem Kunden die Leistung zur Abnahme bereitgestellt. (2) Erfolgt innerhalb von 14 Tagen keine Rückmeldung, gilt die Leistung als abgenommen. (3) Nach Abnahme sind nur noch kostenpflichtige Änderungen möglich. ## 9. Vergütung und Zahlung (1) Die Vergütung ergibt sich aus dem Angebot. (2) Wird nach Aufwand abgerechnet, erfasst WEBklar die Arbeitszeit je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. Wirkt eine weitere Person unterstützend mit, rechnet WEBklar für sie die Hälfte der Arbeitszeit des Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede. Jeder Arbeitsschritt und jede Unterstützung ist im Kundenportal einsehbar. (3) Projektleistungen sind nach Vereinbarung fällig. (4) Für die Abos gelten die Vertragsbedingungen für die WEBklar-Abos. ## 10. Zahlungsverzug (1) Bei Zahlungsverzug erfolgen bis zu zwei Mahnungen. (2) Danach ist WEBklar berechtigt: - Leistungen zu sperren - Verzugszinsen zu berechnen - den Vertrag außerordentlich zu kündigen ## 11. Haftung (1) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat. (2) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen. (3) Der Kunde bewahrt Kopien der Inhalte und Daten auf, die er WEBklar übergibt oder selbst einpflegt. Für den Verlust solcher Inhalte und Daten haftet WEBklar außer in den Fällen von Absatz 1 nur in Höhe des Aufwands, der für ihre Wiederherstellung nötig gewesen wäre, wenn der Kunde diese Kopien aufbewahrt hätte. (4) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar. ## 12. Kündigung (1) Laufzeitverträge können zum Ende der jeweiligen Laufzeit gekündigt werden. Laufzeit und Kündigung der Abos richten sich nach den Vertragsbedingungen für die WEBklar-Abos. (2) Das Recht zur außerordentlichen Kündigung bleibt unberührt. ## 13. Referenzen WEBklar darf das Projekt nur nach ausdrücklicher Zustimmung des Kunden als Referenz verwenden. ## 14. Schlussbestimmungen (1) Es gilt deutsches Recht. (2) Gerichtsstand ist der Sitz von WEBklar, soweit zulässig. (3) Sollten einzelne Bestimmungen unwirksam sein, bleibt der Vertrag im Übrigen wirksam. ``` - [ ] **Step 4: Lauf, PASS erwartet.** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/auftraegeRechtstexte.test.js 2>&1 | command grep -a -E "^(✔|✖|﹣|ℹ (tests|pass|fail|skipped))" ``` Erwartet: 22 Tests grün, **`skipped 0`**. Steht bei `agb_text_gleich_website` ein „﹣“ (übersprungen), ist `/home/webklar/apps/webklar.com/build/src/pages/AGB.tsx` nicht lesbar: Pfad prüfen (Umgebungsvariable `WEBKLAR_AGB_QUELLE` setzt einen anderen), den Test nicht als bestanden werten. Weicht die Kopie ab, nennt die Fehlermeldung von `agb_text_gleich_website` die erste abweichende Stelle (mit Umgebung aus beiden Texten). Dort in `docs/rechtliches/agb.md` nachlesen und den Text der Plan-Vorgabe wiederherstellen; die Website wird nie angepasst. - [ ] **Step 5: Commit.** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7 git add docs/rechtliches/agb.md test/auftraegeRechtstexte.test.js git status --short git commit -m "AGB-Kopie: Quelle docs/rechtliches/agb.md wortgleich mit der Website (Fassung 2026-09-30), Tests gegen AGB.tsx" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 4: Generator `auftraege` und die beiden Seiten **Files:** - Modify: `scripts/build-legal-html.mjs` - Create (erzeugt): `public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.html`, `public/rechtliches/agb-2026-09-30.html` - Modify: `test/auftraegeRechtstexte.test.js` (Teil 3 anhängen) **Interfaces:** - `node scripts/build-legal-html.mjs auftraege [--check]`: schreibt (bzw. prüft mit `--check`, Exit 1 bei Abweichung) beide Seiten. Aufruf ohne Satz oder mit falschem Satz: Exit 2 und Hinweis; die erste Zeile des Hinweises bleibt `Aufruf: node scripts/build-legal-html.mjs termine|shop [--check]` (diese Zeile prüft `test/buildLegalHtml.test.js`), darunter steht die Zeile für `auftraege`. - `export function auftraegeDokumente(texteFassung = TEXTE_FASSUNG, agbFassung = AGB_FASSUNG) → [{ doc, md, markdown?, html, meta }]` mit `doc` `'widerruf'` (`md: null`, `markdown()` liefert die Seite aus `texte.js`) und `'agb'` (`md: 'docs/rechtliches/agb.md'`). - `export function projektWiderrufMarkdown(fassung, belehrung, formular) → string`, `export const PROJEKT_WIDERRUF_EINLEITUNG`. - Die Seiten tragen die Hülle der anderen Sätze (`render()`), `meta.fassung` ist `TEXTE_FASSUNG` bzw. `AGB_FASSUNG`; beide verlinken einander und die Abo-Bedingungen. - [ ] **Step 1: Test zuerst.** An das Ende von `test/auftraegeRechtstexte.test.js` eine Leerzeile und diesen Block anhängen: ```js // ---------------------------------------------------------------------------------------------------------- Seiten der Widerrufsbelehrung und der AGB // jedes Dokument wird in den Tests neu geholt, damit ein fehlender Generator nur die betroffenen Tests scheitern lässt (nicht das Laden der Datei) const dokumente = () => gen.auftraegeDokumente() const klartext = (html) => html .replace(/