Rechtstexte: Widerrufsbelehrung der Funktionen aus dem Gesetzesmuster

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 02:35:04 +00:00
1 parent 3073fffcf5
commit 6b4cbb3204
3 files changed
+268 -7

No files matched your search

+179
View File
@@ -1,10 +1,14 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { config } from '../server/config.js'
import {
RECHTSTEXTE_VERSION,
RECHTSTEXTE_ENTWURF,
WEBKLAR_KONTAKT,
widerrufsbelehrung,
widerrufsbelehrungFunktion,
formularHinweis,
datenschutzAbsatz,
ZUSTIMMUNG_VORZEITIG,
@@ -481,3 +485,178 @@ test('6c-Entwurf: nur als Entwurfsdatei, public/datenschutz.html bleibt unverän
const html = lies('public/datenschutz.html')
assert.ok(!/6c\./.test(html) && !html.includes('id="termine"'), '6c darf vor TP2 nicht in public/datenschutz.html stehen')
})
// ---------------------------------------------------------------- Widerrufsbelehrung der Funktionen (WEBklar als Unternehmer, Task F3)
const KONTAKT_WEBKLAR = 'WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon: 0176 23726355, E-Mail: support@webklar.com'
const AN_ZEILE_WEBKLAR = 'An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:'
const WIDERRUFEN_URL = 'https://project.webklar.com/widerrufen.html'
const funktion = () => widerrufsbelehrungFunktion()
test('Gäste-Belehrung bleibt byte-gleich (Hash der Ausgabe vor Task F3)', () => {
const text = widerrufsbelehrung(BETRIEB)
assert.equal(createHash('sha256').update(text).digest('hex'), 'a4b3549b7b4ba0bdeeca438bc823d07eede5e4aa35e21e335fb5304ff36a1ff6')
assert.equal(text.length, 2983)
})
test('WEBKLAR_KONTAKT: WEBklar GbR als Unternehmer', () => {
assert.deepEqual(WEBKLAR_KONTAKT, {
name: 'WEBklar GbR',
adresse: 'Am Schwimmbad 10, 67722 Winnweiler',
telefon: '0176 23726355',
email: 'support@webklar.com',
})
})
test('belehrung_funktion_muster_woertlich: jeder Absatz der Anlage 1 für Dienstleistungen steht wörtlich darin, nur die Lücken sind gefüllt', () => {
const text = funktion()
const muster = musterZeilen()
assert.equal(muster.length, 8)
assert.equal(fristBaustein(), 'des Vertragsabschlusses.')
for (const zeile of muster) {
const erwartet = zeile
.replace('ab dem Tag .', `ab dem Tag ${fristBaustein()}`)
.replace('uns ( )', `uns (${KONTAKT_WEBKLAR})`)
assert.ok(text.includes(erwartet), `Absatz fehlt oder ist verändert: ${erwartet}`)
}
// Fristbeginn und Kontakt stehen genau einmal
assert.equal(text.split('ab dem Tag des Vertragsabschlusses.').length - 1, 1)
assert.equal(text.split(KONTAKT_WEBKLAR).length - 1, 1)
})
test('belehrung_funktion_widerrufsfunktion: Satzrahmen der Anlage 1 wörtlich, die Lücke ist die Adresse der Widerrufsfunktion', () => {
const text = funktion()
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')
const absatz = `${m[1]} ${WIDERRUFEN_URL} ${m[2]} ${m[3]}`
assert.ok(text.includes(absatz), 'Absatz zur Widerrufsfunktion fehlt oder weicht vom Satzrahmen der Anlage ab')
assert.equal(text.split(WIDERRUFEN_URL).length - 1, 1)
// kein Hinweis auf eine Schaltfläche der Gäste-Fassung
assert.ok(!text.includes('Termin verwalten') && !text.includes('Schaltfläche'))
// der Wertersatz für Dienstleistungen steht wie in der Gäste-Fassung
const w = /Haben Sie verlangt, dass die Dienstleistungen oder Lieferung von Wasser\/Gas\/Strom\/Fernwärme \[Unzutreffendes streichen\] (während[^“]+?)\s*“/.exec(ANLAGE_1)
assert.ok(w)
assert.ok(text.includes(`Haben Sie verlangt, dass die Dienstleistungen ${w[1]}`))
})
test('belehrung_funktion_formular: Muster-Widerrufsformular der Anlage 2 wörtlich, „An“-Zeile mit WEBklar', () => {
const text = funktion()
const z = zeilen(ANLAGE_2)
const start = z.indexOf('Muster-Widerrufsformular')
assert.ok(start >= 0)
const inhalt = z.slice(start).filter((s) => s && s !== '–')
let anZeile = 0
for (const zeile of inhalt) {
if (zeile.startsWith('An [hier ist')) {
anZeile += 1
assert.ok(text.includes(`– ${AN_ZEILE_WEBKLAR}`), '„An“-Zeile mit WEBklar fehlt')
} else {
assert.ok(text.includes(zeile), `Formularzeile fehlt oder ist verändert: ${zeile}`)
}
}
assert.equal(anZeile, 1)
assert.equal(text.split(AN_ZEILE_WEBKLAR).length - 1, 1)
})
test('Belehrung der Funktionen: reiner Text ohne Platzhalter, deterministisch, Sie-Form, nichts zu Waren', () => {
const text = funktion()
assert.equal(typeof text, 'string')
assert.ok(text.startsWith('Widerrufsbelehrung\n\nWiderrufsrecht\n'))
assert.ok(text.includes('\n') && !text.includes('\r'))
assert.ok(!/\{\{|\}\}|\[|\]|…|\.\.\.|\(\s*\)/.test(text), 'offener Platzhalter')
assert.ok(!/undefined|null|NaN/.test(text))
assert.ok(!/Rücksendung|zurückzusenden|Besitz genommen|Wir holen die Waren|Sie haben die Waren|elektronisch ausfüllen|Wir können die Rückzahlung verweigern/.test(text))
assert.ok(!DU_WOERTER.test(text))
assert.ok(!/&amp;|<[a-z/]/i.test(text))
assert.equal(funktion(), funktion())
// Gäste-Fassung und Funktionen-Fassung unterscheiden sich nur in Kontakt, Widerrufsfunktion und „An“-Zeile
const gast = widerrufsbelehrung(WEBKLAR_KONTAKT)
assert.notEqual(gast, text)
const ohneUnterschied = (t) => t.replace(/uns \([^)]*\)/, 'uns (K)').replace(/Sie können Ihr Widerrufsrecht auch online unter .*? ausüben\./, 'WF').replace(/– An [^\n]*/, '– An A')
assert.equal(ohneUnterschied(gast), ohneUnterschied(text))
})
/** Text eines HTML-Ausschnitts: Tags weg, Entitäten der Seite aufgelöst, Leerraum zu einem Leerzeichen. */
const klartext = (html) =>
html
.replace(/<[^>]+>/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim()
test('belehrung_gleich_abo: Belehrung der Funktionen = Belehrung in den Abo-Bedingungen (Drift-Wächter), Muster-Formular ebenso', () => {
const html = lies('public/abo-bedingungen.html')
const von = html.indexOf('<h3>Widerrufsrecht</h3>')
const bis = html.indexOf('Ende der Widerrufsbelehrung')
assert.ok(von > 0 && bis > von, 'Aufbau der Abo-Bedingungen hat sich geändert')
// Die Kontaktklammer wird vorher in beiden durch einen Platzhalter ersetzt: die Gäste-Bausteine setzen „Telefon: …, E-Mail: …“ mit
// Doppelpunkt, die Abo-Bedingungen ohne
const klammer = (t) => t.replace(/uns \([^)]*\)/, 'uns (KONTAKT)')
const abo = klammer(klartext(html.slice(von, bis)))
const text = funktion()
const formularKopf = text.indexOf('\n\nMuster-Widerrufsformular\n\n')
assert.ok(formularKopf > 0)
const textBelehrung = text.slice(text.indexOf('Widerrufsrecht'), formularKopf)
assert.equal(klammer(klartext(textBelehrung)), abo)
// Muster-Widerrufsformular: Kasten der Abo-Bedingungen gegen die Formularzeilen, „An“-Zeile vorher ersetzt
const formVon = html.indexOf('<h3 id="formular">Muster-Widerrufsformular</h3>')
const formBis = html.indexOf('<h2 id="widerruf-hinweise">')
assert.ok(formVon > bis && formBis > formVon)
const an = (t) => t.replace(/– An .*?:(?= – Hiermit)/, '– An ANSCHRIFT:')
const aboForm = an(klartext(html.slice(formVon, formBis)).replace(/^Muster-Widerrufsformular /, ''))
const textForm = an(klartext(text.slice(formularKopf + 2).replace(/^Muster-Widerrufsformular\n+/, '')))
assert.equal(textForm, aboForm)
assert.ok(aboForm.includes('– An ANSCHRIFT:') && textForm.includes('– An ANSCHRIFT:'))
})
// ---------------------------------------------------------------- Dokument widerrufsbelehrung-funktionen.md
const DOKU = 'docs/rechtliches/widerrufsbelehrung-funktionen.md'
/** Absätze eines Textes: durch Leerzeilen getrennt, jede Zeile eines Absatzes für sich (Formularzeilen), Überschriften-Zeichen weg. */
const absaetze = (text) =>
text
.replace(/\r/g, '')
.split(/\n{2,}/)
.flatMap((block) => block.split('\n'))
.map((z) => z.replace(/^#+\s+/, '').trim())
.filter(Boolean)
test('belehrung_dokument_gepinnt: Textblock der Datei = Ausgabe von widerrufsbelehrungFunktion(), Kopf mit Fassung, keine Platzhalter', () => {
const md = lies(DOKU).replace(/\r/g, '')
const zeilenMd = md.split('\n')
assert.equal(zeilenMd[0], '# Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)')
assert.equal(zeilenMd[2], `**Fassung ${config.termine.avvVersion}**`)
assert.ok(!/\[|\]|…|\{\{|\}\}/.test(md), 'offener Platzhalter')
assert.ok(!/ENTWURF|Entwurf|Prüfpunkt|TODO|<!--/.test(md))
assert.ok(md.includes('Das Widerrufsrecht gilt nur für Verbraucher.'))
// Belehrung und Formular stehen als Überschriften und Absätze (Markdown ohne Codeblock), wörtlich und in Reihenfolge
const von = md.indexOf('## Widerrufsbelehrung')
const bis = md.indexOf('## Hinweise zum Widerruf')
assert.ok(von > 0 && bis > von, 'Aufbau der Datei: Abschnitte „Widerrufsbelehrung“ … „Hinweise zum Widerruf“')
assert.deepEqual(absaetze(md.slice(von, bis)), absaetze(widerrufsbelehrungFunktion()))
// keine Codeblöcke, Zitate oder Tabellen im Belehrungsteil
assert.ok(!/```|^>|^\|/m.test(md.slice(von, bis)))
})
test('belehrung_dokument_hinweise: vier Hinweise zum Widerruf, nicht Teil der gesetzlichen Belehrung', () => {
const md = lies(DOKU).replace(/\r/g, '')
const hinweise = md.slice(md.indexOf('## Hinweise zum Widerruf'))
assert.match(hinweise, /nicht zur gesetzlichen Widerrufsbelehrung/)
const punkte = hinweise.split('\n').filter((z) => z.startsWith('- '))
assert.equal(punkte.length, 4)
assert.match(punkte[0], /jede Bestellung einer Funktion gesondert/)
assert.match(punkte[0], /Website-Projekt/)
assert.match(punkte[1], /endet diese Funktion/)
assert.match(punkte[1], /Wartungs-Abo bleibt davon unberührt/)
assert.match(punkte[2], /ausdrücklichen Wunsch/)
assert.match(punkte[2], /angemessenen Betrag für die bis dahin erbrachte Leistung/)
assert.match(punkte[2], /Anteil am Gesamtpreis/)
assert.match(punkte[2], /Arbeitsblatt erfasste Aufwand/)
assert.match(punkte[3], /ursprüngliche Zahlungsmittel/)
assert.match(punkte[3], /Stripe/)
assert.ok(!DU_WOERTER.test(md))
})