Terminplaner: Rechtstext-Bausteine fuer Gaeste (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ad8515d60
commit
459efff10a
6 files changed
+703
No files matched your search
@@ -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('&') && !/<[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')
|
||||
})
|
||||
Reference in new issue
Block a user