From 495d5ad9ca3a8f4ff9eb744735d7487a4ebccff1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 04:08:46 +0000 Subject: [PATCH] =?UTF-8?q?G=C3=A4stetexte=20des=20Terminplaners:=20Entwur?= =?UTF-8?q?f-Kennzeichnung=20entfernt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- docs/funktionen/termine-einbau.md | 2 +- ...c-entwurf.md => termine-datenschutz-6c.md} | 2 - .../rechtliches/termine-datenschutz-absatz.md | 13 +------ docs/rechtliches/termine-hinweise.md | 9 ----- .../termine-widerrufsbelehrung-muster.md | 11 ------ server/services/termine/einbauanleitung.js | 10 +++-- server/services/termine/rechtstexte.js | 9 +++-- test/funktionenTermine.test.js | 7 +++- test/termineEinbauanleitung.test.js | 39 ++++++++++++++++++- test/termineEmbedStatic.test.js | 2 +- test/termineRechtstexte.test.js | 31 +++++++++++---- 11 files changed, 79 insertions(+), 56 deletions(-) rename docs/rechtliches/{termine-datenschutz-6c-entwurf.md => termine-datenschutz-6c.md} (99%) diff --git a/docs/funktionen/termine-einbau.md b/docs/funktionen/termine-einbau.md index 4f892b8..4703ae6 100644 --- a/docs/funktionen/termine-einbau.md +++ b/docs/funktionen/termine-einbau.md @@ -124,7 +124,7 @@ Bedienung nur mit der Tastatur: Tab und Umschalt+Tab zwischen den Elementen, im ## 6. Datenschutzerklärung des Kunden -Das Skript lädt beim Aufruf der Seite die Konfiguration (Texte, Leistungen) von `project.webklar.com`; im Modus `inline` folgen die Termine beim Durchklicken, im Modus `knopf` erst beim Klick auf den Knopf. Dabei sieht der Server die IP-Adresse des Besuchers. Die Datenschutzerklärung der Website braucht deshalb den Absatz „Online-Terminbuchung“ aus `docs/rechtliches/termine-datenschutz-absatz.md` (zwei Fassungen: Sie und Du, nimm die zur Erklärung der Website passende). Der Absatz trägt den Kopf **„ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen“**. Solange der Kopf dort steht, gilt: nur beim Pilotkunden einsetzen, bei anderen Betrieben erst nach der Rechtsprüfung. Die `[[wenn …]]`-Sätze richten sich nach den Einstellungen des Betriebs (Telefon-Feld, Nachricht, Erinnerung, Verbraucher). Bau das Widget erst ein, wenn der Absatz in der Datenschutzerklärung steht. Die URL der Datenschutzerklärung (und des Impressums) muss in den Betriebsangaben im Portal stehen, sonst fehlt der Link im Fuß. +Das Skript lädt beim Aufruf der Seite die Konfiguration (Texte, Leistungen) von `project.webklar.com`; im Modus `inline` folgen die Termine beim Durchklicken, im Modus `knopf` erst beim Klick auf den Knopf. Dabei sieht der Server die IP-Adresse des Besuchers. Die Datenschutzerklärung der Website braucht deshalb den Absatz „Online-Terminbuchung“ aus `docs/rechtliches/termine-datenschutz-absatz.md` (zwei Fassungen: Sie und Du, nimm die zur Erklärung der Website passende). Die `[[wenn …]]`-Sätze richten sich nach den Einstellungen des Betriebs (Telefon-Feld, Nachricht, Erinnerung, Verbraucher). Bau das Widget erst ein, wenn der Absatz in der Datenschutzerklärung steht. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar den Absatz beim Einbau ein (Teil der Einrichtung, nicht auf das Stundenkontingent angerechnet); sonst erhält der Kunde den Absatz als Textbaustein per Chat und fügt ihn selbst ein. Die URL der Datenschutzerklärung (und des Impressums) muss in den Betriebsangaben im Portal stehen, sonst fehlt der Link im Fuß. ## 7. Abnahme-Checkliste diff --git a/docs/rechtliches/termine-datenschutz-6c-entwurf.md b/docs/rechtliches/termine-datenschutz-6c.md similarity index 99% rename from docs/rechtliches/termine-datenschutz-6c-entwurf.md rename to docs/rechtliches/termine-datenschutz-6c.md index d049801..a8839ad 100644 --- a/docs/rechtliches/termine-datenschutz-6c-entwurf.md +++ b/docs/rechtliches/termine-datenschutz-6c.md @@ -1,5 +1,3 @@ -ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen - # Datenschutzerklärung von WEBklar: Abschnitt 6c „Online-Terminbuchung auf Websites unserer Kunden“ Fassung 2026-10-02. Entwurf für `public/datenschutz.html`, **nicht eingebaut und nicht veröffentlicht**: Die Datei `public/datenschutz.html` bleibt bis TP2 unverändert (dort: Livegang des Terminplaners, Prüfrunde wie bei der Website-Analyse). Der Abschnitt beschreibt, was WEBklar selbst verarbeitet: als Auftragsverarbeiter für die Gäste der Betriebe und als Verantwortlicher für die Betriebsdaten im Kundenportal. Den Text für die Website des Betriebs enthält `termine-datenschutz-absatz.md`. diff --git a/docs/rechtliches/termine-datenschutz-absatz.md b/docs/rechtliches/termine-datenschutz-absatz.md index a518531..43dded2 100644 --- a/docs/rechtliches/termine-datenschutz-absatz.md +++ b/docs/rechtliches/termine-datenschutz-absatz.md @@ -1,8 +1,6 @@ -ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen - # Datenschutz-Absatz „Online-Terminbuchung“ für die Website des Betriebs -Fassung 2026-10-05. Grundlage: `recherche-recht.md` Abschnitt 11.1. Der Absatz gehört in die Datenschutzerklärung der Website des **Betriebs** (der Betrieb ist Verantwortlicher, WEBklar Auftragsverarbeiter). Die Einbauanleitung für Mitarbeiter (`docs/funktionen/termine-einbau.md`, TP2) verweist hierher. Für die Datenschutzerklärung von WEBklar selbst gilt `termine-datenschutz-6c-entwurf.md`. +Fassung 2026-10-05. Grundlage: `recherche-recht.md` Abschnitt 11.1. Der Absatz gehört in die Datenschutzerklärung der Website des **Betriebs** (der Betrieb ist Verantwortlicher, WEBklar Auftragsverarbeiter). Die Einbauanleitung für Mitarbeiter (`docs/funktionen/termine-einbau.md`, TP2) verweist hierher. Für die Datenschutzerklärung von WEBklar selbst gilt `termine-datenschutz-6c.md`. Zwei Fassungen weiter unten: Sie und Du. Nimm die Fassung, die zur Datenschutzerklärung der Website passt (in der Regel zur `anrede` des Betriebs). @@ -30,15 +28,6 @@ Zwei Fassungen weiter unten: Sie und Du. Nimm die Fassung, die zur Datenschutzer 4. **Nachweise**: Gespeichert werden auch der Zeitpunkt der Zustimmungen und die Fassung der angezeigten Texte (Spezifikation, Feld `nachweis`). 5. **Löschfristen** angepasst (siehe Tabelle); 11.1 kannte nur „nicht bestätigt 48 Stunden“, „abgesagt 30 Tage“ und „bestätigt {{90}} Tage“. -## Für die Rechtsprüfung offen - -1. **Server-Protokolle**: Frist (14 Tage) und Rechtsgrundlage (Art. 6 Abs. 1 lit. f DSGVO) müssen zur Datenschutzerklärung von WEBklar passen; bei Änderung dort hier nachziehen. -2. **Drittländer**: „Eine Übermittlung in Drittländer findet nicht statt“ gilt nur, solange Server und E-Mail-Versand bei Hetzner in Deutschland bleiben (WEBklar-Datenschutzerklärung Abschnitte 3 und 8). -3. **Löschlauf**: „48 Stunden“ und „30 Tage“ sind die frühesten Zeitpunkte, an denen der Löschlauf löscht; er läuft in Abständen. -4. **Kontakt der Gäste**: Anfragen nach Auskunft oder Löschung gehen an den Betrieb; WEBklar leitet Anfragen, die bei ihr eingehen, als Auftragsverarbeiter weiter (in der Datenschutzerklärung von WEBklar, 6c). -5. **Gesundheitsbereich**: Der Terminplaner ist dafür nicht bestimmt; der Satz „Bitte keine Angaben zur Gesundheit“ steht auch im Formular. -6. **Knopfmodus**: Das Widget lädt die Konfiguration (Texte und Leistungen) im Knopfmodus schon beim Laden der Seite (R54); „Aufruf“ (Abweichung 3) stimmt also auch für diesen Abruf. Prüfen, ob der Text zwischen Skript und Konfiguration unterscheiden muss; beides geht an denselben Server. - ## Fassung Sie > ### Online-Terminbuchung diff --git a/docs/rechtliches/termine-hinweise.md b/docs/rechtliches/termine-hinweise.md index 59c13aa..841be00 100644 --- a/docs/rechtliches/termine-hinweise.md +++ b/docs/rechtliches/termine-hinweise.md @@ -1,5 +1,3 @@ -ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen - # Hinweise, Haken und Knopftexte für Gäste (Terminplaner) Fassung 2026-10-05. Grundlage: `recherche-recht.md` Abschnitt 11.2 (Hinweis unter dem Formular) und 11.3, `recherche-recht-zahlung.md` Abschnitt 7.3 C bis E (Haken, Widerrufshinweis, Widerrufsfunktion). Die Texte in den Abschnitten 1 bis 3 sind die Fassung aus `server/services/termine/rechtstexte.js` (Platzhalter in doppelten geschweiften Klammern); `test/termineRechtstexte.test.js` prüft, dass sie hier wörtlich stehen. Ändere sie deshalb nur in `rechtstexte.js` und übernimm sie hierher. Die Abschnitte 4 bis 6 sind reine Entwurfstexte für das Widget und die Mails; sie stehen nicht im Code. @@ -76,10 +74,3 @@ Gäste, die nur Unternehmen sind (`recht.gaesteSind = unternehmer`): kein Hinwei > Wir bestätigen den Eingang Ihres Widerrufs. Inhalt Ihrer Erklärung: Widerruf des Vertrags zur Buchung {{nummer}}. Eingegangen am {{eingang}} (Datum und Uhrzeit). {{Nur im Block „zugeordnet“: Ihr Termin ist abgesagt.}} Absender: {{Firma}}. Der Eingang muss mit Datum und Uhrzeit bestätigt werden (§ 356a BGB); das ist auch der Text der Belehrung („Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs“). - -## Für die Rechtsprüfung und das Widget offen - -1. **Belehrung vor dem Vertragsschluss** (erledigt durch R14, R55): Das Widget zeigt die vollständige Belehrung samt Muster-Widerrufsformular (`texte.widerrufsbelehrung`, aufklappbar) in der Zusammenfassung vor dem Knopf, bei jeder Verbraucher-Buchung auch bei Anfragen; der Nachweis der Buchung hält ihren Hash fest. Die Bestätigungs-E-Mail wiederholt sie. Offen bleibt nur, ob der Rechtsprüfer die aufklappbare Darstellung genügen lässt. -2. **Widerruf der Einwilligung zur Erinnerung**: Der feste Wortlaut des Hakens (Abschnitt 2) enthält seit Fassung 2026-10-05 den Satz „Diese Einwilligung kann ich jederzeit widerrufen.“ (Art. 7 Abs. 3 DSGVO); die Einzelheiten stehen im Datenschutz-Absatz (`termine-datenschutz-absatz.md`). Prüfen, ob dieser Satz am Haken genügt, ohne Link zur Datenschutzerklärung und zum Impressum direkt am Haken (⚖ Q17). -3. **Link zur Datenschutzerklärung** neben dem Hinweis: `betrieb.datenschutzUrl` ist in der Einstellung optional. Ohne Link verweist der Hinweis auf eine Datenschutzerklärung, die der Gast nicht findet; das Portal sollte die Adresse vor dem Einschalten verlangen. -4. **Bestätigungsfrist**: Der Hinweis nennt „48 Stunden nach Ablauf der Bestätigungsfrist“. Der Löschlauf läuft in Abständen; „48 Stunden“ ist die früheste Löschung, nicht eine Sekundengarantie. diff --git a/docs/rechtliches/termine-widerrufsbelehrung-muster.md b/docs/rechtliches/termine-widerrufsbelehrung-muster.md index 4d43f5b..47ee6d2 100644 --- a/docs/rechtliches/termine-widerrufsbelehrung-muster.md +++ b/docs/rechtliches/termine-widerrufsbelehrung-muster.md @@ -1,5 +1,3 @@ -ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen - # Widerrufsbelehrung und Muster-Widerrufsformular (Terminplaner, Dienstleistung) Fassung 2026-10-05. Quelle des Wortlauts: Anlage 1 und Anlage 2 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB (Fundstelle BGBl. I 2013, S. 3663 bis 3665), abgerufen am 2026-10-02 von gesetze-im-internet.de; die Textdateien liegen unverändert in `docs/rechtliches/quellen/anlage-1.txt` und `anlage-2.txt`. @@ -62,12 +60,3 @@ Muster-Widerrufsformular - G02 (Bestätigung an den Gast), Block `widerruf`, als `{{widerrufsbelehrung}}` (die Mailvorlage setzt den Text HTML-maskiert ein); nur wenn die Buchung ein Widerrufsrecht hat (verbindlich, Verbraucher). - Bei einer Freizeit-Dienstleistung mit festem Termin (§ 312g Abs. 2 Nr. 9 BGB) und bei Gästen, die nur Unternehmen sind, entfallen Belehrung, Formular und Widerrufsfunktion; der Hinweis steht dann in `termine-hinweise.md`. - -## Für die Rechtsprüfung offen - -1. **Lieferkosten-Klammer** im Absatz „Folgen des Widerrufs“ („einschließlich der Lieferkosten (mit Ausnahme …)“) steht unverändert im Text. Anlage 1 enthält keinen Gestaltungshinweis, der sie bei reinen Dienstleistungen zu streichen erlaubt; sie ist deshalb nicht gekürzt. Prüfen, ob sie für Dienstleistungen entfallen darf. -2. **„… beginnen soll“**: Nach dem Streichen von „oder Lieferung von Wasser/Gas/Strom/Fernwärme“ steht „die Dienstleistungen … beginnen soll“ (Singular im Gesetzestext). Der Wortlaut ist bewusst nicht geglättet, damit das Muster unverändert bleibt. Prüfen, ob „sollen“ zulässig ist. -3. **Anschrift und Telefon**: Das Muster verlangt Anschrift und Telefonnummer. `widerrufsbelehrung()` wirft deshalb einen TypeError, wenn eine der vier Angaben fehlt. Die Einstellungsprüfung (späterer Task) soll Anschrift und Telefon verlangen, sobald Gäste ein Widerrufsrecht haben können (öffentlich `widerruf: true`). -4. **Wertersatz-Absatz** gilt nur, wenn der Gast den vorzeitigen Beginn verlangt hat (Zustimmung `ZUSTIMMUNG_VORZEITIG`, nur bei Terminbeginn innerhalb von 14 Tagen). Der Absatz steht trotzdem immer in der Belehrung, wie das Muster es für Dienstleistungen vorsieht. -5. **Hinweis statt Internetadresse**: „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ist als geeigneter Hinweis gewählt (Entscheidung R12). Prüfen, ob ein Hinweis ohne Internetadresse genügt oder die Adresse der Seite genannt werden muss. -6. **Gültigkeit der Gesetzlichkeitsfiktion**: Sie gilt nur, wenn das Muster inhaltlich und gestalterisch dem Gesetz entspricht. Jede Abweichung (auch Zeilenumbrüche, Überschriften, die „–“-Aufzählung im Formular) ist hier Darstellung, nicht Inhalt. Vor dem Einsatz bei fremden Betrieben durch eine Kanzlei prüfen lassen. diff --git a/server/services/termine/einbauanleitung.js b/server/services/termine/einbauanleitung.js index 80966a3..cb879b8 100644 --- a/server/services/termine/einbauanleitung.js +++ b/server/services/termine/einbauanleitung.js @@ -483,8 +483,10 @@ const abschnittDatenschutz = (anrede) => (d) => { // ---------------------------------------------------------------- Abschnitt Abnahme-Checkliste -// Zwischenregel bis Block F (F10b2 ersetzt sie durch die Endregel aus F4: WEBklar fuegt den Absatz ein, wenn die Website von WEBklar betreut wird) -const DATENSCHUTZ_ZWISCHENREGEL = 'liegt die Datenschutzerklärung nicht in unserem Repo: Absatz per Chat an den Kunden senden' +// Wer den Datenschutz-Absatz einfuegt: die Regel der Leistungsbeschreibung (Nr. 4 Abs. 9); Checkliste und Schritt 4 der Kurzfassung teilen sie +const DATENSCHUTZ_REGEL = + 'liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügen wir den Absatz beim Einbau ein, das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet; ' + + 'sonst den Absatz als Textbaustein per Chat an den Kunden senden, der ihn selbst einfügt' function abschnittCheckliste(d) { const { e } = d @@ -499,7 +501,7 @@ function abschnittCheckliste(d) { 'Einbau-Code eingefügt (Abschnitt „Einbau-Code (Snippet)“). Das Widget bleibt unsichtbar bis zur Freischaltung: Bis dahin steht nur der Ausweichtext auf der Seite.', ...(knopf ? ['Modus knopf: --wkt-akzent immer setzen (Abschnitt „Aussehen (CSS)“). Der Knopf kennt die Betriebsfarbe erst nach dem Klick und nimmt sonst #1d4ed8.'] : []), 'Ausweichtext geprüft: JavaScript aus oder falscher siteKey: Der Text steht, nichts ist kaputt.', - `Datenschutz-Absatz „Online-Terminbuchung“ steht in der Datenschutzerklärung der Website (Fassung ${e.anrede === 'du' ? 'Du' : 'Sie'}; ${DATENSCHUTZ_ZWISCHENREGEL}).`, + `Datenschutz-Absatz „Online-Terminbuchung“ steht in der Datenschutzerklärung der Website (Fassung ${e.anrede === 'du' ? 'Du' : 'Sie'}; ${DATENSCHUTZ_REGEL}).`, `URLs von Impressum (${impressum}) und Datenschutzerklärung (${datenschutz}) stimmen und beginnen mit https://, sonst fehlen die Links im Fuß des Widgets.`, `„Freischalten (Abnahme)“ im Ticketsystem ausgelöst${wo}. Erst danach antwortet die öffentliche Schnittstelle (nur im Zustand active), und das Widget erscheint.`, // am sichtbaren Widget @@ -666,7 +668,7 @@ function kurzSchritte(k) { '1. WOMS-Panel öffnen (Kunde → Reiter Funktionen → Anleitung des Projekts): dort stehen alle Einstellungen, die Kopierknöpfe und die Abnahme-Checkliste.', '2. Einbau-Code einfügen: den Code oben an der Stelle einsetzen, die der Kunde nennt (siehe Kundenwunsch), das Skript einmal pro Seite.', '3. Gestaltung und Kontrast: das CSS aus der Anleitung (Abschnitt „Aussehen (CSS)“) in die Website übernehmen und den Kontrast prüfen.', - `4. Datenschutz-Absatz (${form}) aus der Anleitung in die Datenschutzerklärung der Website aufnehmen, bevor freigeschaltet wird (${DATENSCHUTZ_ZWISCHENREGEL}).`, + `4. Datenschutz-Absatz (${form}) aus der Anleitung in die Datenschutzerklärung der Website aufnehmen, bevor freigeschaltet wird (${DATENSCHUTZ_REGEL}).`, '5. „Freischalten (Abnahme)“ im WOMS auslösen, danach eine Probebuchung durchspielen und den Testtermin wieder stornieren (Checkliste in der Anleitung).', `6. Ticket erledigen und die Zeit im Arbeitsblatt buchen. Nicht auf das Stundenkontingent anrechnen${bezahlt}.`, ].join('\n') diff --git a/server/services/termine/rechtstexte.js b/server/services/termine/rechtstexte.js index 84ca754..720fb47 100644 --- a/server/services/termine/rechtstexte.js +++ b/server/services/termine/rechtstexte.js @@ -1,6 +1,4 @@ /** - * ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen. - * * Rechtstext-Bausteine des Terminplaners fuer Gaeste (Spezifikation 2026-10-02-terminplaner-design.md, Abschnitt 11). * Reine Funktionen und Konstanten, ohne Appwrite; sie nutzen nur Hilfsfunktionen aus einstellungen.js (zeigtWiderruf). Die Texte * sind Rechtstexte und stehen deshalb immer in der Sie-Form (auch bei Du-Betrieben); nur der Datenschutz-Hinweis unter dem @@ -31,8 +29,11 @@ import { zeigtWiderruf } from './einstellungen.js' */ export const RECHTSTEXTE_VERSION = '2026-10-05' -/** true, solange die Rechtstexte als ENTWURF gelten (Kopf KOPF der Dokumente in docs/rechtliches/); Block F setzt false zusammen mit den Koepfen. */ -export const RECHTSTEXTE_ENTWURF = true +/** + * Kennzeichen, ob die Gaestetexte noch als Entwurf (nur Pilot, vor dem Einsatz bei fremden Betrieben zu pruefen) gelten. Ist es true, + * nennt die Einbauanleitung das als erste Warnung. Seit der Endfassung der Gaestetexte (Fassung 2026-10-05) false. + */ +export const RECHTSTEXTE_ENTWURF = false const ZEILENUMBRUCH = '\n' const ABSATZ = '\n\n' diff --git a/test/funktionenTermine.test.js b/test/funktionenTermine.test.js index 7fc0c52..852dd32 100644 --- a/test/funktionenTermine.test.js +++ b/test/funktionenTermine.test.js @@ -1217,12 +1217,15 @@ test('Anleitung ohne gespeicherte Einstellung: nur der Einbau-Code und die Warnu }) test('Anleitung, Drift: Ticket auf Version 1, Einstellung jetzt Version 3 => Warnung am Ende von warnungen, ticketVersion und ticketWoid', async () => { - const k = legeAnleitung({ version: 3, ticketDetails: TICKET_MIT_STAND(1) }) + // eine andere Warnung muss davor stehen (ohne Entwurfs-Banner sonst keine): fehlende Impressum-URL + const einstellung = d8Einstellung() + einstellung.betrieb.impressumUrl = '' + const k = legeAnleitung({ einstellung, version: 3, ticketDetails: TICKET_MIT_STAND(1) }) const res = await anleitung(k) assert.equal(res.status, 200, JSON.stringify(res.body)) assert.equal(warnungDrift(1, 3), 'Einstellungen seit der Bestellung geändert (Ticket Version 1, jetzt 3).') assert.equal(res.body.warnungen.at(-1), warnungDrift(1, 3)) - assert.ok(res.body.warnungen.length > 1, 'sie steht hinter den uebrigen Warnungen') + assert.ok(res.body.warnungen.length > 1 && /Impressum/.test(res.body.warnungen[0]), `sie steht hinter den uebrigen Warnungen: ${res.body.warnungen.join(' | ')}`) assert.equal(res.body.warnungen.filter((w) => w.startsWith('Einstellungen seit der Bestellung')).length, 1) assert.equal(res.body.ticketVersion, 1) assert.equal(res.body.ticketWoid, ticketDoc(k.ticketId).woid) diff --git a/test/termineEinbauanleitung.test.js b/test/termineEinbauanleitung.test.js index 6d60cb5..8bc4419 100644 --- a/test/termineEinbauanleitung.test.js +++ b/test/termineEinbauanleitung.test.js @@ -219,6 +219,11 @@ test('cssVorschlag: der Hinweis nennt den Kontrast gegen Weiss, wenn der Akzent const NOW = new Date('2026-10-05T10:00:00Z') // Montag, 12:00 Berliner Zeit const SPAETER = new Date('2026-10-06T10:00:00Z') // einen Tag spaeter: nur die "Erzeugt am"-Zeile aendert sich const GEHEIM = 'XYZ-GEHEIM' +// Endregel (Leistungsbeschreibung Nr. 4 Abs. 9): wer den Datenschutz-Absatz einfügt; Checkliste und Schritt 4 der Kurzfassung nennen sie +const DATENSCHUTZ_ENDREGEL = + 'liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügen wir den Absatz beim Einbau ein, das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet; ' + + 'sonst den Absatz als Textbaustein per Chat an den Kunden senden, der ihn selbst einfügt' + const BETRIEB = { name: 'Salon Muster', email: 'inhaber-intern@salon-muster.example', @@ -880,6 +885,36 @@ test('D3b Banner: ENTWURF_BANNER genau dann, wenn entwurf, und zwar als erste Wa assert.equal(erzeuge({ entwurf: true }).warnungen.filter((w) => w === ENTWURF_BANNER).length, 1) }) +test('einbau_ohne_banner: im Standardaufruf steht kein ENTWURF-Banner in Warnungen, Volltext und Kurzfassung; mit entwurf: true bleibt er', () => { + assert.equal(RECHTSTEXTE_ENTWURF, false) + const r = erzeuge() + assert.deepEqual(r.warnungen, []) + assert.ok(!r.warnungen.includes(ENTWURF_BANNER)) + assert.ok(!r.text.includes('ENTWURF'), 'Volltext') + assert.ok(!r.kurz.includes('ENTWURF'), 'Kurzfassung (Ticket)') + for (const a of r.abschnitte) assert.ok(!a.text.includes('ENTWURF'), `Abschnitt ${a.id}`) + // die D3b-Tests mit ausdrücklichem entwurf: true bleiben gültig: der Banner ist weiter eine eigene Warnung + const mit = erzeuge({ entwurf: true }) + assert.deepEqual(mit.warnungen, [ENTWURF_BANNER]) + assert.ok(mit.kurz.includes(ENTWURF_BANNER)) +}) + +test('checkliste_datenschutz_endregel: die Checkliste nennt die Endregel der Leistungsbeschreibung, die Zwischenregel ist weg', () => { + const text = abschnitt(erzeuge(), 'checkliste').text + const punkt = zeilen(text).find((z) => z.includes('Datenschutz-Absatz „Online-Terminbuchung“ steht in der Datenschutzerklärung')) + assert.ok(punkt, text) + assert.ok(punkt.includes(`(Fassung Sie; ${DATENSCHUTZ_ENDREGEL}).`), punkt) + for (const teil of ['von WEBklar betreuten Website', 'fügen wir den Absatz beim Einbau ein', 'nicht auf das Stundenkontingent angerechnet', 'Textbaustein per Chat an den Kunden']) { + assert.ok(punkt.includes(teil), `Endregel: ${teil}`) + } + assert.ok(!text.includes('liegt die Datenschutzerklärung nicht in unserem Repo'), 'Zwischenregel aus D3b ist weg') + assert.ok(!erzeuge().kurz.includes('nicht in unserem Repo'), 'auch nicht in der Kurzfassung') + assert.ok(!erzeuge().text.includes('nicht in unserem Repo'), 'und nicht im Volltext') + const du = fixtureEinstellung() + du.anrede = 'du' + assert.ok(abschnitt(erzeuge({ einstellung: du }), 'checkliste').text.includes(`(Fassung Du; ${DATENSCHUTZ_ENDREGEL}).`)) +}) + test('D3b Warnungen: fehlende Telefonnummer, und Anschrift nur, wenn Gaeste ein Widerrufsrecht haben', () => { const ohneTelefon = fixtureEinstellung() ohneTelefon.betrieb.telefon = '' @@ -996,7 +1031,7 @@ test('D3b Checkliste: immer-Punkte (Freischalten, Probebuchung, stornieren, Kont 'Datenschutz-Absatz', 'Impressum', 'Kontrast', '390 px', 'Tastatur', ]) assert.ok(text.includes(erwartet), `${erwartet}\n---\n${text}`) assert.ok(text.includes(BETRIEB.impressumUrl) && text.includes(BETRIEB.datenschutzUrl), 'die URLs stehen zum Pruefen im Punkt') - assert.ok(text.includes('liegt die Datenschutzerklärung nicht in unserem Repo: Absatz per Chat an den Kunden senden'), 'Zwischenregel bis Block F') + assert.ok(text.includes(DATENSCHUTZ_ENDREGEL), 'Endregel der Leistungsbeschreibung (Nr. 4 Abs. 9)') assert.ok(text.includes(`${config.womsBaseUrl}/customers/c1?tab=funktionen`), 'Link ins WOMS-Kundenpanel') // die Reihenfolge: erst Einbau, dann Freischalten, dann Probebuchung, dann die Pruefungen am sichtbaren Widget const stelle = (teil) => z.findIndex((x) => x.includes(teil)) @@ -1246,7 +1281,7 @@ test('D4 kurz Schritt 4 und 6: Datenschutz-Fassung nach Anrede, Preis nur bei be du.anrede = 'du' assert.match(schritt({}, 4), /Sie-Form/) assert.match(schritt({ einstellung: du }, 4), /Du-Form/) - assert.ok(schritt({}, 4).includes('Absatz per Chat an den Kunden senden'), 'Zwischenregel aus der Checkliste (F10b2 ersetzt sie an einer Stelle)') + assert.ok(schritt({}, 4).includes(DATENSCHUTZ_ENDREGEL), 'dieselbe Endregel wie in der Checkliste') assert.match(schritt({ feature: PAKET_FEATURE }, 6), /Nicht auf das Stundenkontingent anrechnen \(Einrichtung bezahlt, 149 €\)/) assert.ok(schritt({}, 6).includes('Nicht auf das Stundenkontingent anrechnen')) assert.ok(!schritt({}, 6).includes('bezahlt'), 'ohne Kauf keine Behauptung, die Einrichtung sei bezahlt') diff --git a/test/termineEmbedStatic.test.js b/test/termineEmbedStatic.test.js index c4baff9..ee0e9a0 100644 --- a/test/termineEmbedStatic.test.js +++ b/test/termineEmbedStatic.test.js @@ -235,7 +235,7 @@ test('Einbauanleitung: Snippet, Variablen, drei Stile, Datenschutz-Absatz, Abnah 'data-modus', 'data-api', 'data-knopf', 'data-leistung', '--wkt-akzent', '--wkt-akzent-text', '--wkt-hintergrund', '--wkt-text', '--wkt-rand', '--wkt-radius', '--wkt-schrift', '--wkt-schrift-titel', '@layer wkt', 'wkt-style', - 'docs/rechtliches/termine-datenschutz-absatz.md', 'ENTWURF', + 'docs/rechtliches/termine-datenschutz-absatz.md', 'Fassung', 'Abnahme', '/api/public/termine/v1', '/config', '/tage', '/zeiten', '/buchungen', '/widerruf', '/kalender/', 'termine-demo.html', 'termine-widget.ui.mjs', ]) { diff --git a/test/termineRechtstexte.test.js b/test/termineRechtstexte.test.js index 81b18fa..242f001 100644 --- a/test/termineRechtstexte.test.js +++ b/test/termineRechtstexte.test.js @@ -1,6 +1,6 @@ import { test } from 'node:test' import assert from 'node:assert/strict' -import { readFileSync } from 'node:fs' +import { existsSync, readFileSync } from 'node:fs' import { createHash } from 'node:crypto' import { config } from '../server/config.js' import { @@ -17,7 +17,6 @@ import { 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. @@ -246,8 +245,8 @@ test('Formular-Hinweis: Sie ist die Vorgabe, Aufbewahrung 90 Tage ohne gültigen // ---------------------------------------------------------------- Datenschutz-Absatz „Online-Terminbuchung“ (Quelle der Einbauanleitung und der Doku) -test('Rechtstexte sind Entwurf, bis Block F die ENTWURF-Köpfe und diese Konstante zusammen entfernt', () => { - assert.equal(RECHTSTEXTE_ENTWURF, true) +test('D1 (f): die Rechtstexte gelten nicht mehr als Entwurf (RECHTSTEXTE_ENTWURF === false)', () => { + assert.equal(RECHTSTEXTE_ENTWURF, false) }) const DS_SCHALTER = ['telefonFeld', 'freitext', 'erinnerung', 'widerruf', 'telefon'] @@ -508,13 +507,29 @@ 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', + 'docs/rechtliches/termine-datenschutz-6c.md', ] -test('Jedes Rechtstext-Dokument beginnt mit dem Entwurfs-Kopf', () => { +// Das Wort ENTWURF als Kennzeichnung; ENTWURF_BANNER und RECHTSTEXTE_ENTWURF (Bezeichner im Code) zählen nicht +const ENTWURF_WORT = /ENTWURF(?![_A-Z])/ + +test('kein_entwurf_mehr: kein ENTWURF-Kopf in den vier Dokumenten, keiner in den Texten von rechtstexte.js und in der Einbau-Doku; die Konstante steht auf false', () => { for (const rel of DOKUMENTE) { - assert.equal(lies(rel).split('\n')[0].trim(), KOPF, rel) + const md = lies(rel) + assert.ok(!ENTWURF_WORT.test(md), `${rel}: ENTWURF steht noch im Dokument`) + assert.notEqual(md.split('\n')[0].trim(), KOPF, `${rel}: der Entwurfs-Kopf steht noch in der ersten Zeile`) + assert.ok(md.startsWith('# '), `${rel}: das Dokument beginnt mit der Überschrift`) } + assert.ok(!existsSync(new URL('../docs/rechtliches/termine-datenschutz-6c-entwurf.md', import.meta.url)), 'die 6c-Datei heißt nicht mehr -entwurf') + const quelle = lies('server/services/termine/rechtstexte.js') + assert.ok(!ENTWURF_WORT.test(quelle.replace(/RECHTSTEXTE_ENTWURF/g, '')), 'rechtstexte.js: ENTWURF in Kopf oder Kommentar (nur der Bezeichner RECHTSTEXTE_ENTWURF ist erlaubt)') + assert.ok(/export const RECHTSTEXTE_ENTWURF = false\b/.test(quelle)) + assert.equal(RECHTSTEXTE_ENTWURF, false) + const einbau = lies('docs/funktionen/termine-einbau.md') + assert.ok(!ENTWURF_WORT.test(einbau), 'termine-einbau.md: ENTWURF') + assert.ok(!/nur beim Pilotkunden einsetzen|Solange der Kopf dort steht/.test(einbau), 'termine-einbau.md Abschnitt 6 ohne die Pilot-Einschränkung') + // die Gäste-Dokumente haben keine offene Rechtsprüfungs-Liste mehr (die Fragen stehen im Kanzlei-Paket) + for (const rel of DOKUMENTE.slice(0, 3)) assert.ok(!/^## Für die Rechtsprüfung/m.test(lies(rel)), `${rel}: Liste „Für die Rechtsprüfung offen“ steht noch`) }) test('Hinweise-Dokument: Formular-Hinweis (Sie/Du) und alle Zustimmungstexte stehen wörtlich darin', () => { @@ -553,7 +568,7 @@ test('Datenschutz-Absatz: Löschfristen der Vorgaben, Sie- und Du-Fassung, Auftr }) 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') + const md = lies('docs/rechtliches/termine-datenschutz-6c.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))