Terminplaner: Rechtstext-Bausteine fuer Gaeste (Entwurf)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:43 +00:00
1 parent 9ad8515d60
commit 459efff10a
6 files changed
+703

No files matched your search

+272
View File
@@ -0,0 +1,272 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import {
RECHTSTEXTE_VERSION,
widerrufsbelehrung,
formularHinweis,
ZUSTIMMUNG_VORZEITIG,
ZUSTIMMUNG_STORNO,
ZUSTIMMUNG_ERINNERUNG,
GESUNDHEIT_HINWEIS,
} from '../server/services/termine/rechtstexte.js'
// ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen.
// Die Belehrung wird gegen den Wortlaut der Anlagen 1 und 2 zu Art. 246a EGBGB geprueft (docs/rechtliches/quellen/,
// abgerufen 2026-10-02): jeder Absatz des Musters muss unveraendert im Ergebnis stehen, nur die Luecken sind gefuellt.
const KOPF = 'ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen'
const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8')
const zeilen = (text) => text.split('\n').map((z) => z.trim())
const BETRIEB = { name: 'Salon Müller & Söhne', adresse: 'Hauptstraße 1, 67722 Winnweiler', telefon: '06301 1234', email: 'info@salon-mueller.de' }
const KONTAKT = 'Salon Müller & Söhne, Hauptstraße 1, 67722 Winnweiler, Telefon: 06301 1234, E-Mail: info@salon-mueller.de'
// Platzhalter-Betrieb: so steht die Belehrung in docs/rechtliches/termine-widerrufsbelehrung-muster.md
const PLATZHALTER = { name: '{{Name des Betriebs}}', adresse: '{{Anschrift}}', telefon: '{{Telefon}}', email: '{{E-Mail}}' }
const ANLAGE_1 = lies('docs/rechtliches/quellen/anlage-1.txt')
const ANLAGE_2 = lies('docs/rechtliches/quellen/anlage-2.txt')
/** Muster-Absaetze der Anlage 1 (zwischen Titel und Gestaltungshinweisen) mit den Luecken des Gesetzestexts. */
function musterZeilen() {
const z = zeilen(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 geaendert')
return z.slice(von, bis).filter(Boolean)
}
/** Textbaustein a) fuer Dienstleistungsvertraege: „des Vertragsabschlusses.“ */
function fristBaustein() {
const m = /Dienstleistungsvertrags[^„]*„([^“]+)“/.exec(ANLAGE_1)
assert.ok(m, 'Gestaltungshinweis a) fehlt in anlage-1.txt')
return m[1]
}
const belehrung = (betrieb = BETRIEB) => widerrufsbelehrung(betrieb)
test('Fassung der Rechtstexte', () => {
assert.equal(RECHTSTEXTE_VERSION, '2026-10-02')
})
// ---------------------------------------------------------------- Widerrufsbelehrung
test('Belehrung: alle Muster-Absaetze der Anlage 1 stehen wörtlich darin, nur die Lücken sind gefüllt', () => {
const text = belehrung()
const muster = musterZeilen()
assert.equal(muster.length, 8, 'Anlage 1: erwartet Titel, Widerrufsrecht, 3 Absätze, Folgen des Widerrufs und 1 Absatz')
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})`)
assert.ok(text.includes(erwartet), `Absatz fehlt oder ist verändert: ${erwartet}`)
}
})
test('Belehrung: Widerrufsfunktion als „anderer geeigneter Hinweis“, Rest des Gesetzeswortlauts unverändert', () => {
const text = belehrung()
const m = /Wenn Sie diese Online-Funktion nutzen, ([^“]+?)\s*“/.exec(ANLAGE_1)
assert.ok(m, 'Gestaltungshinweis zur Widerrufsfunktion fehlt in anlage-1.txt')
assert.ok(m[1].includes('Eingangsbestätigung') && m[1].includes('Uhrzeit ihres Eingangs'))
const absatz = 'Sie können Ihr Widerrufsrecht auch online über die Schaltfläche „Vertrag widerrufen“ ausüben, '
+ 'die Sie im Terminbereich auf unserer Website und über den Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail finden. '
+ `Wenn Sie diese Online-Funktion nutzen, ${m[1]}`
assert.ok(text.includes(absatz), 'Absatz zur Widerrufsfunktion fehlt oder weicht ab')
assert.ok(text.includes('Vertrag widerrufen'))
assert.ok(!text.includes('online unter'), 'keine Internetadresse, kein Parameter')
assert.ok(!/https?:\/\//.test(text))
})
test('Belehrung: Wertersatz für Dienstleistungen, Unzutreffendes (Wasser/Gas/Strom/Fernwärme) gestrichen', () => {
const text = belehrung()
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')
assert.ok(text.includes(`Haben Sie verlangt, dass die Dienstleistungen ${m[1]}`), 'Wertersatz-Absatz fehlt oder weicht ab')
assert.ok(!/Wasser|Strom|Fernwärme|\[Unzutreffendes/.test(text))
})
test('Belehrung: Muster-Widerrufsformular der Anlage 2 steht wörtlich darin, Betrieb in der „An“-Zeile', () => {
const text = belehrung()
const z = zeilen(ANLAGE_2)
const start = z.indexOf('Muster-Widerrufsformular')
assert.ok(start >= 0, 'Aufbau von anlage-2.txt hat sich geaendert')
const inhalt = z.slice(start).filter((s) => s && s !== '–')
assert.ok(inhalt.length >= 9)
let anZeile = 0
for (const zeile of inhalt) {
if (zeile.startsWith('An [hier ist')) {
anZeile += 1
assert.ok(text.includes('An Salon Müller & Söhne, Hauptstraße 1, 67722 Winnweiler, info@salon-mueller.de:'), '„An“-Zeile mit Betrieb fehlt')
} else {
assert.ok(text.includes(zeile), `Formularzeile fehlt oder ist verändert: ${zeile}`)
}
}
assert.equal(anZeile, 1)
// die (*)-Alternativen bleiben unverändert stehen
assert.ok(text.includes('Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)'))
assert.ok(text.includes('(*) Unzutreffendes streichen.'))
})
test('Belehrung: Betriebsdaten, kein offener Platzhalter, nichts zu Waren-Rücksendung oder Webformular', () => {
const text = belehrung()
for (const wert of Object.values(BETRIEB)) assert.ok(text.includes(wert), `Betriebsangabe fehlt: ${wert}`)
assert.ok(text.includes('Vertrag widerrufen'))
assert.ok(text.includes('Termin verwalten'))
assert.ok(text.includes('vierzehn Tagen'))
assert.ok(!/\{\{|\}\}|\[|\]|…|\.\.\.|\(\s*\)/.test(text), 'offener Platzhalter')
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(!/undefined|null|NaN/.test(text))
})
test('Belehrung: reiner Text mit Zeilenumbrüchen, nicht HTML-maskiert (die Mail maskiert beim Einsetzen)', () => {
const text = belehrung()
assert.equal(typeof text, 'string')
assert.ok(text.includes('\n'))
assert.ok(text.startsWith('Widerrufsbelehrung\n'))
assert.ok(text.includes('Salon Müller & Söhne'))
assert.ok(!text.includes('&amp;') && !/<[a-z/]/i.test(text))
assert.ok(!text.includes('\r'))
assert.equal(belehrung(), belehrung(), 'deterministisch')
})
test('Belehrung: fehlende Telefonnummer oder Anschrift hinterlässt keine leeren Klammern oder Kommas', () => {
const ohneTel = belehrung({ ...BETRIEB, telefon: '' })
assert.ok(ohneTel.includes('uns (Salon Müller & Söhne, Hauptstraße 1, 67722 Winnweiler, E-Mail: info@salon-mueller.de)'))
assert.ok(!ohneTel.includes('Telefon:'))
const ohneAdresse = belehrung({ ...BETRIEB, adresse: '', telefon: undefined })
assert.ok(ohneAdresse.includes('uns (Salon Müller & Söhne, E-Mail: info@salon-mueller.de)'))
assert.ok(ohneAdresse.includes('An Salon Müller & Söhne, info@salon-mueller.de:'))
assert.ok(!/,\s*,|\(\s*,|,\s*\)|:\s*,/.test(ohneAdresse))
})
test('Belehrung: Betriebsname und E-Mail sind Pflicht, Leerraum in Angaben wird zu einem Leerzeichen', () => {
assert.throws(() => widerrufsbelehrung({ ...BETRIEB, name: '' }), TypeError)
assert.throws(() => widerrufsbelehrung({ ...BETRIEB, email: undefined }), TypeError)
assert.throws(() => widerrufsbelehrung(undefined), TypeError)
assert.throws(() => widerrufsbelehrung(null), TypeError)
const text = belehrung({ ...BETRIEB, name: ' Salon \n Müller ', adresse: 'Hauptstraße 1,\n67722 Winnweiler' })
assert.ok(text.includes('uns (Salon Müller, Hauptstraße 1, 67722 Winnweiler, Telefon:'))
})
test('Belehrung ist die Quelle von docs/rechtliches/termine-widerrufsbelehrung-muster.md (Platzhalter-Fassung steht dort wörtlich)', () => {
const md = lies('docs/rechtliches/termine-widerrufsbelehrung-muster.md')
assert.ok(md.replace(/\r/g, '').includes(widerrufsbelehrung(PLATZHALTER)), 'Textblock im Muster-Dokument weicht von widerrufsbelehrung() ab')
})
// ---------------------------------------------------------------- Zustimmungen und Hinweise
test('Zustimmungstexte wortgleich zu den Vorgaben (Sie-Form)', () => {
assert.equal(
ZUSTIMMUNG_VORZEITIG('Salon Müller'),
'Ich verlange ausdrücklich, dass Salon Müller mit der Leistung vor Ablauf der Widerrufsfrist 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).',
)
assert.equal(ZUSTIMMUNG_STORNO, 'Ich habe die Stornobedingungen gelesen.')
assert.equal(ZUSTIMMUNG_ERINNERUNG(24), 'Ich möchte 24 Stunden vor dem Termin eine Erinnerung per E-Mail erhalten.')
assert.equal(ZUSTIMMUNG_ERINNERUNG(2), 'Ich möchte 2 Stunden vor dem Termin eine Erinnerung per E-Mail erhalten.')
assert.equal(GESUNDHEIT_HINWEIS, 'Bitte keine Gesundheitsangaben.')
})
test('Zustimmung zum vorzeitigen Beginn setzt den Betriebsnamen unverändert ein (kein Ersetzungsmuster)', () => {
const text = ZUSTIMMUNG_VORZEITIG("Frisör $& $1 'Anna'")
assert.ok(text.startsWith("Ich verlange ausdrücklich, dass Frisör $& $1 'Anna' mit der Leistung vor Ablauf"))
})
const EINSTELLUNG = { anrede: 'sie', aufbewahrungTage: 90, betrieb: BETRIEB }
test('Formular-Hinweis (Sie): Zweck, Auftragsverarbeiter, Löschfristen, keine Cookies, Gesundheit, Verantwortlicher', () => {
const text = formularHinweis(EINSTELLUNG)
assert.ok(text.startsWith('Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO).'))
assert.ok(text.includes('in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland)'))
assert.ok(text.includes('Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist.'))
assert.ok(text.includes('Es werden keine Cookies gesetzt.'))
assert.ok(text.includes('Bitte machen Sie keine Angaben zu Ihrer Gesundheit.'))
assert.ok(text.includes('Mehr erfahren Sie in unserer Datenschutzerklärung.'))
assert.ok(text.endsWith('Verantwortlich: Salon Müller & Söhne, Hauptstraße 1, 67722 Winnweiler.'))
assert.ok(!/\{\{|\}\}|\[|\]|undefined|null/.test(text))
assert.ok(!text.includes('info@salon-mueller.de'), 'Betriebs-E-Mail gehört nicht in die öffentliche Konfiguration')
assert.ok(!/\bdein|\bdu\b/i.test(text))
})
test('Formular-Hinweis (Du): gleiche Aussagen in Du-Form', () => {
const text = formularHinweis({ ...EINSTELLUNG, anrede: 'du', aufbewahrungTage: 30 })
assert.ok(text.startsWith('Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO).'))
assert.ok(text.includes('Wir speichern deine Daten bis 30 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist.'))
assert.ok(text.includes('Bitte mach keine Angaben zu deiner Gesundheit.'))
assert.ok(text.includes('Mehr erfährst du in unserer Datenschutzerklärung.'))
assert.ok(text.includes('Es werden keine Cookies gesetzt.'))
assert.ok(!/\bSie\b|\bIhre|\bIhren/.test(text))
})
test('Formular-Hinweis: Sie ist die Vorgabe, Aufbewahrung 90 Tage ohne gültigen Wert, Adresse optional', () => {
const text = formularHinweis({ betrieb: { name: 'Salon Müller', email: 'info@salon.de' } })
assert.ok(text.includes('Wir speichern Ihre Daten bis 90 Tage nach dem Termin'))
assert.ok(text.endsWith('Verantwortlich: Salon Müller.'))
assert.ok(formularHinweis({ ...EINSTELLUNG, aufbewahrungTage: 'viel' }).includes('bis 90 Tage'))
assert.ok(formularHinweis({ ...EINSTELLUNG, aufbewahrungTage: 365 }).includes('bis 365 Tage'))
assert.ok(formularHinweis({ ...EINSTELLUNG, anrede: 'unbekannt' }).includes('Ihre Angaben'))
assert.throws(() => formularHinweis({}), TypeError)
assert.throws(() => formularHinweis(undefined), TypeError)
})
// ---------------------------------------------------------------- Dokumente
const DOKUMENTE = [
'docs/rechtliches/termine-widerrufsbelehrung-muster.md',
'docs/rechtliches/termine-datenschutz-absatz.md',
'docs/rechtliches/termine-hinweise.md',
'docs/rechtliches/termine-datenschutz-6c-entwurf.md',
]
test('Jedes Rechtstext-Dokument beginnt mit dem Entwurfs-Kopf', () => {
for (const rel of DOKUMENTE) {
assert.equal(lies(rel).split('\n')[0].trim(), KOPF, rel)
}
})
test('Hinweise-Dokument: Formular-Hinweis (Sie/Du) und alle Zustimmungstexte stehen wörtlich darin', () => {
const md = lies('docs/rechtliches/termine-hinweise.md').replace(/\r/g, '')
const platzhalter = { betrieb: { name: '{{Firma}}', adresse: '{{Anschrift}}', email: '{{E-Mail}}' }, aufbewahrungTage: 90 }
assert.ok(md.includes(formularHinweis({ ...platzhalter, anrede: 'sie' })), 'Formular-Hinweis (Sie) weicht ab')
assert.ok(md.includes(formularHinweis({ ...platzhalter, anrede: 'du' })), 'Formular-Hinweis (Du) weicht ab')
assert.ok(md.includes(ZUSTIMMUNG_VORZEITIG('{{Firma}}')))
assert.ok(md.includes(ZUSTIMMUNG_STORNO))
assert.ok(md.includes(ZUSTIMMUNG_ERINNERUNG('{{n}}')))
assert.ok(md.includes(GESUNDHEIT_HINWEIS))
for (const knopf of ['Zahlungspflichtig buchen', 'Termin buchen', 'Termin anfragen', 'Vertrag widerrufen', 'Widerruf bestätigen']) {
assert.ok(md.includes(knopf), `Knopftext fehlt: ${knopf}`)
}
})
test('Datenschutz-Absatz: Löschfristen der Vorgaben, Sie- und Du-Fassung, Auftragsverarbeiter und Gesundheitshinweis', () => {
const md = lies('docs/rechtliches/termine-datenschutz-absatz.md').replace(/\r/g, '')
assert.ok(/Fassung Sie/i.test(md) && /Fassung Du/i.test(md))
assert.ok(md.includes('Online-Terminbuchung'))
// jede Fassung nennt die vier Fristen: nicht bestätigt 48 Stunden, abgesagt 30 Tage, vergangen {{Tage}} Tage, Abschaltung 30 Tage
const fassungen = md.split(/^## Fassung /m).slice(1)
assert.equal(fassungen.length, 2)
for (const f of fassungen) {
assert.ok(f.includes('48 Stunden'), 'Frist nicht bestätigter Buchungen fehlt')
assert.ok(f.includes('{{Tage}} Tage nach dem Termin'), 'Aufbewahrung vergangener Termine fehlt')
assert.ok(/abgesagte[^.]*30 Tage/.test(f), 'Frist abgesagter Termine fehlt')
assert.ok(/abgeschaltet[^.]*30 Tage|30 Tage nach dem Abschalten/.test(f), 'Frist nach Abschaltung fehlt')
assert.ok(f.includes('WEBklar GbR') && f.includes('Art. 28 DSGVO') && f.includes('Hetzner'))
assert.ok(f.includes('Gesundheit'))
assert.ok(f.includes('Art. 6 Abs. 1 lit. b DSGVO') && f.includes('Art. 6 Abs. 1 lit. a DSGVO'))
}
assert.ok(fassungen[0].startsWith('Sie') && fassungen[1].startsWith('Du'))
assert.ok(!/\bdein|\bdu\b|\bdir\b|\bdich\b/i.test(fassungen[0]), 'Du-Form in der Sie-Fassung')
assert.ok(!/\bIhr|\bIhnen\b|\bSie (haben|können|erhalten|wünschen|machen|buchen|klicken|sind|wollen)\b/.test(fassungen[1]), 'Sie-Form in der Du-Fassung')
})
test('6c-Entwurf: nur als Entwurfsdatei, public/datenschutz.html bleibt unverändert (ohne Abschnitt 6c)', () => {
const md = lies('docs/rechtliches/termine-datenschutz-6c-entwurf.md')
assert.ok(md.includes('6c. Online-Terminbuchung'))
assert.ok(md.includes('Auftragsverarbeiter'))
assert.ok(/nicht (in|auf) .*(einbauen|veröffentlich)|nicht eingebaut|nicht veröffentlicht/i.test(md))
// TP2 baut 6c ein und entfernt diese Absicherung
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')
})