Parallele Stränge L0–L7 (Portal), W0–W2 (WOMS), Z (Integration und Kontrolle); Vertragsfragen der Planrunde entschieden (Abschnitt „Nachträge nach der Planrunde“ in der Übersicht). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3042 lines
215 KiB
Markdown
3042 lines
215 KiB
Markdown
# 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/<Datei>`. 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 <noreply@anthropic.com>`; 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 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### 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('<h1 className="wk-seite__titel">')
|
||
const bis = tsx.indexOf('</article>')
|
||
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 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### 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(/<style>[\s\S]*?<\/style>/g, '')
|
||
.replace(/<[^>]+>/g, ' ')
|
||
.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
const quelleVon = (d) => (d.markdown ? d.markdown() : lies(d.md))
|
||
|
||
test('auftraegeDokumente: widerruf (aus texte.js) und agb (aus docs/rechtliches/agb.md) mit Quelle, Ziel und Navigation auf die Fassungen', () => {
|
||
const DOKUMENTE = dokumente()
|
||
assert.deepEqual(DOKUMENTE.map((d) => d.doc), ['widerruf', 'agb'])
|
||
assert.deepEqual(
|
||
DOKUMENTE.map((d) => [d.md, d.html]),
|
||
[
|
||
[null, `public/rechtliches/widerrufsbelehrung-projektauftrag-${texte.TEXTE_FASSUNG}.html`],
|
||
['docs/rechtliches/agb.md', `public/rechtliches/agb-${AGB_FASSUNG}.html`],
|
||
],
|
||
)
|
||
assert.deepEqual(DOKUMENTE.map((d) => d.meta.fassung), [texte.TEXTE_FASSUNG, AGB_FASSUNG])
|
||
const adressen = DOKUMENTE.map((d) => d.html.replace(/^public/, ''))
|
||
for (const d of DOKUMENTE) {
|
||
const hrefs = d.meta.nav.map((l) => l.href)
|
||
for (const a of adressen) assert.ok(hrefs.includes(a), `Navigation von ${d.doc} verlinkt ${a}`)
|
||
assert.ok(hrefs.includes('/abo-bedingungen.html') && hrefs.includes('/datenschutz.html'))
|
||
}
|
||
// andere Fassungen wirken auf Dateinamen und Meta; die Quelle der Belehrung folgt der Fassung
|
||
const andere = gen.auftraegeDokumente('2027-01-02', '2027-01-03')
|
||
assert.equal(andere[0].html, 'public/rechtliches/widerrufsbelehrung-projektauftrag-2027-01-02.html')
|
||
assert.equal(andere[1].html, 'public/rechtliches/agb-2027-01-03.html')
|
||
assert.ok(andere[0].markdown().includes('**Fassung 2027-01-02**'))
|
||
})
|
||
|
||
test('auftraege_html_gleich_generator: die eingecheckten Seiten sind genau render(Quelle), deterministisch, ohne Datum außer den Fassungen; --check Exit 0', () => {
|
||
const DOKUMENTE = dokumente()
|
||
for (const d of DOKUMENTE) {
|
||
const md = quelleVon(d)
|
||
const html = gen.render(md, d.meta)
|
||
assert.equal(lies(d.html), html, `${d.html} weicht vom Generator ab (node scripts/build-legal-html.mjs auftraege)`)
|
||
assert.equal(gen.render(md, d.meta), html, 'deterministisch: zweiter Lauf gleiche Bytes')
|
||
assert.doesNotMatch(html.replaceAll(texte.TEXTE_FASSUNG, '').replaceAll(AGB_FASSUNG, ''), /\b\d{4}-\d{2}-\d{2}\b/, `${d.html}: Datum außer den Fassungen`)
|
||
}
|
||
const lauf = spawnSync(process.execPath, ['--max-old-space-size=512', 'scripts/build-legal-html.mjs', 'auftraege', '--check'], { cwd: ROOT, encoding: 'utf8', timeout: 60000 })
|
||
assert.equal(lauf.status, 0, lauf.stdout + lauf.stderr)
|
||
assert.equal([...lauf.stdout.matchAll(/^ok /gm)].length, 2)
|
||
})
|
||
|
||
test('auftraege_html_text_gleich_quelle: der sichtbare Text jeder Seite ist genau der Text der Quelle (plus Navigation und Marke)', () => {
|
||
const DOKUMENTE = dokumente()
|
||
for (const d of DOKUMENTE) {
|
||
const aus = quelleVon(d).replace(/\r/g, '').split('\n').filter((z) => z.trim() !== '').map((z) => z.replace(/^#{1,3} /, '').replace(/^- /, ''))
|
||
const erwartet = norm(aus.join(' ').replace(/\*\*|`/g, ''))
|
||
const html = lies(d.html)
|
||
const main = html.slice(html.indexOf('<main>'), html.indexOf('</main>')).replace(/<nav class="top">[\s\S]*?<\/nav>/, '').replace('<div class="brand">WEBKLAR</div>', '')
|
||
assert.equal(klartext(main), erwartet, `${d.html}: sichtbarer Text weicht von der Quelle ab`)
|
||
}
|
||
})
|
||
|
||
test('auftraege_html_sauber: keine Kommentare, Entwurfsmarken, Skripte oder Ereignis-Attribute; Fassung im Kopf; Links nur relativ oder https', () => {
|
||
const DOKUMENTE = dokumente()
|
||
for (const d of DOKUMENTE) {
|
||
const html = lies(d.html)
|
||
assert.doesNotMatch(html, /<!--|Entwurf|ENTWURF|Prüfpunkt|<script|<iframe|<img|<form|<link|\sstyle=|\ssrc=/, `${d.html}: verbotener Inhalt`)
|
||
assert.doesNotMatch(html, /\son[a-z]+\s*=/i, `${d.html}: Ereignis-Attribut`)
|
||
assert.doesNotMatch(html, /⚖|\bQ\d+\b|\{\{|\}\}|undefined|NaN/, `${d.html}: Marker oder Platzhalter`)
|
||
assert.ok(html.includes(`<strong>Fassung ${d.meta.fassung}</strong>`), `${d.html}: Fassung fehlt im Kopf`)
|
||
assert.ok(html.includes(`<meta name="webklar-fassung" content="${d.meta.fassung}">`))
|
||
assert.match(html, /<html lang="de">/)
|
||
assert.match(html, /<title>[^<]+ – WEBklar<\/title>/)
|
||
const hrefs = [...html.matchAll(/\shref="([^"]*)"/g)].map((m) => m[1])
|
||
assert.ok(hrefs.length >= 6)
|
||
for (const h of hrefs) assert.ok(/^\/(?!\/)/.test(h) || /^https:\/\//.test(h), `${d.html}: Link ${h}`)
|
||
assert.ok(html.includes(`content: "WEBklar · ${d.meta.seitenkopf}"`))
|
||
assert.equal([...html.matchAll(/<h1>/g)].length, 1)
|
||
assert.ok(html.includes('<div class="brand">WEBKLAR</div>'))
|
||
}
|
||
})
|
||
|
||
test('auftraege_lauf: --check meldet fehlende oder abweichende Seiten mit Exit 1 und schreibt nichts; ohne --check werden beide Seiten geschrieben; Aufruf nennt alle Sätze', () => {
|
||
const DOKUMENTE = dokumente()
|
||
const dir = mkdtempSync(join(tmpdir(), 'legalhtml-auftraege-'))
|
||
try {
|
||
mkdirSync(join(dir, 'docs/rechtliches'), { recursive: true })
|
||
mkdirSync(join(dir, 'public/rechtliches'), { recursive: true })
|
||
cpSync(join(ROOT, AGB_MD), join(dir, AGB_MD))
|
||
const zeilen = []
|
||
const ruf = (...args) => gen.lauf(args, { root: dir, log: (z) => zeilen.push(z), fehler: (z) => zeilen.push(z) })
|
||
// noch keine Seiten: --check meldet beide und schreibt nichts
|
||
assert.equal(ruf('auftraege', '--check'), 1)
|
||
assert.equal(zeilen.filter((z) => z.startsWith('ABWEICHT')).length, 2)
|
||
assert.equal(existsSync(join(dir, DOKUMENTE[0].html)), false, '--check schreibt nichts')
|
||
// schreiben, dann stimmt --check, und die Bytes sind die der eingecheckten Seiten
|
||
assert.equal(ruf('auftraege'), 0)
|
||
assert.equal(ruf('auftraege', '--check'), 0)
|
||
for (const d of DOKUMENTE) assert.equal(readFileSync(join(dir, d.html), 'utf8'), lies(d.html), 'dieselben Bytes wie die eingecheckte Seite')
|
||
// eine Seite von Hand geändert, die andere Quelle geändert: Abweichung, die Dateien bleiben wie sie sind
|
||
const ziel = join(dir, DOKUMENTE[1].html)
|
||
const handgemacht = readFileSync(ziel, 'utf8').replace('<h2>1. Vertragsgegenstand</h2>', '<h2>1. Vertragsgegenstand (von Hand)</h2>')
|
||
assert.notEqual(handgemacht, readFileSync(ziel, 'utf8'))
|
||
writeFileSync(ziel, handgemacht)
|
||
zeilen.length = 0
|
||
assert.equal(ruf('auftraege', '--check'), 1)
|
||
assert.deepEqual(zeilen.filter((z) => z.startsWith('ABWEICHT')), [`ABWEICHT ${DOKUMENTE[1].html}`])
|
||
assert.equal(readFileSync(ziel, 'utf8'), handgemacht, '--check ändert die Datei nicht')
|
||
writeFileSync(ziel, lies(DOKUMENTE[1].html))
|
||
writeFileSync(join(dir, AGB_MD), `${lies(AGB_MD)}\n## 15. Neu\n\n(1) Neuer Absatz.\n`)
|
||
assert.equal(ruf('auftraege', '--check'), 1)
|
||
// falscher Aufruf: Exit 2, die Meldung nennt termine, shop und auftraege
|
||
zeilen.length = 0
|
||
assert.equal(ruf('analyse'), 2)
|
||
assert.equal(ruf('auftraege', '--schreiben'), 2)
|
||
assert.equal(ruf(), 2)
|
||
assert.match(zeilen[0], /Aufruf: node scripts\/build-legal-html\.mjs termine\|shop \[--check\]/)
|
||
assert.match(zeilen[0], /node scripts\/build-legal-html\.mjs auftraege \[--check\]/)
|
||
} finally {
|
||
rmSync(dir, { recursive: true, force: true })
|
||
}
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: Lauf, FAIL erwartet** (`gen.auftraegeDokumente is not a function`).
|
||
|
||
```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 fünf neuen Tests (`auftraegeDokumente`, `auftraege_html_*`, `auftraege_lauf`) rot, die 22 früheren grün.
|
||
|
||
- [ ] **Step 3: Umsetzung am Generator.** Sechs Änderungen in `scripts/build-legal-html.mjs`, jede mit dem Edit-Werkzeug (`old_string` kommt genau einmal vor; Zeilenumbrüche und Leerzeichen genau wie unten, ohne den Zeilenumbruch hinter dem Block):
|
||
|
||
**Edit 1** · `scripts/build-legal-html.mjs`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
// Funktionen gehört zum Satz termine und wird von shop nicht angefasst)
|
||
//
|
||
// Fassung = config.termine.avvVersion (Satz termine) bzw. config.shop.fassung (Satz shop). Die Ausgabe
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
// Funktionen gehört zum Satz termine und wird von shop nicht angefasst)
|
||
// node scripts/build-legal-html.mjs auftraege [--check]
|
||
// dasselbe für die Widerrufsbelehrung der Projektaufträge (Wortlaut aus server/services/auftraege/texte.js)
|
||
// und die AGB-Kopie (Quelle docs/rechtliches/agb.md)
|
||
//
|
||
// Fassung = config.termine.avvVersion (Satz termine), config.shop.fassung (Satz shop) bzw. TEXTE_FASSUNG und AGB_FASSUNG (Satz auftraege). Die Ausgabe
|
||
```
|
||
|
||
**Edit 2** · `scripts/build-legal-html.mjs`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
import { legalPaths } from '../server/services/legalDocs.js'
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
import { legalPaths } from '../server/services/legalDocs.js'
|
||
import { TEXTE_FASSUNG, WIDERRUFSBELEHRUNG_PROJEKT, WIDERRUFSFORMULAR } from '../server/services/auftraege/texte.js'
|
||
import { AGB_FASSUNG } from '../server/services/auftraege/konstanten.js'
|
||
```
|
||
|
||
**Edit 3** · `scripts/build-legal-html.mjs`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
/** Sätze der Kommandozeile: Name => Dokumente. */
|
||
const SAETZE = { termine: termineDokumente, shop: shopDokumente }
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
// ---------------------------------------------------------------------------------------------------------- Projektaufträge
|
||
|
||
/** Einleitung der Seite zur Widerrufsbelehrung der Projektaufträge (Seitentext, nicht Teil der Belehrung nach dem Muster). */
|
||
export const PROJEKT_WIDERRUF_EINLEITUNG =
|
||
'Das Widerrufsrecht gilt nur für Verbraucher. Wir fügen die Belehrung und das Muster-Widerrufsformular jeder Auftragsbestätigung an eine Privatperson bei '
|
||
+ 'und zeigen sie hier zusätzlich allen Kunden. Die Belehrung gilt für den Auftrag, den die jeweilige Auftragsbestätigung bestätigt.'
|
||
|
||
/**
|
||
* Markdown der Seite „Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge)“ aus server/services/auftraege/texte.js: Überschrift,
|
||
* Fassung, Einleitung, dann die Abschnitte der Belehrung und das Formular, jede Formularzeile als eigener Absatz.
|
||
*/
|
||
export function projektWiderrufMarkdown(fassung = TEXTE_FASSUNG, belehrung = WIDERRUFSBELEHRUNG_PROJEKT, formular = WIDERRUFSFORMULAR) {
|
||
const teile = [
|
||
'# Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge)',
|
||
`**Fassung ${fassung}**`,
|
||
PROJEKT_WIDERRUF_EINLEITUNG,
|
||
`## ${belehrung.titel}`,
|
||
...belehrung.abschnitte.flatMap((a) => [`### ${a.ueberschrift}`, ...a.absaetze]),
|
||
`## ${formular.titel}`,
|
||
formular.einleitung,
|
||
...formular.zeilen,
|
||
]
|
||
return `${teile.join('\n\n')}\n`
|
||
}
|
||
|
||
/**
|
||
* Die zwei Seiten des Satzes auftraege: die Widerrufsbelehrung der Projektaufträge (Fassung TEXTE_FASSUNG, Wortlaut aus texte.js, ohne Markdown-Datei)
|
||
* und die AGB-Kopie (Fassung AGB_FASSUNG, Quelle docs/rechtliches/agb.md). Beide Seiten verlinken einander. Die Adressen stehen nicht in legalDocs.js:
|
||
* die Mails und das PDF der Auftragsbestätigung verlinken die versionierten Dateien (public/rechtliches/<slug>-<Fassung>.html|pdf) unmittelbar.
|
||
*/
|
||
export function auftraegeDokumente(texteFassung = TEXTE_FASSUNG, agbFassung = AGB_FASSUNG) {
|
||
const widerrufAdresse = `/rechtliches/widerrufsbelehrung-projektauftrag-${texteFassung}.html`
|
||
const agbAdresse = `/rechtliches/agb-${agbFassung}.html`
|
||
const nav = [
|
||
{ href: '/dashboard.html', text: '← Kundenportal' },
|
||
{ href: '/abo-bedingungen.html', text: 'Abo-Bedingungen' },
|
||
{ href: widerrufAdresse, text: 'Widerrufsbelehrung Projektaufträge' },
|
||
{ href: agbAdresse, text: 'AGB' },
|
||
{ href: '/datenschutz.html', text: 'Datenschutzerklärung' },
|
||
{ href: 'https://webklar.com/impressum', text: 'Impressum' },
|
||
]
|
||
return [
|
||
{
|
||
doc: 'widerruf',
|
||
md: null,
|
||
markdown: () => projektWiderrufMarkdown(texteFassung),
|
||
html: `public${widerrufAdresse}`,
|
||
meta: { fassung: texteFassung, titel: 'Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge)', seitenkopf: 'Widerrufsbelehrung Projektaufträge', nav },
|
||
},
|
||
{
|
||
doc: 'agb',
|
||
md: 'docs/rechtliches/agb.md',
|
||
html: `public${agbAdresse}`,
|
||
meta: { fassung: agbFassung, titel: 'Allgemeine Geschäftsbedingungen', seitenkopf: 'AGB', nav },
|
||
},
|
||
]
|
||
}
|
||
|
||
/** Sätze der Kommandozeile: Name => Dokumente. */
|
||
const SAETZE = { termine: termineDokumente, shop: shopDokumente, auftraege: auftraegeDokumente }
|
||
```
|
||
|
||
**Edit 4** · `scripts/build-legal-html.mjs`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
* Kommandozeile: `termine|shop [--check]`.
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
* Kommandozeile: `termine|shop|auftraege [--check]`.
|
||
```
|
||
|
||
**Edit 5** · `scripts/build-legal-html.mjs`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
fehler('Aufruf: node scripts/build-legal-html.mjs termine|shop [--check]')
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
fehler('Aufruf: node scripts/build-legal-html.mjs termine|shop [--check]\n node scripts/build-legal-html.mjs auftraege [--check]')
|
||
```
|
||
|
||
**Edit 6** · `scripts/build-legal-html.mjs`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
const html = render(readFileSync(join(root, d.md), 'utf8'), d.meta)
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
const html = render(d.markdown ? d.markdown() : readFileSync(join(root, d.md), 'utf8'), d.meta)
|
||
```
|
||
|
||
- [ ] **Step 4: Seiten erzeugen und kontrollieren.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
node scripts/build-legal-html.mjs auftraege
|
||
node scripts/build-legal-html.mjs auftraege --check
|
||
node scripts/build-legal-html.mjs termine --check
|
||
node scripts/build-legal-html.mjs shop --check
|
||
node scripts/build-legal-html.mjs analyse; echo "Exit $? (erwartet 2, Hinweis mit zwei Zeilen)"
|
||
git status --short
|
||
```
|
||
|
||
Erwartet: zwei neue Dateien unter `public/rechtliches/`, alle `--check` mit Exit 0 (`ok public/rechtliches/…`), `termine` und `shop` unverändert.
|
||
|
||
- [ ] **Step 5: Lauf, PASS erwartet** (neue Tests und der bestehende Generator-Test).
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
for f in auftraegeRechtstexte buildLegalHtml; 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
|
||
```
|
||
|
||
Erwartet: `auftraegeRechtstexte` 27 Tests grün (`skipped 0`), `buildLegalHtml` 18 Tests grün.
|
||
|
||
- [ ] **Step 6: Commit.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
git add scripts/build-legal-html.mjs public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.html public/rechtliches/agb-2026-09-30.html test/auftraegeRechtstexte.test.js
|
||
git status --short
|
||
git commit -m "build-legal-html: Satz auftraege für die Widerrufsbelehrung der Projektaufträge und die AGB, beide Seiten erzeugt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: PDF-Modus `auftraege` und Druck
|
||
|
||
**Files:**
|
||
- Modify: `scripts/build-legal-pdfs.sh`
|
||
- Create (erzeugt): `public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.pdf` und `.pdf.sha256`, `public/rechtliches/agb-2026-09-30.pdf` und `.pdf.sha256`
|
||
- Modify: `test/auftraegeRechtstexte.test.js` (Teil 4 anhängen)
|
||
|
||
**Interfaces:**
|
||
- `scripts/build-legal-pdfs.sh auftraege`: druckt `public/rechtliches/widerrufsbelehrung-projektauftrag-<TEXTE_FASSUNG>.html` und `public/rechtliches/agb-<AGB_FASSUNG>.html` zu PDF und schreibt neben jedes PDF `<PDF>.sha256` (nur die SHA-256 der HTML-Seite und ein Zeilenende). Ein PDF mit passender Prüfsumme wird nicht neu gedruckt (`FORCE=1` druckt neu). Die Fassungen kommen aus `TEXTE_FASSUNG` (`texte.js`) und `AGB_FASSUNG` (`konstanten.js`), überschreibbar mit `TEXTE_VERSION` und `AGB_VERSION`. Fehlt eine HTML-Seite, bricht das Skript mit dem Hinweis auf `node scripts/build-legal-html.mjs auftraege` ab.
|
||
- Die Datei `public/rechtliches/agb-<AGB_FASSUNG>.pdf` ist der Anhang der Mail K31 (Anhangsname `WEBklar-AGB-<AGB_FASSUNG>.pdf`) und das Ziel des Links `agbUrl` der Bestätigungsseite (L5, L6).
|
||
- Die Funktionen `texte_version`, `agb_version`, `auftraege_pdfs` stehen **vor** `shop_pdfs`: `test/buildLegalHtml.test.js` liest den Modus `shop` ab `shop_pdfs() {` und erwartet dort kein „widerrufsbelehrung“.
|
||
|
||
- [ ] **Step 1: Test zuerst.** An das Ende von `test/auftraegeRechtstexte.test.js` eine Leerzeile und diesen Block anhängen:
|
||
|
||
```js
|
||
// ---------------------------------------------------------------------------------------------------------- PDFs
|
||
|
||
test('auftraege_pdf_dateien: je Seite ein PDF unter demselben Namen, Prüfsumme der HTML-Seite daneben (HTML geändert ohne neues PDF fällt auf)', () => {
|
||
const DOKUMENTE = dokumente()
|
||
const mindest = { widerruf: { seiten: 1, bytes: 5 * 1024 }, agb: { seiten: 2, bytes: 20 * 1024 } }
|
||
for (const d of DOKUMENTE) {
|
||
const pdf = d.html.replace(/\.html$/, '.pdf')
|
||
const bytes = readFileSync(join(ROOT, pdf))
|
||
assert.equal(bytes.subarray(0, 5).toString('latin1'), '%PDF-', `${pdf}: kein PDF`)
|
||
assert.ok(bytes.length > mindest[d.doc].bytes, `${pdf}: ${bytes.length} Bytes sind zu wenig`)
|
||
assert.ok(bytes.subarray(-1024).toString('latin1').includes('%%EOF'), `${pdf}: PDF ist abgeschnitten`)
|
||
const seiten = bytes.toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []
|
||
assert.ok(seiten.length >= mindest[d.doc].seiten, `${pdf}: ${seiten.length} Seiten`)
|
||
const summe = lies(`${pdf}.sha256`)
|
||
assert.match(summe, /^[0-9a-f]{64}\n$/, `${pdf}.sha256: nur die Prüfsumme und ein Zeilenende`)
|
||
assert.equal(summe.trim(), sha256(readFileSync(join(ROOT, d.html))), `${d.html} wurde geändert, ohne das PDF neu zu drucken (scripts/build-legal-pdfs.sh auftraege)`)
|
||
}
|
||
// die AGB liegen unter dem Namen, den die Auftragsbestätigung (Mail K31 und Bestätigungsseite) verlinkt und anhängt
|
||
assert.ok(existsSync(join(ROOT, `public/rechtliches/agb-${AGB_FASSUNG}.pdf`)))
|
||
})
|
||
|
||
test('auftraege_script_modus: build-legal-pdfs.sh kennt auftraege (Fassungen aus texte.js und konstanten.js), all druckt es mit, bestehende Modi und Grenzen unverändert', () => {
|
||
const sh = lies('scripts/build-legal-pdfs.sh')
|
||
assert.match(sh, /^auftraege\)\n {2}auftraege_pdfs\n {2};;$/m)
|
||
for (const name of ['TEXTE_VERSION', 'TEXTE_FASSUNG', 'AGB_VERSION', 'AGB_FASSUNG', 'widerrufsbelehrung-projektauftrag:$texte', 'agb:$agb']) assert.ok(sh.includes(name), name)
|
||
// die Funktionen stehen vor shop_pdfs (test/buildLegalHtml.test.js liest ab dort den Modus shop) und zeigen auf die HTML-Quelle
|
||
const auftraege = sh.slice(sh.indexOf('\nauftraege_pdfs() {'), sh.indexOf('\nshop_pdfs() {'))
|
||
assert.ok(auftraege.length > 100 && sh.indexOf('\nauftraege_pdfs() {') > 0, 'auftraege_pdfs steht vor shop_pdfs')
|
||
assert.ok(auftraege.includes('sha256sum "$quelle"') && auftraege.includes('echo "$summe" > "$ziel.sha256"') && auftraege.includes('(unveraendert)') && auftraege.includes('${FORCE:-}'))
|
||
assert.ok(auftraege.includes('node scripts/build-legal-html.mjs auftraege'), 'fehlt die HTML-Seite, nennt die Meldung den Generator')
|
||
assert.ok(sh.slice(sh.indexOf('\nall)'), sh.indexOf('\n*)')).includes(' auftraege_pdfs\n'), 'all druckt auch diese PDFs')
|
||
assert.ok(sh.includes('Usage: $0 [all|analyse|termine|shop|datenschutz]') && sh.includes('$0 auftraege'), 'Aufruf nennt alle Modi')
|
||
// je Chrome-Aufruf höchstens 120 Sekunden, kein paralleler Lauf, Node-Aufrufe mit Heap-Grenze und Zeitgrenze
|
||
assert.match(sh, /^ {2}timeout 120 "\$CHROME" /m)
|
||
assert.equal([...sh.matchAll(/timeout 60 node --max-old-space-size=256 /g)].length, 4, 'je Fassungsfunktion ein Aufruf: termine, shop, texte und agb')
|
||
assert.ok(!/ &$|^\s*wait\b|xargs -P/m.test(sh), 'keine parallelen Läufe')
|
||
assert.equal(spawnSync('bash', ['-n', 'scripts/build-legal-pdfs.sh'], { cwd: ROOT, encoding: 'utf8' }).status, 0)
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: Lauf, FAIL erwartet** (kein Modus, keine PDFs).
|
||
|
||
```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: `auftraege_pdf_dateien` und `auftraege_script_modus` rot, die 27 früheren grün.
|
||
|
||
- [ ] **Step 3: Umsetzung am Skript.** Fünf Änderungen in `scripts/build-legal-pdfs.sh` (Edit-Werkzeug, `old_string` kommt genau einmal vor; die Reihenfolge ist gleichgültig):
|
||
|
||
**Edit 1** · `scripts/build-legal-pdfs.sh`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
# Usage: CHROME=/pfad/zu/chrome scripts/build-legal-pdfs.sh [all|analyse|termine|shop|datenschutz]
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
# Usage: CHROME=/pfad/zu/chrome scripts/build-legal-pdfs.sh [all|analyse|termine|shop|auftraege|datenschutz]
|
||
```
|
||
|
||
**Edit 2** · `scripts/build-legal-pdfs.sh`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
# gehoert zu termine (dieselbe Datei) und wird hier nicht gedruckt. `all` druckt auch diese beiden PDFs.
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
# gehoert zu termine (dieselbe Datei) und wird hier nicht gedruckt. `all` druckt auch diese beiden PDFs.
|
||
# auftraege = Widerrufsbelehrung der Projektauftraege (Fassung TEXTE_VERSION, Standard: TEXTE_FASSUNG aus server/services/auftraege/texte.js) und AGB
|
||
# (Fassung AGB_VERSION, Standard: AGB_FASSUNG aus server/services/auftraege/konstanten.js); die HTML-Seiten kommen vorher aus
|
||
# `node scripts/build-legal-html.mjs auftraege`, Pruefsumme und Neudruck wie bei termine und shop. `all` druckt auch diese beiden PDFs.
|
||
```
|
||
|
||
**Edit 3** · `scripts/build-legal-pdfs.sh`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
# Leistungsbeschreibung und AVV des Webshops drucken (je PDF nur, wenn die Pruefsumme der HTML-Seite nicht mehr passt)
|
||
shop_pdfs() {
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
# Fassung der Projektauftrags-Texte (Widerrufsbelehrung): TEXTE_VERSION, sonst TEXTE_FASSUNG aus server/services/auftraege/texte.js
|
||
texte_version() {
|
||
local v="${TEXTE_VERSION:-}"
|
||
if [ -z "$v" ]; then
|
||
v="$(timeout 60 node --max-old-space-size=256 --input-type=module -e "const { TEXTE_FASSUNG } = await import('./server/services/auftraege/texte.js'); console.log(TEXTE_FASSUNG)")"
|
||
fi
|
||
[[ "$v" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "Ungueltige Fassung: $v" >&2; exit 1; }
|
||
echo "$v"
|
||
}
|
||
|
||
# Fassung der AGB-Kopie: AGB_VERSION, sonst AGB_FASSUNG aus server/services/auftraege/konstanten.js
|
||
agb_version() {
|
||
local v="${AGB_VERSION:-}"
|
||
if [ -z "$v" ]; then
|
||
v="$(timeout 60 node --max-old-space-size=256 --input-type=module -e "const { AGB_FASSUNG } = await import('./server/services/auftraege/konstanten.js'); console.log(AGB_FASSUNG)")"
|
||
fi
|
||
[[ "$v" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "Ungueltige Fassung: $v" >&2; exit 1; }
|
||
echo "$v"
|
||
}
|
||
|
||
# Widerrufsbelehrung der Projektauftraege und AGB drucken (je PDF nur, wenn die Pruefsumme der HTML-Seite nicht mehr passt)
|
||
auftraege_pdfs() {
|
||
local texte agb eintrag slug fassung quelle ziel summe
|
||
texte="$(texte_version)"
|
||
agb="$(agb_version)"
|
||
for eintrag in "widerrufsbelehrung-projektauftrag:$texte" "agb:$agb"; do
|
||
slug="${eintrag%%:*}"
|
||
fassung="${eintrag##*:}"
|
||
quelle="public/rechtliches/$slug-$fassung.html"
|
||
ziel="public/rechtliches/$slug-$fassung.pdf"
|
||
[ -f "$quelle" ] || { echo "HTML fehlt: $quelle (zuerst node scripts/build-legal-html.mjs auftraege)"; exit 1; }
|
||
summe="$(sha256sum "$quelle" | cut -d' ' -f1)"
|
||
if [ -z "${FORCE:-}" ] && [ -f "$ziel" ] && [ "$(cat "$ziel.sha256" 2>/dev/null)" = "$summe" ]; then
|
||
echo "= $ziel (unveraendert)"
|
||
else
|
||
pdf "$quelle" "$ziel"
|
||
echo "$summe" > "$ziel.sha256"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# Leistungsbeschreibung und AVV des Webshops drucken (je PDF nur, wenn die Pruefsumme der HTML-Seite nicht mehr passt)
|
||
shop_pdfs() {
|
||
```
|
||
|
||
**Edit 4** · `scripts/build-legal-pdfs.sh`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
shop)
|
||
shop_pdfs
|
||
;;
|
||
datenschutz)
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
shop)
|
||
shop_pdfs
|
||
;;
|
||
auftraege)
|
||
auftraege_pdfs
|
||
;;
|
||
datenschutz)
|
||
```
|
||
|
||
**Edit 5** · `scripts/build-legal-pdfs.sh`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
shop_pdfs
|
||
;;
|
||
*)
|
||
echo "Usage: $0 [all|analyse|termine|shop|datenschutz]"; exit 1
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
shop_pdfs
|
||
auftraege_pdfs
|
||
;;
|
||
*)
|
||
echo "Usage: $0 [all|analyse|termine|shop|datenschutz]"; echo " $0 auftraege (Widerrufsbelehrung der Projektauftraege und AGB)"; exit 1
|
||
```
|
||
|
||
- [ ] **Step 4: Syntax und statische Prüfungen** (ohne Chrome).
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
bash -n scripts/build-legal-pdfs.sh && echo "Syntax ok"
|
||
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: `auftraege_script_modus` grün, nur `auftraege_pdf_dateien` noch rot (PDFs fehlen).
|
||
|
||
- [ ] **Step 5: Drucken.** Zuerst den freien Arbeitsspeicher prüfen (Spalte „available“, mindestens 1500 MB, sonst warten), dann hinter dem flock der Übersicht drucken:
|
||
|
||
```bash
|
||
free -m
|
||
SP=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
flock "$SP/auftraege-schwer.lock" nice -n 10 scripts/build-legal-pdfs.sh auftraege
|
||
ls -la public/rechtliches/*projektauftrag* public/rechtliches/agb-*
|
||
```
|
||
|
||
Erwartet: zwei Zeilen `-> public/rechtliches/….pdf`, vier neue Dateien (zwei PDF, zwei `.sha256`), jedes PDF deutlich größer als 5 KB (Belehrung) bzw. 20 KB (AGB). Schlägt ein Druck fehl (Zeitgrenze, Chrome nicht gefunden): nicht wiederholen, nicht ersetzen, im Bericht melden. Ein zweiter Lauf desselben Befehls druckt nichts neu und meldet `= … (unveraendert)`.
|
||
|
||
- [ ] **Step 6: Lauf, PASS erwartet.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
for f in auftraegeRechtstexte buildLegalHtml; 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
|
||
```
|
||
|
||
Erwartet: `auftraegeRechtstexte` 29 Tests grün (`skipped 0`), `buildLegalHtml` 18 Tests grün (er prüft `script_modus` und `shop_script_modus` gegen das geänderte Skript).
|
||
|
||
- [ ] **Step 7: Commit.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
git add scripts/build-legal-pdfs.sh test/auftraegeRechtstexte.test.js public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.pdf public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.pdf.sha256 public/rechtliches/agb-2026-09-30.pdf public/rechtliches/agb-2026-09-30.pdf.sha256
|
||
git status --short
|
||
git commit -m "build-legal-pdfs: Modus auftraege, PDFs und Prüfsummen der Widerrufsbelehrung der Projektaufträge und der AGB" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: Datenschutzerklärung
|
||
|
||
**Files:**
|
||
- Modify: `public/datenschutz.html`
|
||
- Modify: `docs/WEBklar-Datenschutzerklaerung.pdf` und `docs/WEBklar-Datenschutzerklaerung.pdf.sha256` (neu gedruckt)
|
||
- Modify: `test/legalTexts.test.js` (eine Zeile)
|
||
- Modify: `test/auftraegeRechtstexte.test.js` (Teil 5 anhängen)
|
||
|
||
**Interfaces:**
|
||
- Neuer Abschnitt `<h2 id="auftraege">9a. Auftragsbestätigungen</h2>` mit drei Absätzen zwischen „9. Buchhaltung und Rechnungen“ und „10. Domains unserer Kunden“; Anker `/datenschutz.html#auftraege` (die Bestätigungsseite und die Mail K31 dürfen darauf verlinken). Die Überschriften 1 bis 15 bleiben unverändert (der Test in `legalTexts.test.js` zählt sie).
|
||
- Der Absatz `<p id="kuendigung-widerruf">` in Abschnitt 7 nennt zusätzlich die Nummer der Auftragsbestätigung (freiwillig, wie auf der Widerrufsseite), die Zuordnung eines Widerrufs und die Aufbewahrung; der Absatz „Alle Portal-Daten …“ in Abschnitt 6 verweist für Auftragsbestätigungen auf 9a; der Stand ist `06.10.2026`.
|
||
- Gepinnte Stellen bleiben unverändert: die Sätze in Abschnitt 6 und 7, die `test/zeitenRecht.test.js` und `test/legalTexts.test.js` festhalten, die Zahl der Vorkommen von „drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet“ (genau drei) und die Reihenfolge „kuendigen.html … Abschnitt 9“ (der neue Verweis auf 9a steht deshalb **hinter** dem ersten „Abschnitt 9“ des Absatzes).
|
||
- Rechtsgrundlagen im neuen Abschnitt: Art. 6 Abs. 1 lit. b und c DSGVO, ohne lit. f; Ratenbegrenzung ohne Zahlen; Aufbewahrung durch Verweis auf Abschnitt 9.
|
||
|
||
- [ ] **Step 1: Tests zuerst.** (a) An das Ende von `test/auftraegeRechtstexte.test.js` eine Leerzeile und diesen Block anhängen:
|
||
|
||
```js
|
||
// ---------------------------------------------------------------------------------------------------------- Datenschutzerklärung
|
||
|
||
const DS_PFAD = 'public/datenschutz.html'
|
||
const dsHtml = () => lies(DS_PFAD)
|
||
const dsKlartext = (html) => norm(html.replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&'))
|
||
/** Der Block von <h2 id="auftraege"> bis vor die nächste Überschrift (roh, mit Tags). */
|
||
const block9a = (html = dsHtml()) => {
|
||
const a = html.indexOf('<h2 id="auftraege">')
|
||
assert.ok(a >= 0, '<h2 id="auftraege"> fehlt')
|
||
const b = html.indexOf('<h2', a + 5)
|
||
assert.ok(b > a)
|
||
return html.slice(a, b)
|
||
}
|
||
/** Ein ganzer Absatz <p …>…</p>, der den Anfang enthält. */
|
||
const absatzMit = (html, anfang) => {
|
||
const a = html.indexOf(anfang)
|
||
assert.ok(a >= 0, `Absatz fehlt: ${anfang}`)
|
||
return html.slice(html.lastIndexOf('<p', a), html.indexOf('</p>', a) + 4)
|
||
}
|
||
|
||
test('ds_9a_stelle: Abschnitt 9a steht zwischen 9 und 10, die Nummerierung 1 bis 15 und Stand und Anker stimmen', () => {
|
||
const html = dsHtml()
|
||
const neun = html.indexOf('<h2>9. Buchhaltung und Rechnungen</h2>')
|
||
const a = html.indexOf('<h2 id="auftraege">')
|
||
const zehn = html.indexOf('<h2>10. Domains unserer Kunden</h2>')
|
||
assert.ok(neun > 0 && a > neun && zehn > a, 'Reihenfolge 9 < 9a < 10')
|
||
assert.equal(html.split('id="auftraege"').length - 1, 1)
|
||
assert.ok(html.includes('<h2 id="auftraege">9a. Auftragsbestätigungen</h2>'))
|
||
assert.ok(!block9a(html).includes('<!--'), 'kein Kommentar im Block')
|
||
assert.equal([...block9a(html).matchAll(/<p>/g)].length, 3, 'drei Absätze')
|
||
const titel = [...html.matchAll(/<h2>(\d+)\. ([^<]*)<\/h2>/g)].map((m) => `${m[1]}. ${m[2]}`)
|
||
assert.equal(titel.length, 15, 'die Überschriften 1 bis 15 sind unverändert')
|
||
assert.ok(html.includes('<p class="meta">Stand 06.10.2026</p>'), 'Stand = Tag der Änderung')
|
||
})
|
||
|
||
test('ds_9a_inhalt: Daten, Bestätigungslink als Hash, keine IP-Adresse, Rechtsgrundlagen b und c, Aufbewahrung wie Abschnitt 9', () => {
|
||
const text = dsKlartext(block9a())
|
||
for (const s of [
|
||
'Wenn Sie uns einen Auftrag erteilen',
|
||
'Ihren Namen, gegebenenfalls Ihre Firma, Ihre Anschrift, Ihre E-Mail-Adresse und Ihre Kundennummer',
|
||
'ob Sie als Unternehmen oder als Privatperson handeln',
|
||
'Datum und Art der Absprache',
|
||
'Hetzner Online GmbH (Abschnitt 8)',
|
||
'eine Kopie geht an das Postfach unseres Teams',
|
||
'mit einer Prüfsumme',
|
||
'Rechenzentrum Falkenstein (Deutschland)',
|
||
'project.webklar.com/auftrag.html',
|
||
'ohne Anmeldung',
|
||
'nur einen Hashwert speichern',
|
||
'gilt 60 Tage ab dem letzten Versand',
|
||
'erhalten Sie einen neuen Link',
|
||
'den Weg (Link oder Kundenportal)',
|
||
'ob Sie ausdrücklich verlangt haben, dass wir schon vor Ablauf der Widerrufsfrist beginnen',
|
||
'weder Ihre IP-Adresse noch Ihre Browserkennung',
|
||
'nicht in Ihrem Namen bestätigen',
|
||
'nur im Arbeitsspeicher des Servers, nicht dauerhaft',
|
||
'Art. 6 Abs. 1 lit. b DSGVO',
|
||
'Art. 6 Abs. 1 lit. c DSGVO',
|
||
'(§ 312f BGB, § 147 AO)',
|
||
'wie in Abschnitt 9 beschrieben auf: als Buchungsbeleg acht Jahre, als Handels- oder Geschäftsbrief sechs Jahre',
|
||
'die Fristen beginnen mit dem Ende des jeweiligen Kalenderjahres',
|
||
'Den Widerruf eines Projektauftrags beschreibt Abschnitt 7.',
|
||
]) assert.ok(text.includes(s), `9a: ${s}`)
|
||
// nicht in diesem Abschnitt: die Frist der Abos, Unterauftragsverarbeiter, Gedankenstriche, feste Zahlen der Ratenbegrenzung, lit. f
|
||
for (const s of ['drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet', 'Unterauftragsverarbeiter', 'lit. f']) assert.ok(!text.includes(s), `9a enthält nicht: ${s}`)
|
||
assert.doesNotMatch(text, /[–—]/)
|
||
assert.doesNotMatch(text, /\b\d+ (Aufrufe|Anfragen|Bestätigungen)\b/)
|
||
// Aufbewahrung: dieselben Fristen wie Abschnitt 9
|
||
const abschnitt9 = dsKlartext(dsHtml().slice(dsHtml().indexOf('<h2>9. Buchhaltung'), dsHtml().indexOf('<h2 id="auftraege">')))
|
||
assert.ok(abschnitt9.includes('acht Jahre auf (§ 147 AO, § 14b UStG), Handels- und Geschäftsbriefe sechs Jahre'))
|
||
})
|
||
|
||
test('ds_widerruf_projektauftrag: der Widerrufsabschnitt nennt die Nummer der Auftragsbestätigung, die Zuordnung und die Aufbewahrung; die gepinnten Sätze bleiben', () => {
|
||
const absatz = dsKlartext(absatzMit(dsHtml(), '<p id="kuendigung-widerruf">'))
|
||
for (const s of [
|
||
'beim E-Mail-Zusatzspeicher die Zahl der Pakete, die Sie kündigen, beim Widerruf eines Projektauftrags die Nummer der Auftragsbestätigung (freiwillig), gegebenenfalls Ihre Kundennummer,',
|
||
'Bei einem Projektauftrag ordnen wir Ihre Erklärung der Auftragsbestätigung zu, anhand Ihrer E-Mail-Adresse und der Nummer, soweit Sie sie angeben, und vermerken den Widerruf dort.',
|
||
'gilt die Frist aus Abschnitt 9. Den Widerruf eines Projektauftrags bewahren wir zusammen mit der Auftragsbestätigung auf (Abschnitt 9a).',
|
||
// unverändert
|
||
'Art. 6 Abs. 1 lit. c DSGVO (§§ 312k, 356a BGB) in Verbindung mit Art. 6 Abs. 1 lit. b DSGVO',
|
||
'mit Ihrer Erklärung speichern wir die IP-Adresse nicht',
|
||
'drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet auf',
|
||
]) assert.ok(absatz.includes(s), `Widerrufsabschnitt: ${s}`)
|
||
// die Frist der Abos kommt weiterhin genau dreimal vor (Abschnitt 7: Bestellungen, Funktionsbestellung, Kündigungs- und Widerrufsseite)
|
||
assert.equal(dsKlartext(dsHtml()).split('drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet').length - 1, 3)
|
||
// Abschnitt 6 verweist für die Auftragsbestätigungen auf 9a
|
||
const sechs = dsKlartext(absatzMit(dsHtml(), 'Alle Portal-Daten liegen in unseren selbst betriebenen Systemen'))
|
||
assert.ok(sechs.includes('Rechnungen bewahren wir wie in Abschnitt 9 beschrieben auf, Auftragsbestätigungen wie in Abschnitt 9a.'))
|
||
})
|
||
```
|
||
|
||
(b) In `test/legalTexts.test.js` den Pin des Stands ändern (Edit-Werkzeug; die Zeile kommt genau einmal vor, im Test `legal_datenschutz_kundenansicht`):
|
||
|
||
**Edit 1** · `test/legalTexts.test.js`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
assert.ok(html.includes('<p class="meta">Stand 05.10.2026</p>'), 'Stand = Tag der Änderung')
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
assert.ok(html.includes('<p class="meta">Stand 06.10.2026</p>'), 'Stand = Tag der Änderung')
|
||
```
|
||
|
||
- [ ] **Step 2: Lauf, FAIL erwartet.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
for f in auftraegeRechtstexte legalTexts; 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
|
||
```
|
||
|
||
Erwartet: `ds_9a_stelle`, `ds_9a_inhalt`, `ds_widerruf_projektauftrag` und in `legalTexts` der Test `legal_datenschutz_kundenansicht` rot.
|
||
|
||
- [ ] **Step 3: Umsetzung an `public/datenschutz.html`.** Sechs Änderungen mit dem Edit-Werkzeug (`old_string` kommt genau einmal vor). Die Absätze sind lange einzelne Zeilen; nichts umbrechen:
|
||
|
||
**Edit 1** · `public/datenschutz.html`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
<p class="meta">Stand 05.10.2026</p>
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
<p class="meta">Stand 06.10.2026</p>
|
||
```
|
||
|
||
**Edit 2** · `public/datenschutz.html`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
Rechnungen bewahren wir wie in Abschnitt 9 beschrieben auf.</p>
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
Rechnungen bewahren wir wie in Abschnitt 9 beschrieben auf, Auftragsbestätigungen wie in Abschnitt 9a.</p>
|
||
```
|
||
|
||
**Edit 3** · `public/datenschutz.html`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
beim E-Mail-Zusatzspeicher die Zahl der Pakete, die Sie kündigen, gegebenenfalls Ihre Kundennummer,
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
beim E-Mail-Zusatzspeicher die Zahl der Pakete, die Sie kündigen, beim Widerruf eines Projektauftrags die Nummer der Auftragsbestätigung (freiwillig), gegebenenfalls Ihre Kundennummer,
|
||
```
|
||
|
||
**Edit 4** · `public/datenschutz.html`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
und tragen die Kündigung oder den Widerruf, soweit möglich, bei Stripe ein.
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
und tragen die Kündigung oder den Widerruf, soweit möglich, bei Stripe ein. Bei einem Projektauftrag ordnen wir Ihre Erklärung der Auftragsbestätigung zu, anhand Ihrer E-Mail-Adresse und der Nummer, soweit Sie sie angeben, und vermerken den Widerruf dort.
|
||
```
|
||
|
||
**Edit 5** · `public/datenschutz.html`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
soweit die E-Mails Geschäftsbriefe sind, gilt die Frist aus Abschnitt 9.</p>
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
soweit die E-Mails Geschäftsbriefe sind, gilt die Frist aus Abschnitt 9. Den Widerruf eines Projektauftrags bewahren wir zusammen mit der Auftragsbestätigung auf (Abschnitt 9a).</p>
|
||
```
|
||
|
||
**Edit 6** · `public/datenschutz.html`
|
||
|
||
`old_string`:
|
||
|
||
```text
|
||
Die Fristen beginnen mit dem Ende des jeweiligen Kalenderjahres.</p>
|
||
|
||
<h2>10. Domains unserer Kunden</h2>
|
||
```
|
||
|
||
`new_string`:
|
||
|
||
```text
|
||
Die Fristen beginnen mit dem Ende des jeweiligen Kalenderjahres.</p>
|
||
|
||
<h2 id="auftraege">9a. Auftragsbestätigungen</h2>
|
||
<p>Wenn Sie uns einen Auftrag erteilen, bestätigen wir ihn mit einer Auftragsbestätigung als PDF-Datei und senden sie Ihnen zusammen mit unseren Allgemeinen Geschäftsbedingungen per E-Mail. Dafür verarbeiten wir Ihren Namen, gegebenenfalls Ihre Firma, Ihre Anschrift, Ihre E-Mail-Adresse und Ihre Kundennummer, die Angabe, ob Sie als Unternehmen oder als Privatperson handeln, und den Inhalt des Auftrags: das Projekt, die vereinbarten Leistungen, Preise oder den Stundensatz, den Zahlungsplan, Termine sowie Datum und Art der Absprache (zum Beispiel telefonisch). Die E-Mail senden wir über den E-Mail-Dienst der Hetzner Online GmbH (Abschnitt 8); eine Kopie geht an das Postfach unseres Teams. Das PDF speichern wir mit einer Prüfsumme auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3), und ändern es nach dem Versand nicht mehr. Ändern wir den Auftrag, senden wir eine neue Auftragsbestätigung; die frühere bleibt als ersetzt gespeichert.</p>
|
||
<p>Die E-Mail enthält einen persönlichen Link zur Seite project.webklar.com/auftrag.html. Dort können Sie den Auftrag ohne Anmeldung bestätigen; im Kundenportal können Sie ihn ebenfalls bestätigen. Der Link enthält ein zufälliges Kennzeichen, von dem wir nur einen Hashwert speichern, und gilt 60 Tage ab dem letzten Versand. Senden wir die Auftragsbestätigung erneut, erhalten Sie einen neuen Link, und der bisherige gilt nicht mehr. Bestätigen Sie den Auftrag, speichern wir den Zeitpunkt, den Weg (Link oder Kundenportal), die Prüfsumme der bestätigten Auftragsbestätigung und die Fassung der angezeigten Texte. Handeln Sie als Privatperson, speichern wir außerdem, ob Sie ausdrücklich verlangt haben, dass wir schon vor Ablauf der Widerrufsfrist beginnen, mit der Fassung dieser Erklärung. Wir speichern dabei weder Ihre IP-Adresse noch Ihre Browserkennung. Sie erhalten eine Eingangsbestätigung per E-Mail, und unser Team erhält eine Nachricht über Ihre Bestätigung. Unsere Mitarbeiter können einen Auftrag nicht in Ihrem Namen bestätigen, auch nicht, wenn sie Ihr Portal so öffnen, wie Sie es sehen (Abschnitt 6). Zum Schutz vor Missbrauch begrenzen wir die Zahl der Aufrufe der Seite und der Bestätigungen je IP-Adresse; die Zeitpunkte der Aufrufe merken wir uns nur im Arbeitsspeicher des Servers, nicht dauerhaft. Im Übrigen gilt für den Aufruf Abschnitt 3.</p>
|
||
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, weil wir die Auftragsbestätigung für die Durchführung des Vertrags mit Ihnen brauchen, und, soweit wir Verbrauchern den Vertrag auf einem dauerhaften Datenträger bestätigen und Unterlagen aufbewahren müssen (§ 312f BGB, § 147 AO), Art. 6 Abs. 1 lit. c DSGVO. Wir bewahren Auftragsbestätigungen wie in Abschnitt 9 beschrieben auf: als Buchungsbeleg acht Jahre, als Handels- oder Geschäftsbrief sechs Jahre; die Fristen beginnen mit dem Ende des jeweiligen Kalenderjahres. Danach löschen wir sie. Entwürfe, die wir nicht versenden, löschen wir, sobald wir sie nicht mehr brauchen. Den Widerruf eines Projektauftrags beschreibt Abschnitt 7.</p>
|
||
|
||
<h2>10. Domains unserer Kunden</h2>
|
||
```
|
||
|
||
- [ ] **Step 4: Lauf, PASS erwartet** (bis auf den PDF-Pin, der den Neudruck verlangt).
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
for f in auftraegeRechtstexte legalTexts; 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
|
||
```
|
||
|
||
Erwartet: `auftraegeRechtstexte` 32 Tests grün; `legalTexts` genau ein roter Test: `pdf_pruefsumme` („public/datenschutz.html wurde geändert, ohne das PDF neu zu drucken“). Jeder andere rote Test ist ein Fehler an der Änderung (ein gepinnter Satz wurde berührt): Änderung prüfen, nicht den Test ändern.
|
||
|
||
- [ ] **Step 5: PDF der Datenschutzerklärung drucken** (Hinweise wie in Task 5: `free -m`, flock, nice).
|
||
|
||
```bash
|
||
free -m
|
||
SP=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
flock "$SP/auftraege-schwer.lock" nice -n 10 scripts/build-legal-pdfs.sh datenschutz
|
||
git status --short
|
||
sha256sum public/datenschutz.html; cat docs/WEBklar-Datenschutzerklaerung.pdf.sha256
|
||
```
|
||
|
||
Erwartet: `-> docs/WEBklar-Datenschutzerklaerung.pdf`, beide Prüfsummen gleich, `git status` zeigt `public/datenschutz.html`, das PDF, die `.sha256`, die zwei Testdateien.
|
||
|
||
- [ ] **Step 6: Alle Tests, die die Datenschutzerklärung lesen.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
for f in auftraegeRechtstexte 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
|
||
```
|
||
|
||
Erwartet: überall `fail 0` (in `legalTexts` ist ein Test übersprungen, wenn der Git-Verlauf `3a0b008` fehlt).
|
||
|
||
- [ ] **Step 7: Commit** (letzter Commit des Strangs, siehe Global Constraints: Der Betreff beginnt mit „Datenschutz: Abschnitt 9a“, damit Z1 den Commit findet und den Branch ohne ihn zusammenführt; nach diesem Commit gibt es keinen weiteren, auch nicht in den Tasks 7 und 8).
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
git add public/datenschutz.html docs/WEBklar-Datenschutzerklaerung.pdf docs/WEBklar-Datenschutzerklaerung.pdf.sha256 test/legalTexts.test.js test/auftraegeRechtstexte.test.js
|
||
git status --short
|
||
git commit -m "Datenschutz: Abschnitt 9a Auftragsbestätigungen, Widerruf von Projektaufträgen, Hinweis in Abschnitt 6, Stand 06.10.2026, PDF neu" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: Freigabe-Dokument für den Nutzer
|
||
|
||
**Files:**
|
||
- Create (außerhalb des Repos, nicht einchecken): `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/rechtsrunde-vorlage.md`, `rechtsrunde-erzeugen.mjs` und `rechtsrunde-quellen.mjs` im selben Ordner
|
||
- Create (außerhalb des Repos): `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md`
|
||
|
||
**Interfaces:**
|
||
- Das Dokument enthält **alle neuen und geänderten Texte im Wortlaut**, aus den Dateien übernommen und nicht abgetippt, je eine kurze Begründung mit Norm und die zwölf offenen Entscheidungen E1 bis E12 mit Empfehlung. Die Begründungen stehen in der Vorlage (diese ist der Text, den der Nutzer liest und freigibt); das Skript setzt nur die Wortlaute ein.
|
||
- **Abschnitte 1 bis 6** kommen aus diesem Branch (`texte.js`, `agb.md`, `datenschutz.html`). **Abschnitt 7** (Beschluss der Planrunde) sammelt die rechtlich gefärbten Sätze der Stränge L5 und L6: die Mails K31 (Privatperson, Erinnerung, Kopie für das Team ohne Link), K32 (gibt `FRUEHBEGINN_TEXT` wörtlich wieder), T22 und die Zeilen des Blocks `projekt` in K04 und T09 sowie alle Sätze der Seiten, die L6 anlegt oder ändert (Zustände der Seite `auftrag.html` wie „Wir beginnen frühestens am …“, Abschnitt „Aufträge“ im Portal, Widerrufsseite mit der Nummer der Auftragsbestätigung „falls zur Hand“). Die Seitentexte teilt das Skript nach Stichwörtern in Sätze mit rechtlichem Bezug (Abschnitt 7.1, nach Gruppen) und übrige (Abschnitt 7.6); nichts wird weggelassen.
|
||
- Das Skript liest L5 und L6 mit `git show` (aus `feature/auftraege`, sobald der Strang dort gemergt ist, sonst aus seinem Branch), rendert die Mails mit denselben Schritten wie `renderMail` (bei jedem Lauf ein Selbsttest gegen `renderMail` mit vier vorhandenen Vorlagen) und nennt Quelle und Stand jedes Strangs im Dokument. Es arbeitet nur, wenn der Worktree dieses Strangs sauber am Ende des Branches steht: das Dokument zeigt nur Eingechecktes.
|
||
- Ende-Codes des Skripts: `0` vollständig; `3` eine Quelle von L5 oder L6 fehlt noch (das Dokument steht, Abschnitt 7 trägt den Vermerk); `4` eine Prüfung der Planrunde-Zusagen ist fehlgeschlagen (Befund im Dokument); `1` Fehler (Meldung auf der Konsole).
|
||
- **Das Skript ist wiederholbar und muss nach dem Zusammenführen der Stränge (Z1) und nach jeder Textänderung erneut laufen** (Vertragsfrage 14).
|
||
|
||
- [ ] **Step 1: Ordner anlegen.**
|
||
|
||
```bash
|
||
mkdir -p /root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug
|
||
ls -la /root/reports/auftraege-rechnungen-2026-10-06/
|
||
```
|
||
|
||
- [ ] **Step 2: Vorlage anlegen.** Datei `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/rechtsrunde-vorlage.md` mit genau diesem Inhalt (die doppelten geschweiften Klammern sind Platzhalter des Skripts, nicht ändern; in Abschnitt 7 steht `{…}` mit einfachen Klammern im Fließtext):
|
||
|
||
```markdown
|
||
# Rechtsrunde „Aufträge und Rechnungen“: Texte zur Freigabe
|
||
|
||
Stand {{HEUTE}} · Fassung der Texte {{FASSUNG}} · Fassung der AGB {{AGB_FASSUNG}} · Branch auftraege/l7-rechtstexte (Stand {{COMMIT}}) · Entwurf zur Freigabe, keine Rechtsberatung
|
||
|
||
Alle neuen und geänderten Texte stehen hier im Wortlaut, je mit kurzer Begründung und Norm. Die Wortlaute sind aus den Dateien übernommen, nicht abgetippt: die Abschnitte 1 bis 6 aus dem Branch auftraege/l7-rechtstexte, Abschnitt 7 aus den Dateien der Stränge L5 (Mails) und L6 (Seiten). Bis zur Freigabe ist die Auftragsbestätigung ausgeschaltet (AUFTRAEGE_ENABLED=off). Eine Ausnahme ist die Datenschutzerklärung: Sie wird mit dem ersten Push auf main sofort öffentlich, deshalb liegt sie im letzten Commit des Branches und bleibt bis zur Freigabe draußen (Abschnitt 8).
|
||
|
||
Nicht Teil dieser Runde sind die Rechnungstexte.
|
||
|
||
Dieses Dokument wird aus den Dateien erzeugt und lässt sich jederzeit neu erzeugen (nach dem Zusammenführen der Stränge und nach jeder Textänderung): `{{BEFEHL}}`
|
||
|
||
## Zu entscheiden
|
||
|
||
| Nr. | Frage | Vorschlag in den Texten | Empfehlung |
|
||
|---|---|---|---|
|
||
| E1 | Welche Norm nennt die Frühbeginn-Erklärung? | § 356 Abs. 5 Nr. 2 BGB (Erlöschen) und § 357a Abs. 2 BGB (Wertersatz), wie bei den Funktionen. Nicht § 356 Abs. 4: Das ist heute die Höchstfrist von zwölf Monaten und 14 Tagen. Geprüft am 06.10.2026 auf gesetze-im-internet.de, Auszüge im Branch. | Übernehmen. |
|
||
| E2 | Häkchen „Auftrag bestätigen“: kurz wie in der Spezifikation oder mit Bezug auf die AGB? | Im Code steht der Wortlaut der Spezifikation. Der Vorschlag mit Bezug auf die AGB (Abschnitt 2) macht ausdrücklich, dass die AGB mit bestätigt werden. | Vorschlag übernehmen; dann ändert der Orchestrator drei Stellen (texte.js, der Wortlaut-Pin im Test der Textbausteine, der Rückfalltext der Bestätigungsseite). |
|
||
| E3 | Wann beginnt die Widerrufsfrist? | Das Muster sagt „ab dem Tag des Vertragsabschlusses“. Die Frist beginnt aber nicht vor der Belehrung (§ 356 Abs. 3 BGB). Das Ticketsystem rechnet „Arbeitsbeginn frühestens“ deshalb ab dem Tag, an dem die Auftragsbestätigung mit der Belehrung gesendet wird. | So lassen, im Text nichts ergänzen. |
|
||
| E4 | Satz zum Beginn bei Privatpersonen in der Auftragsbestätigung | „Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.“ (Abschnitt 1.5). Der PDF-Strang (L4) reicht dafür das Kennzeichen „privat“ an den Baustein durch (Beschluss der Planrunde). | Übernehmen. |
|
||
| E5 | AGB als PDF: Überschrift, Nummerierung, Link | Wortgleich zur Website. Darüber steht die Überschrift „Allgemeine Geschäftsbedingungen“ mit der Fassung. Die Absätze sind mit (1), (2) nummeriert statt als Liste. Der Link auf die Abo-Bedingungen in Nr. 5 ist im PDF kein Link. | Übernehmen. |
|
||
| E6 | Titel der AGB-Seite auf der Website | Die Website nennt die Seite „Kaufvertrag – WEBklar (Modularer Projektvertrag)“. Für Kunden ist das irritierend. | Website später in „Allgemeine Geschäftsbedingungen“ umbenennen; das ist eine neue Fassung mit neuem Datum und neuer PDF-Kopie. |
|
||
| E7 | Datenschutzerklärung: Nummer, Stand, Rechtsgrundlagen | Neuer Abschnitt 9a (keine Umnummerierung; 6e ist für Öffnungszeiten live vorgemerkt). Stand 06.10.2026; bei späterem Livegang auf den Tag der Veröffentlichung setzen. Rechtsgrundlagen Art. 6 Abs. 1 lit. b und c DSGVO, ohne lit. f. | Übernehmen. |
|
||
| E8 | Gefunden außerhalb dieses Plans: Abschnitt 9 der Datenschutzerklärung und Bankabfrage | Abschnitt 9 beschreibt die Rechnungen noch mit der eigenen Buchhaltungssoftware und für Abos über Stripe. Seit dem Finanzen-Umbau (01.10.2026) laufen auch Projektrechnungen über Stripe. Die Abfrage der Bankumsätze (Enable Banking) steht in der Erklärung gar nicht. | Im Finanzen-Strang prüfen und als eigene Fassung ändern; hier nicht angefasst. |
|
||
| E9 | Fragen für die Kanzlei (zusammen mit Q3 der Funktionen) | (a) Ist die Erstellung einer Website für Verbraucher eine Dienst- oder Werkleistung nach § 356 Abs. 5 BGB oder die Herstellung digitaler Inhalte (§ 327 ff. BGB, § 356 Abs. 6)? Im zweiten Fall greift für Verbraucher auch § 650 Abs. 2 BGB: Dann gelten für Mängel und Abnahme nicht §§ 633 bis 640 BGB, sondern die Vorschriften über Verbraucherverträge über digitale Produkte (Abschnitt 3 Titel 2a). (b) Aufwandsaufträge ohne Ende: Das Widerrufsrecht erlischt erst mit vollständiger Erbringung. (c) Braucht die Auftragsbestätigung für Privatpersonen einen Hinweis auf die gesetzlichen Mängelrechte (Art. 246a § 1 Abs. 1 Nr. 11b EGBGB)? (d) AGB Nr. 8 Abs. 2: Die Abnahme nach 14 Tagen gilt gegenüber Verbrauchern nur, wenn bei der Aufforderung zur Abnahme in Textform auf die Folgen hingewiesen wird (§ 640 Abs. 2 Satz 2 BGB); bei Verbraucherverträgen über digitale Inhalte entfällt die Abnahme nach § 650 Abs. 2 BGB ganz (siehe a). (e) Weg „vor Ort“ bei Privatpersonen (außerhalb von Geschäftsräumen geschlossen): § 312f Abs. 1 BGB verlangt die Bestätigung auf Papier, es sei denn, der Verbraucher stimmt einem anderen dauerhaften Datenträger zu. Reicht die E-Mail mit PDF, oder braucht der Dialog einen Vermerk, dass der Kunde der Bestätigung per E-Mail zugestimmt hat? (f) Rechtsnatur der Bestätigung: Die Auftragsbestätigung bestätigt einen schon abgesprochenen Vertrag, der Klick des Kunden dokumentiert das. Die Mail K31 sagt „Bestätigung möglich bis …“ und bittet zu bestätigen. Ist damit klar, dass der Vertrag schon besteht und der Link nur die Bestätigung festhält, oder wirkt die Auftragsbestätigung wie ein Angebot, das der Kunde innerhalb einer Frist annimmt (§ 148 BGB)? | In die nächste Kanzlei-Runde; zu (d) zusätzlich eine Vorlage für die Aufforderung zur Abnahme anlegen; zu (e) bis zur Antwort bei Privatpersonen mit Weg „vor Ort“ die Auftragsbestätigung zusätzlich ausdrucken und übergeben (der Dialog zeigt den Hinweis); zu (f) bleibt der Text bis zur Antwort, wie er ist. |
|
||
| E10 | Fassungsdatum | Texte und Frühbeginn-Erklärung tragen den 06.10.2026. Ändert sich nach der Freigabe ein Wortlaut, bekommt die Fassung den Tag der Freigabe (Anleitung in Abschnitt 8). | Beim Freigeben entscheiden. |
|
||
| E11 | Aufwandsregel ohne geschätzten Umfang | Ohne Schätzung entfällt der Satz „Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.“, weil es nichts zu überschreiten gibt (§ 649 BGB betrifft den Kostenanschlag; am 06.10.2026 ein zweites Mal auf gesetze-im-internet.de geprüft). Mit Schätzung steht er immer da (Abschnitt 1.2). | Übernehmen; wer die Zusage immer will, ändert eine Zeile in texte.js. |
|
||
| E12 | Texte der Kundenseiten und Mails (Abschnitt 7) | Die Sätze stehen in Abschnitt 7 im Wortlaut der Dateien der Stränge L5 (Mails K31, K32, T22 und der Block „projekt“ in K04 und T09) und L6 (Seiten). Sie stehen nicht in texte.js. K32 gibt die Frühbeginn-Erklärung wörtlich wieder, die Kopie der K31 für das Team trägt keinen Link, die Nummer der Auftragsbestätigung auf der Widerrufsseite ist freiwillig (Beschlüsse der Planrunde). | Freigeben oder je Satz den neuen Wortlaut nennen; die Änderung betrifft dann die Dateien der Stränge L5 und L6 und deren Tests, nicht texte.js (Abschnitt 8, Punkt 7). |
|
||
|
||
## 1. Auftragsbestätigung: Textbausteine
|
||
|
||
Die Bausteine ergeben zusammen das PDF der Auftragsbestätigung. Daten, Beträge und Namen sind hier Beispielwerte.
|
||
|
||
### 1.1 Einleitung
|
||
|
||
{{AB_EINLEITUNG}}
|
||
|
||
Begründung: Die Auftragsbestätigung ist die Bestätigung des Vertrags auf einem dauerhaften Datenträger, die der Unternehmer Verbrauchern geben muss (§ 312f Abs. 2 BGB bei Telefon, E-Mail und Chat; Abs. 1 bei Verträgen außerhalb von Geschäftsräumen, etwa „vor Ort“; dort verlangt das Gesetz Papier, solange der Verbraucher nicht einem anderen dauerhaften Datenträger zustimmt, Frage E9 e). Sie nennt Datum und Weg der Absprache und gibt den Inhalt wieder. Ist kein Weg gewählt, entfällt die Klammer.
|
||
|
||
### 1.2 Abrechnung nach Aufwand
|
||
|
||
Mit geschätztem Umfang:
|
||
|
||
{{AB_AUFWAND_MIT}}
|
||
|
||
Ohne Schätzung:
|
||
|
||
{{AB_AUFWAND_OHNE}}
|
||
|
||
Begründung: Bei Aufwandsaufträgen gibt es keinen Gesamtpreis; die Art der Preisberechnung genügt (Art. 246a § 1 Abs. 1 Nr. 5 EGBGB). Rundung und Unterstützung entsprechen AGB Nr. 9 Abs. 2. Die Schätzung ist unverbindlich, wie ein Kostenanschlag ohne Gewähr (§ 649 Abs. 1 BGB). Die Zusage „melden wir uns vorher“ geht über die Pflicht zur unverzüglichen Anzeige bei zu erwartender Überschreitung hinaus; § 649 Abs. 2 BGB lautet: „Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.“ Geprüft am 06.10.2026 auf gesetze-im-internet.de (zweiter Abruf): § 649 BGB trägt die Überschrift „Kostenanschlag“; § 650 BGB ist der Werklieferungsvertrag und der Verbrauchervertrag über digitale Produkte, nicht der Kostenanschlag (Auszüge im Branch: docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt). Ohne Schätzung entfällt der Satz, weil es nichts zu überschreiten gibt (Entscheidung E11).
|
||
|
||
### 1.3 Kleinunternehmer
|
||
|
||
{{AB_KLEIN}}
|
||
|
||
Begründung: § 19 UStG. Derselbe Satz steht auf den Rechnungen und in den Abo-Bedingungen; der Preis ist damit für Verbraucher ein Endpreis (Art. 246a § 1 Abs. 1 Nr. 5 EGBGB).
|
||
|
||
### 1.4 Zahlungspläne
|
||
|
||
Alles nach Abnahme:
|
||
|
||
{{AB_ZP_NACH_ABNAHME}}
|
||
|
||
Anzahlung und Rest:
|
||
|
||
{{AB_ZP_ANZAHLUNG}}
|
||
|
||
Monatlich nach Aufwand:
|
||
|
||
{{AB_ZP_MONATLICH}}
|
||
|
||
Frei (der Text kommt aus dem Dialog, höchstens 500 Zeichen):
|
||
|
||
{{AB_ZP_FREI}}
|
||
|
||
Begründung: Zahlungsbedingungen gehören zu den Pflichtangaben (Art. 246a § 1 Abs. 1 Nr. 10 EGBGB). Die Fälligkeit folgt der Vereinbarung (AGB Nr. 9 Abs. 3). „Nach Rechnungsdatum“ entspricht dem Fälligkeitsdatum der Stripe-Rechnung. Das Zahlungsziel „0 Tage“ heißt „sofort nach Rechnungsstellung“.
|
||
|
||
### 1.5 Termine
|
||
|
||
{{AB_TERMINE}}
|
||
|
||
Für Privatpersonen kommt ein Satz dazu:
|
||
|
||
{{AB_TERMINE_PRIVAT}}
|
||
|
||
Begründung: Beginn und Fertigstellung sind freiwillige Angaben und unverbindlich („voraussichtlich“). Der Satz für Privatpersonen folgt aus § 356 Abs. 5 Nr. 2 und § 357a Abs. 2 BGB: Vor Ablauf der Widerrufsfrist beginnen wir nur auf ausdrückliches Verlangen (Entscheidung E4).
|
||
|
||
### 1.6 Mitwirkung und Abnahme
|
||
|
||
{{AB_MITWIRKUNG}}
|
||
|
||
Begründung: Verweis auf AGB Nr. 7 und 8. Die Abnahmeregel aus Nr. 8 Abs. 2 wird nicht wiederholt, weil sie gegenüber Verbrauchern besondere Voraussetzungen hat (Frage E9 d).
|
||
|
||
### 1.7 Hinweis auf die AGB
|
||
|
||
{{AB_AGB}}
|
||
|
||
Begründung: Die AGB werden ausdrücklich genannt und liegen der E-Mail als PDF bei; auf der Bestätigungsseite sind sie verlinkt. So können Verbraucher in zumutbarer Weise Kenntnis nehmen (§ 305 Abs. 2 BGB). Das Datum der Fassung steht im Satz und im Nachweis der Bestätigung.
|
||
|
||
### 1.8 Hinweis bei einer neuen Fassung
|
||
|
||
{{AB_ERSETZT}}
|
||
|
||
### 1.9 Gruß
|
||
|
||
{{AB_GRUSS}}
|
||
|
||
## 2. Häkchen „Auftrag bestätigen“
|
||
|
||
Im Code (Wortlaut der Spezifikation, unverändert):
|
||
|
||
{{BESTAETIGUNG}}
|
||
|
||
Vorschlag zur Entscheidung E2, noch nicht im Code:
|
||
|
||
> Ich bestätige den Auftrag zu den Bedingungen dieser Auftragsbestätigung und der Allgemeinen Geschäftsbedingungen, auf die sie verweist.
|
||
|
||
Wo: Bestätigungsseite (project.webklar.com/auftrag.html) und Kundenportal, für alle Kunden.
|
||
|
||
Begründung: Das Häkchen ist die Erklärung des Kunden, die wir als Bestätigung protokollieren (Zeitpunkt, Weg, Prüfsumme des bestätigten PDFs, Fassung der Texte). Die kurze Fassung lässt offen, ob „diese Bedingungen“ die AGB einschließen; die Auftragsbestätigung nennt sie zwar und die Seite verlinkt sie, doch der Vorschlag macht die Geltung der AGB (§ 305 Abs. 2 BGB bei Verbrauchern, § 310 Abs. 1 BGB bei Unternehmen) im Häkchen selbst ausdrücklich.
|
||
|
||
## 3. Frühbeginn-Erklärung (nur Privatpersonen, freiwillig)
|
||
|
||
{{FRUEHBEGINN}}
|
||
|
||
Wo: zweites Häkchen auf der Bestätigungsseite und im Kundenportal, nur bei Kundenart Privatperson. Die Eingangsbestätigung K32 gibt die Erklärung wörtlich wieder (Abschnitt 7.3), damit sie auf einem dauerhaften Datenträger vorliegt; ein Satz wie „Ihr Widerrufsrecht bleibt davon unberührt“ steht dort nicht.
|
||
|
||
Normen:
|
||
|
||
- Verlangen (§ 357a Abs. 2 Nr. 1 BGB) und ausdrückliche Zustimmung zum Beginn vor Fristablauf (§ 356 Abs. 5 Nr. 2 Buchstabe a BGB): der erste Satz.
|
||
- Kenntnis, dass das Widerrufsrecht mit vollständiger Vertragserfüllung erlischt (§ 356 Abs. 5 Nr. 2 Buchstabe c BGB): der zweite Satz. Er dient zugleich der Information über die Umstände des vorzeitigen Erlöschens (Art. 246a § 1 Abs. 3 Nr. 2 EGBGB); ob das genügt, gehört zur Kanzlei-Frage Q3 der Funktionen.
|
||
- Wertersatz für die bis zum Widerruf erbrachte Leistung (§ 357a Abs. 2 BGB): der dritte Satz, ergänzt durch den Absatz „Haben Sie verlangt …“ der Belehrung.
|
||
- Dauerhafter Datenträger (§ 356 Abs. 5 Nr. 2 Buchstabe b und § 357a Abs. 2 Nr. 2 BGB, bei außerhalb von Geschäftsräumen geschlossenen Verträgen, etwa „vor Ort“): K32 mit dem Wortlaut und dem Zeitpunkt.
|
||
|
||
Warum § 356 Abs. 5 Nr. 2 und nicht Abs. 4: Abs. 4 regelt heute die Höchstfrist von zwölf Monaten und 14 Tagen. Das Erlöschen bei Dienstleistungen steht in Abs. 5. Nr. 1 gilt nur für unentgeltliche Verträge, Nr. 2 für Verträge mit Preis. Abs. 6 betrifft die Bereitstellung digitaler Inhalte und passt nicht zu einem Projektauftrag (offene Frage E9 a). Der Wortlaut ist derselbe wie bei News, Terminplaner, Webshop und Öffnungszeiten live, nur der Gegenstand ist „die Ausführung dieses Projektauftrags“.
|
||
|
||
## 4. Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge)
|
||
|
||
Die Belehrung liegt dem PDF der Auftragsbestätigung bei, wenn der Kunde eine Privatperson ist, und steht als eigene Seite und PDF unter public/rechtliches/widerrufsbelehrung-projektauftrag-{{FASSUNG}}.html und .pdf.
|
||
|
||
{{BELEHRUNG}}
|
||
|
||
{{FORMULAR}}
|
||
|
||
Begründung: Das ist das gesetzliche Muster der Anlagen 1 und 2 für einen Dienstleistungsvertrag. Wer es zutreffend ausgefüllt in Textform übermittelt, erfüllt die Informationspflicht (Art. 246a § 1 Abs. 2 Satz 2 EGBGB). Ausgefüllt sind nur die Lücken: Frist „des Vertragsabschlusses.“ (Textbaustein a für Dienstleistungsverträge), Name, Anschrift, Telefon und E-Mail des Unternehmers, die Widerrufsfunktion mit der Adresse der Widerrufsseite (§ 356a BGB) und der Wertersatz für Dienstleistungen. Die Bausteine für Waren sind weggelassen. Ein Test vergleicht jeden Absatz mit dem Gesetzestext der Anlagen und mit der Belehrung, die Kunden bei der Bestellung einer Funktion sehen: Beide sind wörtlich gleich. Zur Frist siehe Entscheidung E3.
|
||
|
||
## 5. AGB als PDF
|
||
|
||
Die AGB werden als versionierte Kopie der Website-AGB (Fassung {{AGB_FASSUNG}}) in die Mail der Auftragsbestätigung gehängt und auf der Bestätigungsseite verlinkt. Der Text ist wortgleich mit der Seite webklar.com/agb; ein Test vergleicht den Klartext der Kopie mit dem Quelltext der Website und hält ihn über eine Prüfsumme fest. Die Fassung {{AGB_FASSUNG}} ist der Tag der letzten Textänderung der Website-AGB (Nr. 9 Zeiterfassung und Unterstützung). Die späteren Änderungen der Website betrafen nur Layout und Seitentitel.
|
||
|
||
Abweichungen von der Seite sind nur diese (Entscheidung E5): Überschrift „Allgemeine Geschäftsbedingungen“ und Fassungszeile über dem Titelblock der Website, Absatznummern (1), (2) statt Listenzeichen, Link auf die Abo-Bedingungen in Nr. 5 ohne Link.
|
||
|
||
Der vollständige Text der Kopie:
|
||
|
||
{{AGB_TEXT}}
|
||
|
||
## 6. Datenschutzerklärung
|
||
|
||
Stand der Erklärung: vorher {{DS_STAND_ALT}}, nachher {{DS_STAND_NEU}}.
|
||
|
||
### 6.1 Neu: Abschnitt 9a „Auftragsbestätigungen“
|
||
|
||
{{DS_9A}}
|
||
|
||
Begründung: Transparenz nach Art. 13 DSGVO für die neue Verarbeitung. Der Abschnitt nennt die Daten, den Weg (E-Mail, Link, Portal), den Bestätigungslink nur als Hashwert, den Verzicht auf IP-Adresse und Browserkennung (Datenminimierung), die Rechtsgrundlagen (lit. b Vertrag, lit. c rechtliche Pflicht: § 312f BGB, § 147 AO) und die Aufbewahrung wie in Abschnitt 9: acht Jahre als Buchungsbeleg, sechs Jahre als Geschäftsbrief (§ 147 Abs. 1 Nr. 2 bis 4 und Abs. 3 AO), gerechnet ab Ende des Kalenderjahres (§ 147 Abs. 4 AO). Eine Ratenbegrenzung je IP-Adresse wird genannt, aber ohne Zahlen, damit der Text nicht von einer Einstellung abhängt.
|
||
|
||
### 6.2 Geändert: Abschnitt 6 (Aufbewahrung)
|
||
|
||
Neu oder geändert (der übrige Absatz bleibt):
|
||
|
||
{{DS_6_NEU}}
|
||
|
||
Ersetzt, stand vorher so da:
|
||
|
||
{{DS_6_ALT}}
|
||
|
||
### 6.3 Geändert: Abschnitt 7 (Kündigungs- und Widerrufsseite)
|
||
|
||
Neu oder geändert (der übrige Absatz bleibt):
|
||
|
||
{{DS_WIDERRUF_NEU}}
|
||
|
||
Ersetzt, stand vorher so da:
|
||
|
||
{{DS_WIDERRUF_ALT}}
|
||
|
||
Begründung: Die Seite widerrufen.html nimmt künftig auch den Widerruf eines Projektauftrags an (E-Mail-Adresse und die freiwillige Nummer der Auftragsbestätigung, Beschluss der Planrunde). Daten, Zuordnung und Aufbewahrung sind ergänzt; die übrigen Sätze sind unverändert.
|
||
|
||
## 7. Kundenseiten und Mails
|
||
|
||
Dieser Abschnitt zeigt, was Kunden auf den Seiten und in den Mails zum Auftrag lesen. Er wird beim Erzeugen aus den Dateien der Stränge L6 (Seiten) und L5 (Mails) gelesen, nicht abgetippt. Mails, PDF und die öffentliche Bestätigungsseite stehen in der Sie-Form, der Abschnitt „Aufträge“ im angemeldeten Kundenportal in der Du-Form wie das übrige Portal (Beschluss der Planrunde). In den Seitentexten steht {…} für einen Wert, der erst beim Anzeigen eingesetzt wird (Datum, Nummer, Betrag).
|
||
|
||
Quellen und Prüfungen beim Erzeugen:
|
||
|
||
{{STAND_L5_L6}}
|
||
|
||
### 7.1 Seitentexte mit rechtlichem Bezug
|
||
|
||
Öffentliche Bestätigungsseite (auftrag.html), Abschnitt „Aufträge“ im Kundenportal und Widerrufsseite. Die Spalte „Gruppe“ ordnet jeden Satz einem Thema zu; ein Satz ohne Treffer steht in Abschnitt 7.6.
|
||
|
||
{{SEITEN_RECHTLICH}}
|
||
|
||
Begründung nach Gruppen:
|
||
|
||
- Beginn der Arbeit: Ohne ausdrückliches Verlangen darf die Arbeit nicht vor Ablauf der Widerrufsfrist beginnen, sonst gibt es bei einem Widerruf keinen Wertersatz (§ 356 Abs. 5 Nr. 2, § 357a Abs. 2 BGB). „Frühestens am …“ ist der Tag nach Fristende, gerechnet ab dem Versand der Auftragsbestätigung mit der Belehrung (Entscheidung E3). Mit Verlangen sagt die Seite, dass vor Fristablauf begonnen wird.
|
||
- Erklärung des Kunden: die beiden Häkchen und die Hinweise zur Bestätigung. Die Wortlaute der Häkchen stehen in den Abschnitten 2 und 3 (aus texte.js); das zweite erscheint nur bei Privatpersonen und ist freiwillig.
|
||
- Speicherung und Datenschutz: der Hinweis, dass die Bestätigung mit Datum und Uhrzeit gespeichert wird, und die Links zu Datenschutz und Impressum. Die öffentliche Seite ist ohne Anmeldung erreichbar und verarbeitet Daten, deshalb gehören Datenschutz und Impressum in die Fußzeile (Art. 13 DSGVO, § 5 DDG). Abschnitt 6.1 nennt dieselben Daten.
|
||
- AGB, Preis und Steuer: Die Zusammenfassung zeigt vor dem Klick, was bestätigt wird (Positionen, Preis, Zahlungsplan, Termine), verlinkt die AGB und nennt den Kleinunternehmer-Satz wie das PDF (§ 305 Abs. 2 BGB: zumutbare Kenntnisnahme der AGB; § 19 UStG).
|
||
- Zustand des Links: Meldungen bei ungültigem, abgelaufenem, ersetztem, storniertem oder widerrufenem Link. Sie haben keine rechtliche Wirkung, sie verhindern aber, dass eine überholte Fassung bestätigt wird (Beweiswert der Bestätigung).
|
||
- Kundenansicht: Im Ansichtsmodus ist das Bestätigen gesperrt, weil es eine Erklärung im Namen des Kunden wäre.
|
||
- Kontakt: Bei jeder Fehlermeldung steht der Kontaktweg, damit niemand ohne Hilfe bleibt.
|
||
- Widerruf und Nummer der Auftragsbestätigung (Widerrufsseite): Die Vertragsart „Projektauftrag“ nimmt den Widerruf einer Privatperson an. Der Widerruf ist formfrei, aus der Erklärung muss nur der Entschluss zum Widerruf eindeutig hervorgehen (§ 355 Abs. 1 Satz 3 BGB). Die Nummer der Auftragsbestätigung ist deshalb freiwillig („falls zur Hand“): Name und E-Mail-Adresse reichen zur Zuordnung, und die Widerrufsfunktion soll Angaben zur Identifizierung des Vertrags ermöglichen, nicht erzwingen (§ 356a Abs. 2 Nr. 2 BGB). Ohne passende Nummer geht dieselbe Eingangsbestätigung heraus, das Team ordnet von Hand zu.
|
||
|
||
### 7.2 Mails an Kunden: K31 Auftragsbestätigung
|
||
|
||
Privatperson, Festpreis (mit den Anhängen Auftragsbestätigung und AGB als PDF):
|
||
|
||
{{MAIL_K31_PRIVAT}}
|
||
|
||
Unternehmen, nach Aufwand, als Erinnerung:
|
||
|
||
{{MAIL_K31_ERINNERUNG}}
|
||
|
||
Kopie für das Team (ohne Link, ohne Anhänge):
|
||
|
||
{{MAIL_K31_TEAM}}
|
||
|
||
Begründung: K31 ist die Bestätigung des Vertrags auf einem dauerhaften Datenträger (§ 312f BGB) mit der Auftragsbestätigung und den AGB als PDF-Anhang; sie bittet, den Auftrag zu bestätigen. „Bestätigung möglich bis …“ nennt die Gültigkeit des Links (60 Tage), keine Frist für den Vertrag (Frage E9 f). Der Hinweis für Privatpersonen fasst zusammen: Widerrufsrecht und Muster-Widerrufsformular stehen in der angehängten Auftragsbestätigung (Art. 246a § 1 Abs. 2 Satz 2 EGBGB, Abschnitt 4), mit der Arbeit beginnen wir erst nach Ablauf der Widerrufsfrist (§ 356 Abs. 5 Nr. 2 BGB). Bei einer Erinnerung gilt der neue Link, der frühere verliert seine Gültigkeit. Die Kopie für das Team trägt keinen Link: Wer den Link hat, kann bestätigen, und das Team soll nicht versehentlich im Namen eines Kunden bestätigen können.
|
||
|
||
### 7.3 Mails an Kunden: K32 Auftrag bestätigt
|
||
|
||
Privatperson mit Verlangen eines früheren Beginns:
|
||
|
||
{{MAIL_K32_FRUEHBEGINN}}
|
||
|
||
Privatperson ohne Verlangen:
|
||
|
||
{{MAIL_K32_OHNE}}
|
||
|
||
Unternehmen (keiner der beiden Blöcke):
|
||
|
||
{{MAIL_K32_FIRMA}}
|
||
|
||
Begründung: K32 bestätigt den Eingang der Bestätigung mit Datum und Uhrzeit. Bei einer Privatperson mit Verlangen gibt sie die Erklärung aus Abschnitt 3 wörtlich wieder: So liegt die Erklärung auf einem dauerhaften Datenträger vor (§ 356 Abs. 5 Nr. 2 Buchstabe b und § 357a Abs. 2 Nr. 2 BGB, bei außerhalb von Geschäftsräumen geschlossenen Verträgen). Der frühere Satz „Ihr Widerrufsrecht bleibt davon unberührt“ entfällt (Beschluss der Planrunde): Er war verkürzt, denn das Recht erlischt mit der vollständigen Erbringung der Leistung (§ 356 Abs. 5 Nr. 2 BGB), und bis dahin schuldet der Kunde bei einem Widerruf Wertersatz (§ 357a Abs. 2 BGB); beides steht im Wortlaut der Erklärung. Ohne Verlangen nennt die Mail den frühesten Beginn.
|
||
|
||
### 7.4 Mails ans Team: T22 Auftrag bestätigt
|
||
|
||
Privatperson mit Verlangen eines früheren Beginns:
|
||
|
||
{{MAIL_T22_FRUEHBEGINN}}
|
||
|
||
Privatperson ohne Verlangen:
|
||
|
||
{{MAIL_T22_OHNE}}
|
||
|
||
Unternehmen:
|
||
|
||
{{MAIL_T22_FIRMA}}
|
||
|
||
Begründung: T22 geht nur ans Team und ist kein Kundentext. Der Hinweis „Wertersatz ist zu prüfen“ erinnert daran, dass Wertersatz nur nach ausdrücklichem Verlangen geschuldet ist (§ 357a Abs. 2 BGB). Der Betreff beginnt mit „[Aufträge]“ wie die übrigen Team-Mails; die Mail enthält keinen Bestätigungslink.
|
||
|
||
### 7.5 Widerruf eines Projektauftrags: K04 und T09
|
||
|
||
Die Widerrufsseite verwendet für den Widerruf eines Projektauftrags die vorhandenen Mails K04 (Eingangsbestätigung an den Kunden) und T09 (Hinweis ans Team). Neu ist der Block „projekt“:
|
||
|
||
K04 Eingang Widerruf:
|
||
|
||
{{MAIL_K04_PROJEKT}}
|
||
|
||
T09 Widerruf eingegangen:
|
||
|
||
{{MAIL_T09_PROJEKT}}
|
||
|
||
Begründung: K04 ist die Eingangsbestätigung der Widerrufsfunktion auf einem dauerhaften Datenträger mit dem Inhalt der Erklärung sowie Datum und Uhrzeit des Eingangs (§ 356a Abs. 4 BGB). Der Block „projekt“ benennt die Vertragsart (Projektauftrag mit Nummer der Auftragsbestätigung); bei Unternehmen enthält der Block laut Vertrag V6 den Hinweis, dass kein gesetzliches Widerrufsrecht besteht, denn § 355 BGB gilt nur für Verbraucher. T09 unterrichtet das Team, nennt Frist und Prüfhinweise; Erstattung und Wertersatz bleiben Handarbeit.
|
||
|
||
### 7.6 Übrige Seitentexte (ohne rechtlichen Bezug)
|
||
|
||
Zur Vollständigkeit: alle weiteren Texte der Seiten, die kein Stichwort aus Abschnitt 7.1 trifft (Beschriftungen, Ladehinweise, Knöpfe).
|
||
|
||
{{SEITEN_UEBRIGE}}
|
||
|
||
### 7.7 Außerhalb dieses Plans gefunden
|
||
|
||
- Datenschutzerklärung Abschnitt 9 und die Bankabfrage: siehe E8.
|
||
- Belehrung und Frühbeginn-Erklärung erscheinen nur bei der Kundenart „Privatperson“. Die Kundenart wählt der Admin im Dialog der Auftragsbestätigung (Pflicht vor dem Senden). Empfehlung: im Zweifel „Privatperson“ wählen, dann gelten Belehrung und Frist für den Kunden.
|
||
- Löschlauf: Auftragsbestätigungen werden nach sechs bzw. acht Jahren gelöscht (Abschnitt 9a). Einen Löschlauf gibt es dafür noch nicht; die erste Frist läuft frühestens Ende 2032 ab.
|
||
|
||
## 8. Nach der Freigabe
|
||
|
||
1. Entscheidungen E1 bis E12 per Chat melden. Texte, die geändert werden sollen, mit dem neuen Wortlaut nennen.
|
||
2. Änderungen im Branch auftraege/l7-rechtstexte einarbeiten, Tests laufen lassen, PDFs neu drucken.
|
||
3. Fassungsdatum: Ändert sich ein Wortlaut, bekommt TEXTE_FASSUNG und FRUEHBEGINN_FASSUNG (server/services/auftraege/texte.js) den Tag der Freigabe. Dann node scripts/build-legal-html.mjs auftraege und scripts/build-legal-pdfs.sh auftraege ausführen. Die Dateien der früheren Fassung bleiben liegen, sobald sie ausgeliefert wurden. Ändern sich die AGB der Website, entsteht dafür eine neue Fassung (AGB_FASSUNG, agb.md, Seite, PDF, Prüfsumme im Test).
|
||
4. Datenschutzerklärung: Sie ist sofort öffentlich, sobald main gepusht wird. Der Datenschutz-Commit ist deshalb der letzte Commit des Branches auftraege/l7-rechtstexte (Betreff „Datenschutz: Abschnitt 9a …“), und beim Zusammenführen in feature/auftraege bleibt er draußen (`git merge --no-ff auftraege/l7-rechtstexte~1`). Er kommt erst zum Livegang der Auftragsbestätigung nach der Freigabe dazu: `git merge --no-ff auftraege/l7-rechtstexte`, dann pushen. Der Stand (06.10.2026) wird dabei auf den Tag der Veröffentlichung gesetzt (eine Zeile in public/datenschutz.html, eine im Test, PDF neu drucken mit scripts/build-legal-pdfs.sh datenschutz). Beim Zusammenführen mit dem Zweig der Öffnungszeiten ändern beide Zweige dieselbe Stand-Zeile: den späteren Tag nehmen.
|
||
5. Claude lädt die freigegebenen Fassungen in WOMS unter Rechtliches hoch (Widerrufsbelehrung Projektaufträge, AGB, Datenschutzerklärung). Dafür braucht Claude den kurzlebigen Schlüssel.
|
||
6. Danach AUFTRAEGE_ENABLED=pilot mit dem Testkunden, Probe-Auftragsbestätigung, dann on.
|
||
7. Wortlaute aus Abschnitt 7 ändern: Mailtexte stehen in server/mail-templates (Strang L5), Seitentexte in public/auftrag-ansicht.js und den übrigen Dateien public/auftrag*.js, auftrag.html, widerrufen.html und dashboard.html (Strang L6). Die Tests dieser Stränge halten die Wortlaute fest und werden mit angepasst. Danach dieses Dokument neu erzeugen (Befehl am Anfang).
|
||
|
||
## 9. WOMS → Rechtliches: Karten und Dateien
|
||
|
||
Nach der Freigabe lädt Claude diese Dateien als neue Versionen unter „Rechtliches“ hoch. Karten, die es schon gibt, bekommen nur eine neue Version.
|
||
|
||
| Karte (Schlüssel) | Titel | Version | Datei im Branch | liveUrl |
|
||
|---|---|---|---|---|
|
||
| widerrufsbelehrung-projekt (neu) | Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge) | {{FASSUNG}} | public/rechtliches/widerrufsbelehrung-projektauftrag-{{FASSUNG}}.pdf | https://project.webklar.com/rechtliches/widerrufsbelehrung-projektauftrag-{{FASSUNG}}.html (mit Fassung; eine Adresse ohne Fassung gibt es nicht, bei jeder neuen Fassung anpassen) |
|
||
| agb (besteht) | AGB | {{AGB_FASSUNG}} | public/rechtliches/agb-{{AGB_FASSUNG}}.pdf | https://webklar.com/agb (besteht) |
|
||
| datenschutz (besteht) | Datenschutzerklärung (Website und Portal) | {{DS_VERSION}} | docs/WEBklar-Datenschutzerklaerung.pdf | https://project.webklar.com/datenschutz.html (besteht) |
|
||
|
||
Keine Karte für die Textbausteine der Auftragsbestätigung: Sie stehen im PDF jeder Auftragsbestätigung (unveränderlich, mit Prüfsumme). Das Häkchen und die Frühbeginn-Erklärung gehören in die Eingangsbestätigung K32 (siehe Abschnitt 3). Ebenso keine Karte für die Texte der Kundenseiten und Mails (Abschnitt 7): Sie stehen in den Dateien der Stränge L5 und L6. Wer trotzdem eine Karte möchte, lädt die Abschnitte 1 bis 3 dieses Dokuments als PDF hoch.
|
||
```
|
||
|
||
- [ ] **Step 3: Hilfsmodul anlegen.** Datei `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/rechtsrunde-quellen.mjs` mit genau diesem Inhalt (Git lesen, Mails rendern, Texte aus JavaScript und HTML ziehen, Selbsttest):
|
||
|
||
```js
|
||
// Hilfen für rechtsrunde-erzeugen.mjs (Strang L7, Task 7): die Texte der Stränge L5 (Mails) und L6 (Seiten) aus Git lesen.
|
||
// Nur lesen (git show, ls-tree, diff, merge-base); geschrieben wird hier nichts. Selbsttests laufen im Speicher.
|
||
import { execFileSync, spawnSync } from 'node:child_process'
|
||
import assert from 'node:assert/strict'
|
||
|
||
/** Leerraum zusammenfassen (Vergleich von Texten). */
|
||
export const norm = (text) => String(text).replace(/\s+/g, ' ').trim()
|
||
|
||
// ---------------------------------------------------------------------------------------------------------- Git
|
||
|
||
export function git(wurzel, ...args) {
|
||
return execFileSync('git', ['-C', wurzel, ...args], { encoding: 'utf8', maxBuffer: 1 << 26 }).trim()
|
||
}
|
||
|
||
function gitOderNull(wurzel, ...args) {
|
||
const lauf = spawnSync('git', ['-C', wurzel, ...args], { encoding: 'utf8', maxBuffer: 1 << 26 })
|
||
return lauf.status === 0 ? lauf.stdout : null
|
||
}
|
||
|
||
export const refGibt = (wurzel, ref) => gitOderNull(wurzel, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`) !== null
|
||
const istVorfahr = (wurzel, a, b) => spawnSync('git', ['-C', wurzel, 'merge-base', '--is-ancestor', a, b]).status === 0
|
||
|
||
/** Inhalt einer Datei in einem Stand (Branch oder Commit); null, wenn es sie dort nicht gibt. */
|
||
export const dateiAus = (wurzel, ref, pfad) => gitOderNull(wurzel, 'show', `${ref}:${pfad}`)
|
||
|
||
/**
|
||
* Stand, aus dem der Text eines Strangs gelesen wird: der Integrationszweig, sobald er den Strang enthält (dort stehen spätere
|
||
* Korrekturen), sonst der Branch des Strangs. null, wenn es den Branch noch nicht gibt.
|
||
*/
|
||
export function quelleVon(wurzel, strangBranch, integration = 'feature/auftraege') {
|
||
if (!refGibt(wurzel, strangBranch)) return null
|
||
const ref = refGibt(wurzel, integration) && istVorfahr(wurzel, strangBranch, integration) ? integration : strangBranch
|
||
return { ref, branch: strangBranch, sha: git(wurzel, 'rev-parse', '--short', ref), inIntegration: ref === integration }
|
||
}
|
||
|
||
/** Quelltext der Mailvorlage `id` (zum Beispiel 'K31') in einem Stand; null, wenn es sie dort nicht gibt. */
|
||
export function vorlageAus(wurzel, ref, id) {
|
||
const liste = gitOderNull(wurzel, 'ls-tree', '-z', '--name-only', ref, 'server/mail-templates/')
|
||
if (liste === null) return null
|
||
const pfad = liste.split('\0').find((name) => name.startsWith(`server/mail-templates/${id} `) && name.endsWith('.html'))
|
||
return pfad ? { pfad, quelltext: dateiAus(wurzel, ref, pfad) } : null
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------- Mailvorlagen rendern
|
||
|
||
const DESIGN_BASE_URL = 'https://project.webklar.com' // wie in server/services/mailTemplates.js
|
||
|
||
/** <!-- WENN x --> ... <!-- ENDE x -->: Inhalt behalten oder ganz entfernen (wie renderMail). */
|
||
function angewandteBloecke(html, bloecke) {
|
||
const block = /<!-- WENN (\w+) -->\n?([\s\S]*?)<!-- ENDE \1 -->\n?/g
|
||
let aus = html
|
||
for (let i = 0; i < 10 && aus.includes('<!-- WENN '); i += 1) aus = aus.replace(block, (_, name, inner) => (bloecke[name] ? inner : ''))
|
||
return aus
|
||
}
|
||
|
||
const kopfFeld = (kopf, label) => kopf.match(new RegExp(`^\\s*${label}[^:\\n]*:[ \\t]*(.*)$`, 'm'))?.[1]?.trim() || ''
|
||
const namenListe = (zeile) => String(zeile || '').split(',').map((s) => s.trim().replace(/^\{\{|\}\}$/g, '')).filter(Boolean)
|
||
|
||
/** Kopfkommentar einer Vorlage auswerten (Betreff, Platzhalter, Blöcke). */
|
||
export function leseVorlage(quelltext) {
|
||
const kopf = quelltext.match(/<!--[ \t]*\n\s*([GKT]\d{2}) ·[\s\S]*?-->\n?/)
|
||
if (!kopf) throw new Error('Kopfkommentar der Mailvorlage fehlt')
|
||
return {
|
||
id: kopf[1],
|
||
betreff: kopfFeld(kopf[0], 'Betreff'),
|
||
platzhalter: namenListe(kopfFeld(kopf[0], 'Platzhalter')),
|
||
bloecke: namenListe(kopfFeld(kopf[0], 'Optionale Blöcke')),
|
||
body: quelltext.replace(kopf[0], ''),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Rendert eine Vorlage aus ihrem Quelltext wie renderMail aus server/services/mailTemplates.js (dieselben Schritte, dieselbe Textfassung),
|
||
* aber ohne die Vorlage im Verzeichnis des Worktrees zu brauchen. `werkzeug` = { htmlToText, escapeHtml, publicBaseUrl }.
|
||
* Fehlt zu einem Platzhalter der Vorlage ein Wert, steht ‹name› im Text; `probleme` nennt unbekannte Platzhalter und Blöcke.
|
||
*/
|
||
export function renderVorlage(quelltext, werte, bloecke, werkzeug) {
|
||
const vorlage = leseVorlage(quelltext)
|
||
const probleme = []
|
||
for (const name of Object.keys(werte)) if (!vorlage.platzhalter.includes(name)) probleme.push(`unbekannter Platzhalter {{${name}}}`)
|
||
for (const name of Object.keys(bloecke)) if (!vorlage.bloecke.includes(name)) probleme.push(`unbekannter Block „${name}“`)
|
||
const wert = (name) => {
|
||
if (!(name in werte)) return `‹${name}›`
|
||
return werte[name] === null || werte[name] === undefined ? '' : String(werte[name])
|
||
}
|
||
let html = angewandteBloecke(vorlage.body, bloecke)
|
||
.replace(/<!--(?!\[if)[\s\S]*?-->\n?/g, '')
|
||
.replaceAll(DESIGN_BASE_URL, werkzeug.publicBaseUrl)
|
||
html = html
|
||
.replace(/\{\{(\w+)\}\}/g, (_, name) => werkzeug.escapeHtml(wert(name)).replace(/\r?\n/g, '<br>'))
|
||
.replace(/Guten Tag ,/g, 'Guten Tag,')
|
||
const betreff = vorlage.betreff.replace(/\{\{(\w+)\}\}/g, (_, name) => wert(name)).replace(/\s+/g, ' ').trim()
|
||
return { id: vorlage.id, betreff, text: werkzeug.htmlToText(html), probleme, vorlage }
|
||
}
|
||
|
||
/** Zeilen, die im Text `mit` vorkommen, aber nicht im Text `ohne` (jede Zeile so oft, wie sie mehr vorkommt). */
|
||
export function zeilenMehr(ohne, mit) {
|
||
const rest = new Map()
|
||
for (const zeile of ohne.split('\n')) rest.set(zeile, (rest.get(zeile) ?? 0) + 1)
|
||
const mehr = []
|
||
for (const zeile of mit.split('\n')) {
|
||
if ((rest.get(zeile) ?? 0) > 0) rest.set(zeile, rest.get(zeile) - 1)
|
||
else if (zeile.trim() !== '') mehr.push(zeile)
|
||
}
|
||
return mehr
|
||
}
|
||
|
||
/** Namen der Blöcke, die in der Vorlage innerhalb des Blocks `name` stehen. */
|
||
export function innereBloecke(quelltext, name) {
|
||
const a = quelltext.indexOf(`<!-- WENN ${name} -->`)
|
||
const b = quelltext.indexOf(`<!-- ENDE ${name} -->`)
|
||
if (a < 0 || b < a) return []
|
||
return [...quelltext.slice(a + 1, b).matchAll(/<!-- WENN (\w+) -->/g)].map((m) => m[1])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------- Texte aus JavaScript und HTML
|
||
|
||
const ESCAPES = { n: '\n', t: '\t', r: '', b: '', f: '', v: '', 0: '' }
|
||
|
||
/** Escape-Folgen einer JavaScript-Zeichenkette auflösen (\n, \", \\, \x41, \u{…} und vier Hexziffern). */
|
||
function entschluessele(roh) {
|
||
return roh.replace(/\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r?\n)|([\s\S]))/g, (_, a, b, c, umbruch, z) => {
|
||
if (a || b || c) return String.fromCodePoint(parseInt(a || b || c, 16))
|
||
if (umbruch !== undefined) return ''
|
||
return Object.hasOwn(ESCAPES, z) ? ESCAPES[z] : z
|
||
})
|
||
}
|
||
|
||
const REGEX_VOR = new Set(['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '<', '>', '~', '^'])
|
||
|
||
/**
|
||
* Alle Zeichenketten eines JavaScript-Quelltexts mit Zeilennummer, ohne Kommentare und ohne Regex-Literale.
|
||
* In Vorlagen-Zeichenketten steht {…} an der Stelle von ${…}.
|
||
*/
|
||
export function zeichenketten(quelle) {
|
||
const aus = []
|
||
const n = quelle.length
|
||
let i = 0
|
||
let zeile = 1
|
||
let letztes = ''
|
||
while (i < n) {
|
||
const c = quelle[i]
|
||
const d = quelle[i + 1]
|
||
if (c === '\n') { zeile += 1; i += 1; continue }
|
||
if (c === ' ' || c === '\t' || c === '\r') { i += 1; continue }
|
||
if (c === '/' && d === '/') { while (i < n && quelle[i] !== '\n') i += 1; continue }
|
||
if (c === '/' && d === '*') {
|
||
const ende = quelle.indexOf('*/', i + 2)
|
||
const stop = ende < 0 ? n : ende + 2
|
||
zeile += (quelle.slice(i, stop).match(/\n/g) ?? []).length
|
||
i = stop
|
||
continue
|
||
}
|
||
if (c === "'" || c === '"' || c === '`') {
|
||
const start = zeile
|
||
let j = i + 1
|
||
let roh = ''
|
||
while (j < n && quelle[j] !== c) {
|
||
const z = quelle[j]
|
||
if (z === '\\') { roh += z + (quelle[j + 1] ?? ''); if (quelle[j + 1] === '\n') zeile += 1; j += 2; continue }
|
||
if (c === '`' && z === '$' && quelle[j + 1] === '{') {
|
||
let tiefe = 1
|
||
j += 2
|
||
while (j < n && tiefe > 0) {
|
||
if (quelle[j] === '{') tiefe += 1
|
||
else if (quelle[j] === '}') tiefe -= 1
|
||
else if (quelle[j] === '\n') zeile += 1
|
||
j += 1
|
||
}
|
||
roh += '{…}'
|
||
continue
|
||
}
|
||
if (z === '\n') { if (c !== '`') break; zeile += 1 }
|
||
roh += z
|
||
j += 1
|
||
}
|
||
aus.push({ text: entschluessele(roh), zeile: start })
|
||
i = j + 1
|
||
letztes = 'x'
|
||
continue
|
||
}
|
||
if (c === '/' && REGEX_VOR.has(letztes)) {
|
||
let j = i + 1
|
||
let klasse = false
|
||
while (j < n && quelle[j] !== '\n') {
|
||
const z = quelle[j]
|
||
if (z === '\\') { j += 2; continue }
|
||
if (z === '[') klasse = true
|
||
else if (z === ']') klasse = false
|
||
else if (z === '/' && !klasse) break
|
||
j += 1
|
||
}
|
||
i = j + 1
|
||
while (/[a-z]/.test(quelle[i] ?? '')) i += 1
|
||
letztes = 'x'
|
||
continue
|
||
}
|
||
letztes = c
|
||
i += 1
|
||
}
|
||
return aus
|
||
}
|
||
|
||
const ENTITAETEN = { nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", rarr: '→', ndash: '–', mdash: '—', hellip: '…', sect: '§', euro: '€' }
|
||
const entitaeten = (text) => text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, code) => {
|
||
if (code[0] !== '#') return ENTITAETEN[code.toLowerCase()] ?? m
|
||
return String.fromCodePoint(code[1].toLowerCase() === 'x' ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10))
|
||
})
|
||
|
||
/** Sichtbare Texte einer HTML-Datei (Textknoten und die Attribute data-text, data-label, placeholder, aria-label, title, alt) mit Zeilennummer. */
|
||
export function htmlTexte(html) {
|
||
const leer = (m) => m.replace(/[^\n]/g, ' ')
|
||
const sauber = html.replace(/<script[\s\S]*?<\/script>/gi, leer).replace(/<style[\s\S]*?<\/style>/gi, leer).replace(/<!--[\s\S]*?-->/g, leer)
|
||
const zeileBei = (index) => sauber.slice(0, index).split('\n').length
|
||
const aus = []
|
||
for (const m of sauber.matchAll(/>([^<>]+)</g)) aus.push({ text: entitaeten(m[1]), zeile: zeileBei(m.index + 1) })
|
||
for (const m of sauber.matchAll(/\b(?:data-text|data-label|placeholder|aria-label|title|alt)="([^"]*)"/g)) aus.push({ text: entitaeten(m[1]), zeile: zeileBei(m.index) })
|
||
return aus.filter((eintrag) => norm(eintrag.text) !== '').sort((a, b) => a.zeile - b.zeile)
|
||
}
|
||
|
||
/** Sieht die Zeichenkette wie ein Satz oder eine Beschriftung für Menschen aus (nicht wie Code, Selektor oder Adresse)? */
|
||
export function istSatz(text) {
|
||
const t = norm(text)
|
||
return t.length >= 12 && t.includes(' ') && /\p{Lu}/u.test(t) && /\p{Ll}{3}/u.test(t)
|
||
&& !/^[#.[{(<\/@$:&*]/.test(t) && !/=>|^(?:import|export|function|return|const|let|var)\b/.test(t)
|
||
}
|
||
|
||
/** Rechtlicher Bezug eines Satzes: das erste passende Stichwort gibt den Namen der Gruppe (Reihenfolge zählt). */
|
||
export const BEZUEGE = [
|
||
['Beginn der Arbeit', /beginn|frühestens/i],
|
||
['Widerruf', /widerruf/i],
|
||
['Erklärung des Kunden', /ich bestätige|ich verlange|häkchen|bestätigt am|zu diesen bedingungen|mit einem klick/i],
|
||
['Speicherung und Datenschutz', /gespeichert|speicher|uhrzeit|datenschutz|impressum/i],
|
||
['AGB, Preis und Steuer', /agb|geschäftsbedingung|§|steuer|gesamtpreis|stundensatz|zahlbar/i],
|
||
['Zustand des Links', /ungültig|abgelaufen|ersetzt|storniert|nicht mehr gültig|neuen link|neuesten/i],
|
||
['Kundenansicht', /ansichtsmodus/i],
|
||
['Kontakt', /erreichen|kontakt|melden sie sich|support@/i],
|
||
['Nummer der Auftragsbestätigung', /falls zur hand|nummer (?:ihrer|der|deiner) auftragsbestätigung/i],
|
||
]
|
||
|
||
/** Name der Gruppe, zu der ein Satz mit rechtlichem Bezug gehört; leer, wenn kein Stichwort trifft. */
|
||
export const bezug = (text) => BEZUEGE.find(([, muster]) => muster.test(text))?.[0] ?? ''
|
||
|
||
/** Texte, die in `neu` stehen und in `alt` nicht (Vergleich ohne Unterschied im Leerraum). */
|
||
export function nurNeue(neu, alt) {
|
||
const bekannt = new Set(alt.map((eintrag) => norm(eintrag.text)))
|
||
return neu.filter((eintrag) => !bekannt.has(norm(eintrag.text)))
|
||
}
|
||
|
||
/** Texte einer Datei (nach Endung .js oder .html), die Sätze sind; mit `alt` nur die neuen. */
|
||
export function seitenTexte(pfad, neu, alt = null) {
|
||
const lies = (quelltext) => (pfad.endsWith('.html') ? htmlTexte(quelltext) : zeichenketten(quelltext))
|
||
const alle = lies(neu)
|
||
const treffer = alt === null ? alle : nurNeue(alle, lies(alt))
|
||
const gesehen = new Set()
|
||
return treffer.filter((eintrag) => {
|
||
const schluessel = norm(eintrag.text)
|
||
if (!istSatz(eintrag.text) || gesehen.has(schluessel)) return false
|
||
gesehen.add(schluessel)
|
||
return true
|
||
}).map((eintrag) => ({ zeile: eintrag.zeile, text: norm(eintrag.text) }))
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------- Selbsttest
|
||
|
||
/**
|
||
* Prüft die Hilfen gegen kleine Beispiele und gegen renderMail (mit einer vorhandenen Vorlage); wirft bei der ersten Abweichung.
|
||
* `probe` = { quelltext: Quelltext einer vorhandenen Vorlage, renderMail } aus dem Worktree.
|
||
*/
|
||
export function selbsttest(werkzeug, probe) {
|
||
const js = [
|
||
"// Kommentar mit 'Anführungszeichen' und \"mehr\"",
|
||
'/* Block \'x\' */',
|
||
"const a = 'Ihre Bestätigung wird mit Datum und Uhrzeit gespeichert.'",
|
||
'const b = "Wir beginnen frühestens am \\"x\\"."',
|
||
'const c = `Bestätigt am ${zeitpunktDe(a.bestaetigtAm)}, ${x ? \'ja\' : "nein"} Uhr`',
|
||
"const d = /['\"]\\/x/g.test(a)",
|
||
"const e = 5 / 2 + 'zwei Wörter hier'",
|
||
"const f = 'Zeile eins\\nZeile zwei'",
|
||
].join('\n')
|
||
assert.deepEqual(zeichenketten(js).map((k) => [k.zeile, k.text]), [
|
||
[3, 'Ihre Bestätigung wird mit Datum und Uhrzeit gespeichert.'],
|
||
[4, 'Wir beginnen frühestens am "x".'],
|
||
[5, 'Bestätigt am {…}, {…} Uhr'],
|
||
[7, 'zwei Wörter hier'],
|
||
[8, 'Zeile eins\nZeile zwei'],
|
||
], 'Selbsttest zeichenketten')
|
||
const html = '<p class="x">Bitte prüfen Sie die\n Angaben & bestätigen Sie.</p>\n<script>var s = "Nicht sichtbar im Satz";</script>\n<input placeholder="Nummer der Auftragsbestätigung (falls zur Hand)">'
|
||
assert.deepEqual(htmlTexte(html).map((k) => [k.zeile, norm(k.text)]), [
|
||
[1, 'Bitte prüfen Sie die Angaben & bestätigen Sie.'],
|
||
[4, 'Nummer der Auftragsbestätigung (falls zur Hand)'],
|
||
], 'Selbsttest htmlTexte')
|
||
assert.ok(istSatz('Wir beginnen frühestens am {…}.') && !istSatz('.ab-karte .ab-meta') && !istSatz('application/json') && !istSatz('Auftrag'), 'Selbsttest istSatz')
|
||
assert.equal(bezug('Wir beginnen frühestens am {…}.'), 'Beginn der Arbeit', 'Selbsttest bezug')
|
||
assert.equal(bezug('Wir beginnen auf Ihren Wunsch schon vor Ablauf der Widerrufsfrist.'), 'Beginn der Arbeit', 'Selbsttest bezug')
|
||
assert.equal(bezug('Ihre Bestätigung wird mit Datum und Uhrzeit gespeichert.'), 'Speicherung und Datenschutz', 'Selbsttest bezug')
|
||
assert.equal(bezug('Ihre Auftragsbestätigung wird geladen …'), '', 'Selbsttest bezug: der Name des Dokuments allein ist kein Bezug')
|
||
assert.deepEqual(zeilenMehr('a\nb\nb', 'a\nb\nb\nb\nc'), ['b', 'c'], 'Selbsttest zeilenMehr')
|
||
// Rendern wie renderMail: dieselbe Vorlage, dieselben Werte, dieselben Blöcke
|
||
const vorlage = leseVorlage(probe.quelltext)
|
||
const werte = Object.fromEntries(vorlage.platzhalter.map((name) => [name, `Probe ${name}`]))
|
||
const bloecke = Object.fromEntries(vorlage.bloecke.map((name, i) => [name, i % 2 === 0]))
|
||
const eigene = renderVorlage(probe.quelltext, werte, bloecke, werkzeug)
|
||
const echte = probe.renderMail(vorlage.id, werte, bloecke)
|
||
assert.equal(eigene.text, echte.text, `Selbsttest renderVorlage: Text von ${vorlage.id} weicht von renderMail ab`)
|
||
assert.equal(eigene.betreff, echte.subject, `Selbsttest renderVorlage: Betreff von ${vorlage.id} weicht von renderMail ab`)
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Skript anlegen.** Datei `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/rechtsrunde-erzeugen.mjs` mit genau diesem Inhalt:
|
||
|
||
```js
|
||
// Erzeugt rechtsrunde.md (Freigabe-Dokument der Rechtsrunde „Aufträge und Rechnungen“) aus rechtsrunde-vorlage.md und den Dateien der Stränge:
|
||
// Abschnitte 1 bis 6: Branch auftraege/l7-rechtstexte (Worktree dev-auftraege-l7, nur wenn sauber und am Ende des Branches),
|
||
// Abschnitt 7: Mails (Strang L5) und Seitentexte (Strang L6), gelesen mit git show aus feature/auftraege (sobald der Strang dort gemergt ist)
|
||
// oder aus dem Branch des Strangs.
|
||
// Nur lesen und genau diese eine Datei schreiben (nicht einchecken). Kann beliebig oft ausgeführt werden: nach dem Merge von L5 und L6 und nach jeder Textänderung.
|
||
// Ende: 0 = vollständig, 3 = Quellen von L5 oder L6 fehlen noch (Dokument steht, Abschnitt 7 trägt den Vermerk), 4 = Prüfung fehlgeschlagen (Befund im Dokument).
|
||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||
import { dirname, join } from 'node:path'
|
||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||
import { dateiAus, git, leseVorlage, norm, quelleVon, renderVorlage, selbsttest, seitenTexte, vorlageAus, zeilenMehr, innereBloecke, bezug } from './rechtsrunde-quellen.mjs'
|
||
|
||
const HIER = dirname(fileURLToPath(import.meta.url))
|
||
const WT = process.env.RECHTSRUNDE_WT || '/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7'
|
||
const OUT_DIR = process.env.RECHTSRUNDE_BERICHTE || '/root/reports/auftraege-rechnungen-2026-10-06'
|
||
const OUT = join(OUT_DIR, 'rechtsrunde.md')
|
||
if (!OUT.startsWith(`${OUT_DIR}/`)) throw new Error('Zielpfad liegt außerhalb des Berichtsordners')
|
||
const L5 = 'auftraege/l5-bestaetigung'
|
||
const L6 = 'auftraege/l6-kundenportal'
|
||
const L7 = 'auftraege/l7-rechtstexte'
|
||
|
||
const laden = (rel) => import(pathToFileURL(join(WT, rel)).href)
|
||
const lies = (rel) => readFileSync(join(WT, rel), 'utf8')
|
||
|
||
// Der Worktree von L7 muss sauber am Ende des Branches stehen: Das Dokument zeigt nur, was eingecheckt ist.
|
||
if (git(WT, 'status', '--porcelain', '--untracked-files=no') !== '') throw new Error(`Der Worktree ${WT} hat nicht eingecheckte Änderungen: erst committen (Task 6), dann erzeugen`)
|
||
if (git(WT, 'rev-parse', 'HEAD') !== git(WT, 'rev-parse', L7)) throw new Error(`Der Worktree ${WT} steht nicht am Ende von ${L7}`)
|
||
|
||
const texte = await laden('server/services/auftraege/texte.js')
|
||
const { AGB_FASSUNG } = await laden('server/services/auftraege/konstanten.js')
|
||
const mailModul = await laden('server/services/mailTemplates.js')
|
||
const { config } = await laden('server/config.js')
|
||
const werkzeug = { htmlToText: mailModul.htmlToText, escapeHtml: mailModul.escapeHtml, publicBaseUrl: config.publicBaseUrl }
|
||
for (const id of ['K16', 'K15', 'K04', 'T09']) selbsttest(werkzeug, { quelltext: vorlageAus(WT, L7, id).quelltext, renderMail: mailModul.renderMail })
|
||
|
||
const T = texte.AB_TEXTE
|
||
const B = texte.WIDERRUFSBELEHRUNG_PROJEKT
|
||
const F = texte.WIDERRUFSFORMULAR
|
||
|
||
/** Text als Zitatblock (jede Zeile mit "> "). */
|
||
const zitat = (text) => text.split('\n').map((z) => (z.trim() === '' ? '>' : `> ${z}`)).join('\n')
|
||
const deDatum = (iso) => iso.split('-').reverse().join('.')
|
||
const klartext = (html) => html.replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/"/g, '"').replace(/\s+/g, ' ').trim()
|
||
/** Ganzer Absatz <p …>…</p>, der den Anfang enthält. */
|
||
const absatzMit = (html, anfang) => {
|
||
const a = html.indexOf(anfang)
|
||
if (a < 0) throw new Error(`Absatz nicht gefunden: ${anfang}`)
|
||
return html.slice(html.lastIndexOf('<p', a), html.indexOf('</p>', a) + 4)
|
||
}
|
||
/** Überschrift und Absätze des Abschnitts 9a als Klartext. */
|
||
const abschnitt9a = (html) => {
|
||
const a = html.indexOf('<h2 id="auftraege">')
|
||
if (a < 0) throw new Error('Abschnitt 9a fehlt in public/datenschutz.html')
|
||
const block = html.slice(a, html.indexOf('<h2', a + 5))
|
||
return [...block.matchAll(/<(h2|p)[^>]*>([\s\S]*?)<\/\1>/g)].map(([, tag, innen]) => (tag === 'h2' ? `**${klartext(innen)}**` : klartext(innen))).join('\n\n')
|
||
}
|
||
/** Sätze eines Absatzes (Satzende gefolgt von einem Großbuchstaben oder einem öffnenden Anführungszeichen). */
|
||
const saetze = (text) => text.split(/(?<=[.;])\s+(?=[A-ZÄÖÜ„])/).map((s) => s.trim()).filter(Boolean)
|
||
/** Sätze, die nur im neuen bzw. nur im alten Absatz stehen. */
|
||
const unterschied = (alt, neu) => {
|
||
const vorher = new Set(saetze(alt))
|
||
const nachher = new Set(saetze(neu))
|
||
return { neu: saetze(neu).filter((s) => !vorher.has(s)), alt: saetze(alt).filter((s) => !nachher.has(s)) }
|
||
}
|
||
const liste = (zeilen) => (zeilen.length > 0 ? zeilen.map((z) => `- ${z}`).join('\n') : '- (keine)')
|
||
const stand = (html) => /Stand \d{2}\.\d{2}\.\d{4}/.exec(html)?.[0] ?? 'Stand unbekannt'
|
||
const standIso = (html) => { const m = /Stand (\d{2})\.(\d{2})\.(\d{4})/.exec(html); return m ? `${m[3]}-${m[2]}-${m[1]}` : 'unbekannt' }
|
||
const heute = new Date().toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' })
|
||
|
||
const dsNeu = lies('public/datenschutz.html')
|
||
const dsCommit = git(WT, 'log', '--format=%H', '-n1', '--grep=^Datenschutz: Abschnitt 9a', L7) // der letzte Commit des Strangs (Task 6)
|
||
if (dsCommit === '') throw new Error('Der Datenschutz-Commit (Betreff „Datenschutz: Abschnitt 9a …“) fehlt auf dem Branch')
|
||
const dsAlt = git(WT, 'show', `${dsCommit}~1:public/datenschutz.html`) // Stand unmittelbar davor
|
||
const WIDERRUF = '<p id="kuendigung-widerruf">'
|
||
const SECHS = 'Alle Portal-Daten liegen in unseren selbst betriebenen Systemen'
|
||
const sechs = unterschied(klartext(absatzMit(dsAlt, SECHS)), klartext(absatzMit(dsNeu, SECHS)))
|
||
const widerruf = unterschied(klartext(absatzMit(dsAlt, WIDERRUF)), klartext(absatzMit(dsNeu, WIDERRUF)))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------- Abschnitt 7: Mails (L5) und Seiten (L6)
|
||
|
||
const fehlend = [] // Quellen, die es noch nicht gibt (Ende 3)
|
||
const befunde = [] // Prüfungen, die fehlschlagen (Ende 4)
|
||
const geprueft = [] // bestandene Prüfungen, für die Liste im Dokument
|
||
const pruefe = (bedingung, ok, nichtOk) => {
|
||
if (bedingung) geprueft.push(ok)
|
||
else befunde.push(nichtOk)
|
||
}
|
||
|
||
const quelleL5 = quelleVon(WT, L5)
|
||
const quelleL6 = quelleVon(WT, L6)
|
||
const stand7 = (quelle, branch, was) => (quelle
|
||
? `- ${was}: \`${quelle.ref}\` @ ${quelle.sha}${quelle.inIntegration ? ' (Strang dort gemergt)' : ` (Branch ${branch}, noch nicht in feature/auftraege)`}`
|
||
: `- ${was}: **Branch ${branch} fehlt**`)
|
||
if (!quelleL5) fehlend.push(`Strang L5: Branch ${L5} fehlt`)
|
||
if (!quelleL6) fehlend.push(`Strang L6: Branch ${L6} fehlt`)
|
||
|
||
const PROBE = {
|
||
name: 'Erika Muster', nummer: 'AB-2026-0001', titel: 'Website für Musterbäckerei', datum: '06.10.2026', betrag: '800,00 €',
|
||
link: 'https://project.webklar.com/auftrag.html#t=BEISPIELLINK', gueltig_bis: '05.12.2026', zeitpunkt: '07.10.2026, 10:30',
|
||
fruehester_beginn: '21.10.2026', fruehbeginn_text: texte.FRUEHBEGINN_TEXT, kunde: 'Muster GmbH', ueber: 'Bestätigungslink',
|
||
woms_link: 'https://ticket.webklar.com/customers/KUNDE?tab=invoices', email: 'erika@example.invalid', kundennummer: 'KD-1',
|
||
produkt: 'Projektauftrag', bestellung: 'Projektauftrag (Auftragsbestätigung AB-2026-0001)', eingang_zeitpunkt: '07.10.2026, 11:00',
|
||
fristende: '20.10.2026', rechtzeitig: 'ja',
|
||
}
|
||
const V6 = {
|
||
K31: ['name', 'nummer', 'titel', 'datum', 'betrag', 'link', 'gueltig_bis'],
|
||
K32: ['name', 'nummer', 'titel', 'zeitpunkt', 'fruehester_beginn', 'fruehbeginn_text'],
|
||
T22: ['kunde', 'nummer', 'titel', 'zeitpunkt', 'ueber', 'woms_link', 'fruehester_beginn'],
|
||
}
|
||
|
||
/** Fußzeile der Mails (Links zum Portal, Anschrift; bei Team-Mails der Hinweis auf die automatische Benachrichtigung) abschneiden. */
|
||
const ohneFuss = (text) => {
|
||
const i = text.search(/\n(?:Kundenportal \(|Automatische Benachrichtigung )/)
|
||
return (i < 0 ? text : text.slice(0, i)).trimEnd()
|
||
}
|
||
|
||
/** Eine Mail in einer Fassung rendern; liefert { betreff, text } oder null (mit Eintrag in fehlend bzw. befunde). */
|
||
function mail(id, ueberschreibe, bloecke, { braucht = [] } = {}) {
|
||
const v = quelleL5 ? vorlageAus(WT, quelleL5.ref, id) : null
|
||
if (!v) {
|
||
if (quelleL5) fehlend.push(`Strang L5: Mailvorlage ${id} fehlt in ${quelleL5.ref}`)
|
||
return null
|
||
}
|
||
const vorlage = leseVorlage(v.quelltext)
|
||
for (const name of V6[id] ?? []) if (!vorlage.platzhalter.includes(name)) befunde.push(`Vorlage ${id}: Platzhalter {{${name}}} fehlt (Vertrag V6)`)
|
||
for (const name of braucht) if (!vorlage.bloecke.includes(name)) befunde.push(`Vorlage ${id}: Block „${name}“ fehlt (Vertrag V6)`)
|
||
const werte = Object.fromEntries(Object.entries({ ...PROBE, ...ueberschreibe }).filter(([name]) => vorlage.platzhalter.includes(name)))
|
||
const an = Object.fromEntries(Object.entries(bloecke).filter(([name, wert]) => wert && vorlage.bloecke.includes(name)))
|
||
const gerendert = renderVorlage(v.quelltext, werte, an, werkzeug)
|
||
return { betreff: gerendert.betreff, text: ohneFuss(gerendert.text), probleme: gerendert.probleme }
|
||
}
|
||
const mailZitat = (m, id) => (m ? zitat(`Betreff: ${m.betreff}\n\n${m.text}`) : `> (nicht vorhanden: Mailvorlage ${id}, Strang L5)`)
|
||
|
||
const kundenmail = (id) => {
|
||
const v = quelleL5 ? vorlageAus(WT, quelleL5.ref, id) : null
|
||
return v ? leseVorlage(v.quelltext).bloecke.includes('kundenmail') : false
|
||
}
|
||
const k31Kunde = kundenmail('K31') ? { kundenmail: true } : {}
|
||
const k31Privat = mail('K31', {}, { festpreis: true, privat: true, ...k31Kunde }, { braucht: ['festpreis', 'privat'] })
|
||
const k31Erinnerung = mail('K31', { betrag: '100,00 € pro Stunde' }, { aufwand: true, erinnerung: true, ...k31Kunde }, { braucht: ['aufwand', 'erinnerung'] })
|
||
const k31Team = mail('K31', { link: '' }, { festpreis: true, teamkopie: true }, { braucht: ['teamkopie'] })
|
||
const k32Frueh = mail('K32', {}, { fruehbeginn: true }, { braucht: ['fruehbeginn', 'ohne_fruehbeginn'] })
|
||
const k32Ohne = mail('K32', {}, { ohne_fruehbeginn: true })
|
||
const k32Firma = mail('K32', {}, {})
|
||
const t22Frueh = mail('T22', {}, { privat_fruehbeginn: true }, { braucht: ['privat_fruehbeginn', 'privat_ohne_fruehbeginn'] })
|
||
const t22Ohne = mail('T22', {}, { privat_ohne_fruehbeginn: true })
|
||
const t22Firma = mail('T22', {}, {})
|
||
|
||
/** Zeilen, die ein Block `projekt` zu K04 bzw. T09 beisteuert (samt seiner inneren Blöcke, je einzeln eingeschaltet). */
|
||
function projektBlock(id) {
|
||
const v = quelleL5 ? vorlageAus(WT, quelleL5.ref, id) : null
|
||
if (!v) {
|
||
if (quelleL5) fehlend.push(`Strang L5: Mailvorlage ${id} fehlt in ${quelleL5.ref}`)
|
||
return `> (nicht vorhanden: Mailvorlage ${id}, Strang L5)`
|
||
}
|
||
const vorlage = leseVorlage(v.quelltext)
|
||
if (!vorlage.bloecke.includes('projekt')) {
|
||
fehlend.push(`Strang L5: Vorlage ${id} hat noch keinen Block „projekt“ (Übersicht V6)`)
|
||
return `> (nicht vorhanden: Block „projekt“ in ${id}, Strang L5)`
|
||
}
|
||
const werte = Object.fromEntries(Object.entries(PROBE).filter(([name]) => vorlage.platzhalter.includes(name)))
|
||
const text = (bloecke) => renderVorlage(v.quelltext, werte, bloecke, werkzeug).text
|
||
const ohne = text({})
|
||
const mitProjekt = text({ projekt: true })
|
||
const teile = [`Zeilen, die der Block „projekt“ ergänzt:\n\n${zitat(zeilenMehr(ohne, mitProjekt).join('\n') || '(keine)')}`]
|
||
for (const innen of innereBloecke(v.quelltext, 'projekt')) {
|
||
if (!vorlage.bloecke.includes(innen)) continue
|
||
teile.push(`Zusätzlich, wenn der Block „${innen}“ innerhalb von „projekt“ an ist:\n\n${zitat(zeilenMehr(mitProjekt, text({ projekt: true, [innen]: true })).join('\n') || '(keine)')}`)
|
||
}
|
||
return teile.join('\n\n')
|
||
}
|
||
const k04Projekt = projektBlock('K04')
|
||
const t09Projekt = projektBlock('T09')
|
||
|
||
// Prüfungen der Zusagen aus der Planrunde
|
||
if (k32Frueh) {
|
||
pruefe(norm(k32Frueh.text).includes(norm(texte.FRUEHBEGINN_TEXT)), 'K32 gibt die Erklärung (FRUEHBEGINN_TEXT) wörtlich wieder: ja', 'K32 gibt die Erklärung FRUEHBEGINN_TEXT nicht wörtlich wieder')
|
||
pruefe(!/Widerrufsrecht bleibt|bleibt davon unberührt/i.test(k32Frueh.text), 'K32 enthält nicht den Satz „Ihr Widerrufsrecht bleibt davon unberührt“: ja', 'K32 enthält noch den Satz „Widerrufsrecht bleibt unberührt“ (Planrunde: entfällt)')
|
||
}
|
||
if (k31Team) pruefe(!/auftrag\.html|#t=/.test(k31Team.text), 'Die Kopie der K31 für das Team enthält keinen Link: ja', 'Die Kopie der K31 für das Team enthält einen Link zur Bestätigungsseite')
|
||
if (k31Privat) pruefe(/Widerruf/.test(k31Privat.text), 'K31 für Privatpersonen weist auf das Widerrufsrecht hin: ja', 'K31 für Privatpersonen nennt das Widerrufsrecht nicht')
|
||
if (t22Frueh) pruefe(t22Frueh.betreff.startsWith('[Aufträge]'), 'Der Betreff von T22 beginnt mit „[Aufträge]“: ja', 'Der Betreff von T22 beginnt nicht mit „[Aufträge]“')
|
||
|
||
// Seitentexte (Strang L6): alle Texte der Dateien unter public/, die L6 angelegt oder geändert hat
|
||
const ROLLEN = {
|
||
'public/auftrag.html': 'Hülle der öffentlichen Seite „Auftrag bestätigen“',
|
||
'public/auftrag.js': 'öffentliche Seite: Ablauf und Meldungen',
|
||
'public/auftrag-ansicht.js': 'gemeinsame Texte: Zusammenfassung, Zustände, Fehlermeldungen',
|
||
'public/auftrag-dom.js': 'Bausteine der Seiten',
|
||
'public/auftrag-portal.js': 'Abschnitt „Aufträge“ im Kundenportal (Du-Form)',
|
||
'public/auftrag-widerruf.js': 'Widerrufsseite: Feld für die Nummer der Auftragsbestätigung',
|
||
'public/dashboard.html': 'Kundenportal: neue oder geänderte Texte (Abschnitt „Aufträge“)',
|
||
'public/widerrufen.html': 'Widerrufsseite: neue oder geänderte Texte (Vertragsart Projektauftrag)',
|
||
}
|
||
const seitenDateien = []
|
||
if (quelleL6) {
|
||
const basis = git(WT, 'merge-base', L6, L7) // Stand nach L0: die beiden Stränge sind Geschwister
|
||
const geaendert = git(WT, 'diff', '--no-renames', '--name-status', basis, L6, '--', 'public').split('\n').filter(Boolean).map((z) => z.split('\t'))
|
||
const ziele = geaendert.filter(([status, pfad]) => /^[AM]$/.test(status) && /\.(js|html)$/.test(pfad) && !/^public\/(rechtliches|fonts)\//.test(pfad) && !/(^|\/)sw\.js$/.test(pfad))
|
||
if (ziele.length === 0) fehlend.push(`Strang L6: ${L6} hat noch keine Seiten unter public/`)
|
||
for (const [status, pfad] of ziele) {
|
||
const neu = dateiAus(WT, quelleL6.ref, pfad) ?? dateiAus(WT, L6, pfad)
|
||
if (neu === null) continue
|
||
const eintraege = seitenTexte(pfad, neu, status === 'A' ? null : dateiAus(WT, basis, pfad))
|
||
if (eintraege.length > 0) seitenDateien.push({ pfad, rolle: ROLLEN[pfad] ?? 'weitere Datei von Strang L6', eintraege })
|
||
}
|
||
const rang = (pfad) => (Object.keys(ROLLEN).includes(pfad) ? Object.keys(ROLLEN).indexOf(pfad) : 99)
|
||
seitenDateien.sort((a, b) => rang(a.pfad) - rang(b.pfad) || a.pfad.localeCompare(b.pfad))
|
||
}
|
||
const zelle = (text) => text.replaceAll('|', '\\|')
|
||
const seitenTabelle = (mitBezug) => {
|
||
const teile = []
|
||
for (const datei of seitenDateien) {
|
||
const zeilen = datei.eintraege.map((e) => ({ ...e, bezug: bezug(e.text) })).filter((e) => (e.bezug !== '') === mitBezug)
|
||
if (zeilen.length === 0) continue
|
||
teile.push(`**${datei.pfad}** (${datei.rolle})\n\n| Zeile | Gruppe | Text |\n|---|---|---|\n${zeilen.map((e) => `| ${e.zeile} | ${e.bezug || '-'} | ${zelle(e.text)} |`).join('\n')}`)
|
||
}
|
||
return teile.length > 0 ? teile.join('\n\n') : (quelleL6 ? '(keine)' : '> (nicht vorhanden: Seiten von Strang L6)')
|
||
}
|
||
|
||
const status7 = [
|
||
stand7(quelleL5, L5, 'Mails (Strang L5)'),
|
||
stand7(quelleL6, L6, 'Seitentexte (Strang L6)'),
|
||
...geprueft.map((z) => `- Geprüft beim Erzeugen: ${z}`),
|
||
...befunde.map((z) => `- **BEFUND: ${z}**`),
|
||
...fehlend.map((z) => `- **NOCH NICHT VORHANDEN: ${z}**`),
|
||
].join('\n') + (fehlend.length > 0 ? '\n\n**Dieser Abschnitt ist unvollständig.** Nach dem Merge von L5 und L6 den Befehl vom Anfang dieses Dokuments noch einmal ausführen.' : '')
|
||
|
||
// ---------------------------------------------------------------------------------------------------------- Ersetzungen
|
||
|
||
const ersetzungen = {
|
||
HEUTE: heute,
|
||
FASSUNG: texte.TEXTE_FASSUNG,
|
||
AGB_FASSUNG,
|
||
COMMIT: git(WT, 'rev-parse', '--short', 'HEAD'),
|
||
BEFEHL: `node ${join(HIER, 'rechtsrunde-erzeugen.mjs')}`,
|
||
AB_EINLEITUNG: zitat(T.einleitung({ datum: '05.10.2026', weg: 'telefon' })),
|
||
AB_AUFWAND_MIT: zitat(T.aufwand({ satz: '100,00 €', schaetzung: '10' })),
|
||
AB_AUFWAND_OHNE: zitat(T.aufwand({ satz: '100,00 €', schaetzung: null })),
|
||
AB_KLEIN: zitat(T.kleinunternehmer),
|
||
AB_ZP_NACH_ABNAHME: zitat(T.zahlungsplan.nach_abnahme({ ziel: 14 })),
|
||
AB_ZP_ANZAHLUNG: zitat(T.zahlungsplan.anzahlung({ prozent: 50, anzahlung: '400,00 €', rest: '400,00 €', ziel: 14 })),
|
||
AB_ZP_MONATLICH: zitat(T.zahlungsplan.monatlich({ ziel: 14 })),
|
||
AB_ZP_FREI: zitat(T.zahlungsplan.frei({ text: 'Je ein Drittel bei Auftrag, zur Halbzeit und nach Abnahme', ziel: 7 })),
|
||
AB_TERMINE: zitat(T.termine({ start: '08.10.2026', fertig: '31.10.2026' })),
|
||
AB_TERMINE_PRIVAT: zitat(T.termine({ start: '08.10.2026', fertig: '31.10.2026', privat: true })),
|
||
AB_MITWIRKUNG: zitat(T.mitwirkung),
|
||
AB_AGB: zitat(T.agb({ fassung: deDatum(AGB_FASSUNG) })),
|
||
AB_ERSETZT: zitat(T.ersetzt({ nummer: 'AB-2026-0001', datum: '05.10.2026' })),
|
||
AB_GRUSS: zitat(T.gruss),
|
||
BESTAETIGUNG: zitat(texte.BESTAETIGUNG_TEXT),
|
||
FRUEHBEGINN: zitat(texte.FRUEHBEGINN_TEXT),
|
||
BELEHRUNG: zitat([`**${B.titel}**`, ...B.abschnitte.flatMap((a) => [`**${a.ueberschrift}**`, ...a.absaetze])].join('\n\n')),
|
||
FORMULAR: zitat([`**${F.titel}**`, F.einleitung, ...F.zeilen].join('\n\n')),
|
||
AGB_TEXT: zitat(lies('docs/rechtliches/agb.md')),
|
||
DS_STAND_ALT: stand(dsAlt),
|
||
DS_STAND_NEU: stand(dsNeu),
|
||
DS_VERSION: standIso(dsNeu),
|
||
DS_9A: zitat(abschnitt9a(dsNeu)),
|
||
DS_6_ALT: liste(sechs.alt),
|
||
DS_6_NEU: liste(sechs.neu),
|
||
DS_WIDERRUF_ALT: liste(widerruf.alt),
|
||
DS_WIDERRUF_NEU: liste(widerruf.neu),
|
||
STAND_L5_L6: status7,
|
||
SEITEN_RECHTLICH: seitenTabelle(true),
|
||
SEITEN_UEBRIGE: seitenTabelle(false),
|
||
MAIL_K31_PRIVAT: mailZitat(k31Privat, 'K31'),
|
||
MAIL_K31_ERINNERUNG: mailZitat(k31Erinnerung, 'K31'),
|
||
MAIL_K31_TEAM: mailZitat(k31Team, 'K31'),
|
||
MAIL_K32_FRUEHBEGINN: mailZitat(k32Frueh, 'K32'),
|
||
MAIL_K32_OHNE: mailZitat(k32Ohne, 'K32'),
|
||
MAIL_K32_FIRMA: mailZitat(k32Firma, 'K32'),
|
||
MAIL_T22_FRUEHBEGINN: mailZitat(t22Frueh, 'T22'),
|
||
MAIL_T22_OHNE: mailZitat(t22Ohne, 'T22'),
|
||
MAIL_T22_FIRMA: mailZitat(t22Firma, 'T22'),
|
||
MAIL_K04_PROJEKT: k04Projekt,
|
||
MAIL_T09_PROJEKT: t09Projekt,
|
||
}
|
||
|
||
const vorlage = readFileSync(join(HIER, 'rechtsrunde-vorlage.md'), 'utf8')
|
||
const text = vorlage.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, name) => {
|
||
if (!Object.hasOwn(ersetzungen, name)) throw new Error(`Platzhalter ohne Wert: ${name}`)
|
||
return ersetzungen[name]
|
||
})
|
||
if (/\{\{|\}\}|undefined/.test(text)) throw new Error('Reste von Platzhaltern im Ergebnis')
|
||
mkdirSync(OUT_DIR, { recursive: true })
|
||
writeFileSync(OUT, text)
|
||
console.log(`geschrieben: ${OUT} (${text.split('\n').length} Zeilen)`)
|
||
for (const z of geprueft) console.log(`geprüft: ${z}`)
|
||
for (const z of befunde) console.log(`BEFUND: ${z}`)
|
||
for (const z of fehlend) console.log(`FEHLT: ${z}`)
|
||
process.exit(befunde.length > 0 ? 4 : fehlend.length > 0 ? 3 : 0)
|
||
```
|
||
|
||
- [ ] **Step 5: Erzeugen.** Erst nach dem letzten Commit aus Task 6 (das Skript verlangt einen sauberen Worktree am Ende des Branches und liest als „vorher“ den gemeinsamen Vorfahren mit `feature/auftraege`):
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
git status --short
|
||
git log -1 --format='%h %s'
|
||
timeout 120 nice -n 10 node --max-old-space-size=256 /root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/rechtsrunde-erzeugen.mjs; echo "Ende-Code: $?"
|
||
```
|
||
|
||
Erwartet: `git status` leer, der letzte Commit beginnt mit „Datenschutz: Abschnitt 9a“. Dann `geschrieben: /root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md (… Zeilen)`, fünf Zeilen `geprüft: …: ja` und `Ende-Code: 0`.
|
||
- `Ende-Code: 3` mit Zeilen `FEHLT: …`: L5 oder L6 hat noch keinen Branch oder noch keine Dateien (Phase B läuft parallel). Das ist erlaubt: Abschnitt 7 trägt den Vermerk „unvollständig“. Im Bericht (Task 8) nennen, dass der Befehl nach dem Merge von L5 und L6 noch einmal laufen muss.
|
||
- `Ende-Code: 4` mit Zeilen `BEFUND: …`: Eine Zusage der Planrunde ist in den Dateien von L5 nicht erfüllt (zum Beispiel enthält K32 den Satz „Ihr Widerrufsrecht bleibt davon unberührt“ noch, oder die Kopie der K31 für das Team hat einen Link). Nichts an L5 oder L6 ändern (fremde Stränge), den Befund im Bericht nennen; das Dokument zeigt ihn.
|
||
- Meldet das Skript `Der Worktree … hat nicht eingecheckte Änderungen` oder `… steht nicht am Ende von auftraege/l7-rechtstexte`: Task 6 abschließen bzw. den Branch auschecken, nicht das Skript ändern.
|
||
- Schlägt der Selbsttest (`Selbsttest …`) fehl, hat sich `server/services/mailTemplates.js` gegenüber dem Stand dieses Plans geändert: das Skript nicht „reparieren“, sondern im Bericht melden.
|
||
|
||
- [ ] **Step 6: Kontrollieren.**
|
||
|
||
```bash
|
||
D=/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md
|
||
wc -l "$D"
|
||
command grep -a -c "{{" "$D" # erwartet 0
|
||
command grep -a -n "^## \|^### " "$D" | head -50
|
||
command grep -a -n "Ich verlange ausdrücklich\|Ich bestätige den Auftrag\|Vielen Dank für Ihren Auftrag" "$D" | cut -c1-160
|
||
sed -n '/^Quellen und Prüfungen beim Erzeugen/,/^### 7.1/p' "$D" | cut -c1-200
|
||
```
|
||
|
||
Prüfen: (a) die Abschnitte 1 bis 9 (mit 7.1 bis 7.7) und die Tabelle E1 bis E12 sind da; (b) die Wortlaute der Abschnitte 1 bis 4 stimmen mit `texte.js` überein (sie sind daraus erzeugt); (c) Abschnitt 6.1 zeigt den vollständigen Abschnitt 9a, 6.2 und 6.3 nur die neuen und ersetzten Sätze; (d) Abschnitt 5 enthält den vollständigen Text der AGB-Kopie; (e) Abschnitt 7: Die Quellenliste nennt L5 und L6 mit Branch und Stand, die fünf „Geprüft“-Zeilen stehen da, und es gibt keine Zeile „BEFUND“ oder „NOCH NICHT VORHANDEN“ (sonst gilt Step 5); die Abschnitte 7.2 bis 7.4 zeigen K31 (drei Fassungen), K32 (drei) und T22 (drei) mit Betreff, 7.5 die Zeilen des Blocks `projekt` von K04 und T09.
|
||
(f) **Gegenprobe der Seitentexte** (wenn L6 vorhanden ist): Jede Zeile, die L6 unter `public/` hinzugefügt oder geändert hat und einen Satz für Kunden mit einem der Stichwörter enthält, muss im Dokument stehen (Abschnitt 7.1 oder 7.6):
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
BASIS=$(git merge-base auftraege/l6-kundenportal auftraege/l7-rechtstexte)
|
||
REF=$(git merge-base --is-ancestor auftraege/l6-kundenportal feature/auftraege && echo feature/auftraege || echo auftraege/l6-kundenportal)
|
||
git diff --no-renames -U0 "$BASIS" "$REF" -- public ':(exclude)public/rechtliches' ':(exclude)public/fonts' \
|
||
| command grep -a -E '^\+[^+]' | command grep -a -i -E 'beginn|widerruf|frühestens|gespeichert|falls zur hand|bestätigt am' | cut -c1-220 | head -60
|
||
```
|
||
|
||
Fehlt ein Satz aus dieser Ausgabe im Dokument (zum Beispiel, weil ein Text aus mehreren Zeichenketten zusammengesetzt wird), im Bericht nennen. Ändert sich später ein Text (Korrektur nach der Freigabe), das Skript erneut ausführen, damit Dokument und Dateien nie auseinanderlaufen.
|
||
|
||
- [ ] **Step 7: Kein Commit.** Das Dokument und sein Werkzeug liegen außerhalb des Repos. Diese Task erzeugt **keinen** Commit: Der Datenschutz-Commit aus Task 6 bleibt der letzte.
|
||
|
||
---
|
||
|
||
### Task 8: Selbstprüfung
|
||
|
||
**Files:** keine Änderung (nur Prüfung; ein Korrektur-Commit nur nach Step 6).
|
||
|
||
- [ ] **Step 1: Arbeitsbaum und Dateien.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
git status --short
|
||
git diff --name-status feature/auftraege...HEAD
|
||
git diff --stat feature/auftraege...HEAD -- server/services/auftraege/konstanten.js test/buildLegalHtml.test.js server/services/legalDocs.js
|
||
git log --oneline feature/auftraege..HEAD
|
||
git log -1 --format=%s
|
||
```
|
||
|
||
Erwartet: `git status` leer. `git diff --name-status` zeigt genau diese Dateien (Buchstabe M bzw. A): `M docs/WEBklar-Datenschutzerklaerung.pdf`, `M docs/WEBklar-Datenschutzerklaerung.pdf.sha256`, `A docs/rechtliches/agb.md`, `A docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt`, `M public/datenschutz.html`, `A public/rechtliches/agb-2026-09-30.html`, `A public/rechtliches/agb-2026-09-30.pdf`, `A public/rechtliches/agb-2026-09-30.pdf.sha256`, `A public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.html`, `A public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.pdf`, `A public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.pdf.sha256`, `M scripts/build-legal-html.mjs`, `M scripts/build-legal-pdfs.sh`, `M server/services/auftraege/texte.js`, `A test/auftraegeRechtstexte.test.js`, `M test/legalTexts.test.js`. Der `--stat` der drei geschützten Dateien ist leer. Fünf Commits (Tasks 2 bis 6), **Task 6 zuletzt**: Der Betreff des letzten Commits beginnt mit „Datenschutz: Abschnitt 9a“ (Z1 führt den Branch ohne ihn zusammen).
|
||
|
||
- [ ] **Step 2: Alle Tests dieses Strangs und der Nachbarn.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
for f in auftraegeRechtstexte legalTexts buildLegalHtml legalDocs zeitenRecht termineKanzleiPaket termineVertragsunterlagen termineRechtstexte termineRechtstexteArchiv avvPage analyseLegal mailTemplates; 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
|
||
```
|
||
|
||
Erwartet: überall `fail 0`; `auftraegeRechtstexte` 32 Tests, `skipped 0`.
|
||
|
||
- [ ] **Step 3: Generatoren und Prüfsummen.**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
for s in termine shop auftraege; do node scripts/build-legal-html.mjs $s --check || echo "ABWEICHUNG bei $s"; done
|
||
bash -n scripts/build-legal-pdfs.sh && echo "Syntax ok"
|
||
for h in public/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06 public/rechtliches/agb-2026-09-30; do
|
||
[ "$(sha256sum $h.html | cut -d' ' -f1)" = "$(cat $h.pdf.sha256)" ] && echo "ok $h" || echo "PRÜFSUMME PASST NICHT: $h"
|
||
done
|
||
[ "$(sha256sum public/datenschutz.html | cut -d' ' -f1)" = "$(cat docs/WEBklar-Datenschutzerklaerung.pdf.sha256)" ] && echo "ok Datenschutz" || echo "PRÜFSUMME PASST NICHT: Datenschutz"
|
||
```
|
||
|
||
Erwartet: dreimal `ok` bei den `--check`-Läufen, `Syntax ok`, drei `ok` bei den Prüfsummen, keine Zeile mit „ABWEICHUNG“ oder „PASST NICHT“.
|
||
|
||
- [ ] **Step 4: Vertragsliste abhaken** (jede Zeile mit dem Befehl oder Test belegen):
|
||
|
||
- [ ] `texte.js` exportiert genau die Namen aus V7 (Test `texte_exporte`), `konstanten.js` ist unverändert.
|
||
- [ ] `AGB_FASSUNG` bleibt `2026-09-30` und stimmt mit der Fassungszeile von `agb.md`, den Dateinamen `agb-2026-09-30.*` und `AB_TEXTE.agb({ fassung: '30.09.2026' })` überein.
|
||
- [ ] Die Dateinamen `public/rechtliches/agb-<AGB_FASSUNG>.pdf` (K31-Anhang, `agbUrl`) und `widerrufsbelehrung-projektauftrag-<TEXTE_FASSUNG>.html|pdf` liegen vor.
|
||
- [ ] Die Anker `/datenschutz.html#auftraege` und die Widerrufsseite `https://project.webklar.com/widerrufen.html` (in der Belehrung) sind in den Texten genannt.
|
||
- [ ] Keine Datei außerhalb der Tabelle „Dateien dieses Strangs“ geändert, kein vorhandenes Rechtstext-PDF oder -HTML verändert und keine alte Fassung gelöscht (`git diff --name-status feature/auftraege...HEAD -- public/rechtliches` zeigt nur `A`, nie `M` oder `D`).
|
||
- [ ] Der Datenschutz-Commit ist der letzte Commit (`git log -1 --format=%s` beginnt mit „Datenschutz: Abschnitt 9a“); danach gibt es keinen weiteren.
|
||
- [ ] Zeichenvorrat: Test `zeichenvorrat_figtree` grün.
|
||
- [ ] § 649 BGB ist im Nachweis der Normen als „Kostenanschlag“ belegt, § 650 als anderes Thema (Test `normen_kostenanschlag` grün).
|
||
- [ ] `AB_TEXTE.termine({ start, fertig, privat })` mit dem Satz zum Beginn nach Ablauf der Widerrufsfrist (Test `ab_texte_termine`).
|
||
- [ ] Freigabe-Dokument `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md` vorhanden, nach dem letzten Commit erzeugt; Abschnitt 7 vollständig (Ende-Code 0) oder mit dem Vermerk „unvollständig“ (Ende-Code 3, dann im Bericht).
|
||
- [ ] Kein Push, kein `git add -A`, kein Ersatz-PDF, kein Upload nach WOMS.
|
||
|
||
- [ ] **Step 5: Bericht an den Orchestrator** (kurz): die fünf Commits, die Zahl grüner Tests, der Pfad des Freigabe-Dokuments und der Ende-Code des Skripts (bei 3: „Abschnitt 7 muss nach dem Merge von L5 und L6 neu erzeugt werden: `node /root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/rechtsrunde-erzeugen.mjs`“), die Entscheidungen E1 bis E12 (nur die Nummern und ein Satz), und die **Vertragsfragen** unten, soweit der Ausgangsstand sie bestätigt oder erweitert hat (zum Beispiel ein Test eines anderen Strangs, der einen Wortlaut festhält).
|
||
|
||
- [ ] **Step 6: Nur bei einem Mangel: Korrektur vor den Datenschutz-Commit einfügen.** Findet die Selbstprüfung einen Mangel in einem früheren Commit, darf die Korrektur nicht hinter dem Datenschutz-Commit stehen (Z1 führt `auftraege/l7-rechtstexte~1` zusammen und würde sie verlieren). Die Korrektur wird deshalb unter den Datenschutz-Commit geschoben; die Befehle ändern nur diesen unveröffentlichten Branch, nichts wird gepusht:
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege-l7
|
||
DS=$(git rev-parse HEAD)
|
||
git log -1 --format='%h %s' $DS # beginnt mit „Datenschutz: Abschnitt 9a“
|
||
git checkout --detach HEAD~1
|
||
# Korrektur an den betroffenen Dateien vornehmen, die betroffenen Tests laufen lassen
|
||
git add <geänderte Dateien einzeln>
|
||
git commit -m "Korrektur: <was und warum>" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
git cherry-pick $DS # der Datenschutz-Commit kommt wieder obenauf (bei einem Konflikt lösen, git cherry-pick --continue)
|
||
git checkout -B auftraege/l7-rechtstexte # der Branch zeigt wieder auf die neue Spitze
|
||
git log --oneline feature/auftraege..HEAD
|
||
```
|
||
|
||
Erwartet: die Korrektur steht vor dem Datenschutz-Commit, der als Letzter bleibt. Hat die Korrektur einen Wortlaut, ein PDF oder die Datenschutzerklärung berührt, danach Step 2 und 3 und das Freigabe-Skript (Task 7, Step 5) erneut ausführen; eine PDF-Prüfsumme, die nicht mehr passt, wird nach Task 5 bzw. Task 6 neu gedruckt (Kopf des betroffenen Tasks).
|
||
|
||
---
|
||
|
||
## Vertragsfragen
|
||
|
||
Lücken und Unschärfen in der Übersicht (V6, V7, Dateizuständigkeit, Livegang) und im Abgleich mit den Plänen der anderen Stränge (Stand 06.10.2026). Die Planrunde hat die meisten entschieden; maßgeblich sind die Übersicht (Abschnitt „Nachträge nach der Planrunde“, V6, V7, Integration) und dieser Plan darauf angepasst. Je Punkt der Stand; offen bleibt nur, was keinen Vermerk „entschieden“ trägt. E1 bis E12 des Freigabe-Dokuments sind Entscheidungen des Nutzers in der Rechtsrunde, keine Vertragsfragen. Keine der Fragen verhindert den Start dieses Strangs.
|
||
|
||
1. **Dateizuständigkeit (und die Prüfung in Z1): entschieden (siehe Übersicht, Nachträge).** Zusätzlich zur Tabelle braucht L7 `docs/rechtliches/agb.md` (Markdown-Quelle der AGB-Kopie; der Generator kennt nur Markdown), `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt` (Nachweis der Zählung der Normen) und eine Zeile in `test/legalTexts.test.js` (Pin des Stands der Datenschutzerklärung, 05.10. → 06.10.2026); die Übersicht führt sie jetzt auf, und `LANES['auftraege/l7-rechtstexte']` in Z1 deckt `/^docs\/rechtliches\//` und `test/legalTexts.test.js` ab. `test/auftraegeRechtstexte.test.js` ist durch den Namensraum `auftraege…` gedeckt. `test/buildLegalHtml.test.js` bleibt unberührt: Die Aufruf-Zeilen der Skripte sind so geschrieben, dass seine Prüfungen unverändert bestehen.
|
||
2. **V7 `AB_TEXTE.termine`: Feld `privat`: entschieden (siehe Übersicht, Nachträge; V7 führt `privat`).** Bei Privatpersonen soll die Auftragsbestätigung sagen: „Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.“ `termine({ start, fertig, privat })` nimmt dazu ein Boolean `privat` (Vorgabe `false`; bei `true` ist die Rückgabe nie `null`). L4 reicht `privat = (kunde.kundenart === 'privat')` durch. Der Wortlaut des Satzes bleibt Entscheidung E4 des Nutzers.
|
||
3. **V7 Eingabeformate: Formate entschieden (siehe Übersicht, Nachträge; V7 und L4), die Prüfung der Pflichtwerte bleibt Vorschlag.** Datumsangaben `'TT.MM.JJJJ'`, `weg` Klartext aus `AUFTRAG_WEGE` (`texte.js` nimmt auch den Schlüssel), `satz`, `anzahlung`, `rest` Euro-Texte (`euroDe`), `prozent` und `ziel` Zahlen, `schaetzung` Stundentext oder `null`. Zusätzlich wirft `texte.js` bei fehlenden Pflichtwerten (`datum`, `satz`, `fassung`, `nummer`, `prozent`, `anzahlung`, `rest`, `text`) einen `TypeError`, damit nie „undefined“ in einer Auftragsbestätigung steht; das steht nicht im Vertrag. Vorschlag: in V7 festschreiben.
|
||
4. **Frühbeginn-Erklärung in der Eingangsbestätigung K32 (L5): entschieden (siehe Übersicht, Nachträge; V6).** K32 gibt die Erklärung mit dem Platzhalter `fruehbeginn_text` wörtlich wieder (§ 356 Abs. 5 Nr. 2 Buchstabe b und § 357a Abs. 2 Nr. 2 BGB verlangen bei außerhalb von Geschäftsräumen geschlossenen Verträgen einen dauerhaften Datenträger) und enthält den verkürzten Satz „Ihr Widerrufsrecht bleibt davon unberührt“ nicht mehr (das Recht erlischt nach der Erklärung mit der vollständigen Erbringung, bis dahin schuldet der Kunde Wertersatz). Ebenfalls in V6 entschieden und im Freigabe-Dokument (Abschnitt 7) gezeigt: K31 ohne Team-BCC mit einer Kopie für das Team ohne Link (Block `teamkopie`), T22 mit dem Betreff-Präfix „[Aufträge]“, der Block `projekt` in K04 und T09. Das Freigabe-Skript prüft bei jedem Lauf die Zusagen zu K32, zur Kopie ohne Link und zu T22.
|
||
5. **V7 `WIDERRUFSFORMULAR` und `gruss`: Hinweis, keine Entscheidung nötig (L4, Vertragsfrage 6).** `zeilen` enthält die Aufzählungszeichen „– “ (Teil des gesetzlichen Musters) und als letzte Zeile „(*) Unzutreffendes streichen.“; `einleitung` ist die Zeile in Klammern. L4 gibt `zeilen` unverändert aus, jede Zeile als eigener Absatz. `gruss` ist nur die Grußformel („Mit freundlichen Grüßen“); den Absender setzt L4 aus `ABSENDER` darunter.
|
||
6. **Reihenfolge in Z1 (Merge): entschieden (siehe Übersicht, Integration).** Z1 führt L1, L2, L3, L4, L7 (ohne den Datenschutz-Commit), L5, L6 zusammen; damit liegt `agb-<AGB_FASSUNG>.pdf` bereit, das K31 anhängt und das die Mail-Tests von L5 brauchen.
|
||
7. **Wortlaut-Pins in anderen Strängen: gilt weiter.** Der Test von L0 (`test/auftraegeTextBausteine.test.js`) prüft nur Form und eingesetzte Werte, hält aber zwei Wortlaute fest: `BESTAETIGUNG_TEXT` (Zeile `assert.equal(T.BESTAETIGUNG_TEXT, …)`) und das Wort „Projektauftrag“ in `WIDERRUFSBELEHRUNG_PROJEKT`, `WIDERRUFSFORMULAR` und `FRUEHBEGINN_TEXT`. Die Endfassung dieses Plans erfüllt beide (`BESTAETIGUNG_TEXT` bleibt der Wortlaut der Spezifikation, `FRUEHBEGINN_TEXT` sagt „dieses Projektauftrags“); der Test von L0 ist gegen die Endfassung geprüft und grün. Wird die Alternative E2 (Häkchen mit Bezug auf die AGB) freigegeben, sind drei Stellen anzupassen: `texte.js`, die Zeile im Test von L0 und der Rückfalltext `BESTAETIGUNG_FALLBACK` der Bestätigungsseite (L6).
|
||
8. **Datenschutz und AGB sind sofort öffentlich: entschieden (siehe Übersicht, Nachträge und Integration).** `public/datenschutz.html`, das PDF und die neuen Dateien unter `public/rechtliches/` sind mit dem ersten Push von `main` erreichbar, auch bei `AUFTRAEGE_ENABLED=off`. Die neuen Dateien unter `public/rechtliches/` sind nirgends verlinkt (unkritisch). Die Datenschutzerklärung beschreibt dagegen eine Verarbeitung, die noch nicht läuft, in einem noch nicht freigegebenen Wortlaut. Deshalb ist der Datenschutz-Commit der letzte Commit des Strangs (Betreff „Datenschutz: Abschnitt 9a …“), Z1 führt `auftraege/l7-rechtstexte~1` zusammen, und er kommt erst zum Livegang der Auftragsbestätigung nach der Freigabe dazu (`git merge --no-ff auftraege/l7-rechtstexte`). Die Tasks 7 und 8 committen nichts; Korrekturen aus Task 8 gehören vor ihn (Task 8, Step 6). Der Branch und der Worktree `dev-auftraege-l7` bleiben bis dahin bestehen (das Freigabe-Skript liest den Worktree).
|
||
9. **Zusammenführung mit den Öffnungszeiten: Hinweis bleibt.** Der Zweig `feature/oeffnungszeiten` hat Abschnitt 6e und Änderungen an Abschnitt 6 und 7 als Entwurf vorbereitet. Dieser Strang legt seinen Abschnitt deshalb als **9a** an und ändert in Abschnitt 6 nur den Aufbewahrungsabsatz. Beide Zweige ändern dieselbe Zeile `Stand` in `public/datenschutz.html` und dieselbe Stand-Prüfung in `test/legalTexts.test.js`: Beim Zusammenführen den späteren Tag nehmen und das PDF neu drucken (auch Plan Z, Vertragsfrage 16).
|
||
10. **Löschung nach der Aufbewahrungsfrist: Hinweis bleibt (Plan Z, Vertragsfrage 8: nur melden, nichts bauen).** Der neue Abschnitt 9a sagt „Danach löschen wir sie.“ Einen Löschlauf für Auftragsbestätigungen sieht kein Plan vor (die erste Frist läuft frühestens Ende 2032 ab). Vorschlag: nach dem Livegang eine Aufgabe „Löschlauf Aufträge“ im Finanzen-Strang anlegen.
|
||
11. **WOMS-Karten unter „Rechtliches“ (Z2.4) und Adressen ohne Fassung: entschieden (siehe Übersicht, Nachträge: Karten laut Freigabe-Dokument).** Die Karte `widerrufsbelehrung-projekt` bekommt als `liveUrl` die versionierte Adresse `https://project.webklar.com/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.html` und wird bei jeder neuen Fassung angepasst (Abschnitt 9 des Freigabe-Dokuments nennt Schlüssel, Titel, Datei und Adresse). Ein Alias ohne Fassung wäre ein eigener kleiner Auftrag nach dem Livegang (zwei Sätze in `legalDocs.js`, Fassung aus `texte.js`). Die Karten `agb` und `datenschutz` bestehen; für sie kommen nur neue Versionen.
|
||
12. **Karte „Auftragsbestätigung (Textbausteine und Frühbeginn-Erklärung)“: entschieden (siehe Übersicht, Nachträge: eine Karte ohne Datei entfällt).** Die Wortlaute der Textbausteine stehen im PDF jeder Auftragsbestätigung (unveränderlich, mit Prüfsumme); das Häkchen und die Frühbeginn-Erklärung gehören in K32. Wer die Wortlaute je Fassung archivieren will, legt (Muster `scripts/termine-texte-archiv.mjs` und `docs/rechtliches/archiv/`) ein Archiv `auftraege-texte-<Fassung>.md` an; das ist ein eigener Auftrag.
|
||
13. **Weg „vor Ort“ bei Privatpersonen (§ 312f Abs. 1 BGB): entschieden (siehe Übersicht, V8 und Spezifikation, Nachträge).** Der AB-Dialog (W2) zeigt bei Weg „vor Ort“ und Kundenart Privatperson den Hinweis „zusätzlich ausdrucken und übergeben“; die Kanzlei-Frage bleibt E9 e im Freigabe-Dokument. `texte.js` bleibt unberührt.
|
||
14. **Neu: Das Freigabe-Dokument muss nach dem Zusammenführen neu erzeugt werden.** Abschnitt 7 liest die Stränge L5 und L6. Läuft Task 7 vor deren Fertigstellung (Phase B ist parallel), endet das Skript mit Code 3 und das Dokument trägt den Vermerk „unvollständig“. Vorschlag für Z: nach dem letzten Merge von Z1 und noch einmal in der Übergabe vor dem Schritt „Rechtsrunde“ `node /root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde-werkzeug/rechtsrunde-erzeugen.mjs` ausführen (erwartet Ende-Code 0; Voraussetzung: der Worktree `dev-auftraege-l7` steht sauber am Ende von `auftraege/l7-rechtstexte`). Das Skript rendert K31 für den Kunden mit dem Block `kundenmail`, wenn die Vorlage ihn kennt (L5, Vertragsfrage 12), und liest die Seitentexte aus allen Dateien unter `public/`, die L6 anlegt oder ändert; es braucht keine Namen von Funktionen aus L5 oder L6.
|
||
15. **Neu: Kostenanschlag, § 649 BGB bestätigt (zweiter Abruf am 06.10.2026).** Der Plan Z vermutet in seiner Vertragsfrage 21, die Regel stehe seit der Reform 2018 in § 650 BGB. Die amtliche Seite nennt § 649 „Kostenanschlag“ (Abs. 2: unverzügliche Anzeige bei zu erwartender Überschreitung); § 650 ist der Werklieferungsvertrag und der Verbrauchervertrag über digitale Produkte. Die Spezifikation und `AB_TEXTE.aufwand` sind richtig, der Nachweis steht in `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt` (Test `normen_kostenanschlag`). Vorschlag: Z, Vertragsfrage 21 streichen und Prüfer 3 nicht nach einer „alten Zählung“ suchen lassen. Der Befund zu § 650 Abs. 2 (Mängel und Abnahme bei Verbraucherverträgen über digitale Inhalte) steht als Kanzlei-Frage in E9 a und d.
|