From 90098726b9ba340a8c7c4c5b993cdfd4cf5675f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 03:42:27 +0000 Subject: [PATCH] =?UTF-8?q?Rechtstexte=20und=20K15:=20Pr=C3=BCfbefunde=20F?= =?UTF-8?q?5=E2=80=93F8=20(AVV=20genauer,=20PDF-Pr=C3=BCfsumme,=20Unterlag?= =?UTF-8?q?en=20der=20vereinbarten=20Fassung)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- docs/rechtliches/avv-terminplaner.md | 18 +-- .../avv-terminplaner-2026-10-05.html | 18 +-- .../avv-terminplaner-2026-10-05.pdf | Bin 104581 -> 106874 bytes .../avv-terminplaner-2026-10-05.pdf.sha256 | 1 + ...reibung-terminplaner-2026-10-05.pdf.sha256 | 1 + ...belehrung-funktionen-2026-10-05.pdf.sha256 | 1 + scripts/build-legal-pdfs.sh | 16 +- server/services/legalDocs.js | 22 ++- server/services/siteFeatures.js | 74 +++++---- test/buildLegalHtml.test.js | 24 +++ test/funktionenRecht.test.js | 152 +++++++++++++++++- test/legalDocs.test.js | 21 ++- test/termineVertragsunterlagen.test.js | 60 ++++++- 13 files changed, 345 insertions(+), 63 deletions(-) create mode 100644 public/rechtliches/avv-terminplaner-2026-10-05.pdf.sha256 create mode 100644 public/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.pdf.sha256 create mode 100644 public/rechtliches/widerrufsbelehrung-funktionen-2026-10-05.pdf.sha256 diff --git a/docs/rechtliches/avv-terminplaner.md b/docs/rechtliches/avv-terminplaner.md index de730ae..fa7dbb0 100644 --- a/docs/rechtliches/avv-terminplaner.md +++ b/docs/rechtliches/avv-terminplaner.md @@ -28,7 +28,7 @@ zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am S ## 3. Datenarten und Betroffene -(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); der Nachweis der Zustimmungen als Zeitpunkt und als Hashes (Prüfsummen) der gezeigten Texte; der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast. +(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast. (2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz. @@ -44,7 +44,7 @@ zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am S (1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet. -(2) Mit der Bestellung weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). +(2) Mit der Bestellung weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um. (3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist. @@ -62,7 +62,7 @@ WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder e (1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt. -(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort. +(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6). (3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO). @@ -78,7 +78,7 @@ WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder e (4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht. -(5) Die Buchung speichert als Nachweis der Zustimmungen nur den Zeitpunkt und die Hashes der Texte, die der Gast gesehen hat, nicht die Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. +(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. (6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2). @@ -86,7 +86,7 @@ WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder e (1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten. -(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. +(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt. (3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage. @@ -94,7 +94,7 @@ WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder e (5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf. -(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Bis dahin kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Auf Weisung des Kunden löscht WEBklar die Daten früher. +(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Bis dahin kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich. (7) Entwürfe aus dem Fragebogen, aus denen keine Bestellung geworden ist, löscht WEBklar 90 Tage nach der letzten Änderung. @@ -136,9 +136,9 @@ WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis de | Speicher im Browser | Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. | | Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. | | Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. | -| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus dem Zeitpunkt und Hashes der gezeigten Texte, nicht aus den Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. | -| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); Abfragen erfolgen nur je Funktion. Die Einstellungen liegen je Funktion in einem eigenen Dokument. | -| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten. Vor jeder Löschung wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. | +| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. | +| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); Das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. | +| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten. Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. | | Protokolle | Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). | | Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). | | Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. | diff --git a/public/rechtliches/avv-terminplaner-2026-10-05.html b/public/rechtliches/avv-terminplaner-2026-10-05.html index ec41242..57d8735 100644 --- a/public/rechtliches/avv-terminplaner-2026-10-05.html +++ b/public/rechtliches/avv-terminplaner-2026-10-05.html @@ -74,7 +74,7 @@

3. Datenarten und Betroffene

-

(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); der Nachweis der Zustimmungen als Zeitpunkt und als Hashes (Prüfsummen) der gezeigten Texte; der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.

+

(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.

(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.

@@ -90,7 +90,7 @@

(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.

-

(2) Mit der Bestellung weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung).

+

(2) Mit der Bestellung weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.

(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.

@@ -108,7 +108,7 @@

(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.

-

(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.

+

(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).

(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).

@@ -124,7 +124,7 @@

(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.

-

(5) Die Buchung speichert als Nachweis der Zustimmungen nur den Zeitpunkt und die Hashes der Texte, die der Gast gesehen hat, nicht die Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.

+

(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.

(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).

@@ -132,7 +132,7 @@

(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.

-

(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung.

+

(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.

(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.

@@ -140,7 +140,7 @@

(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.

-

(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Bis dahin kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Auf Weisung des Kunden löscht WEBklar die Daten früher.

+

(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Bis dahin kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.

(7) Entwürfe aus dem Fragebogen, aus denen keine Bestellung geworden ist, löscht WEBklar 90 Tage nach der letzten Änderung.

@@ -183,9 +183,9 @@ Speicher im BrowserEs werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. ZugriffskontrolleDas Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. MissbrauchsschutzEin verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. - DatenminimierungPflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus dem Zeitpunkt und Hashes der gezeigten Texte, nicht aus den Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. - TrennungAlle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); Abfragen erfolgen nur je Funktion. Die Einstellungen liegen je Funktion in einem eigenen Dokument. - LöschungTäglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten. Vor jeder Löschung wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. + DatenminimierungPflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. + TrennungAlle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); Das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. + LöschungTäglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten. Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. ProtokolleDie Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). DatensicherungTägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). RechenzentrumHetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. diff --git a/public/rechtliches/avv-terminplaner-2026-10-05.pdf b/public/rechtliches/avv-terminplaner-2026-10-05.pdf index 1875c631cbcbc8bfa761841748e94c0fbd4af137..5b98a55a557246faf49150b6791bc7ba36882a6b 100644 GIT binary patch delta 38584 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 is2~EQ_+vsi3=V-Mab{sk6T{(P5P.sha256 mit der +# SHA-256 der HTML-Seite, aus der es gedruckt wurde (test/buildLegalHtml.test.js prueft, dass beide zusammenpassen). Ein PDF, +# dessen Pruefsumme noch stimmt, wird nicht neu gedruckt (FORCE=1 druckt alle neu). # datenschutz = nur die Datenschutzerklaerung (docs/WEBklar-Datenschutzerklaerung.pdf) # Vorher `free -m` pruefen und mit `nice` aufrufen; jeder Chrome-Aufruf laeuft hoechstens 120 Sekunden (timeout), die PDFs nacheinander. set -euo pipefail @@ -25,7 +27,7 @@ ANALYSE_VERSION="${ANALYSE_VERSION:-2026-10-02}" termine_version() { local v="${TERMINE_VERSION:-}" if [ -z "$v" ]; then - v="$(node --max-old-space-size=256 --input-type=module -e "const { config } = await import('./server/config.js'); console.log(config.termine.avvVersion)")" + v="$(timeout 60 node --max-old-space-size=256 --input-type=module -e "const { config } = await import('./server/config.js'); console.log(config.termine.avvVersion)")" fi [[ "$v" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "Ungueltige Fassung: $v" >&2; exit 1; } echo "$v" @@ -40,7 +42,15 @@ termine) TERMINE_VERSION="$(termine_version)" for slug in leistungsbeschreibung-terminplaner avv-terminplaner widerrufsbelehrung-funktionen; do [ -f "public/rechtliches/$slug-$TERMINE_VERSION.html" ] || { echo "HTML fehlt: public/rechtliches/$slug-$TERMINE_VERSION.html (zuerst node scripts/build-legal-html.mjs termine)"; exit 1; } - pdf "public/rechtliches/$slug-$TERMINE_VERSION.html" "public/rechtliches/$slug-$TERMINE_VERSION.pdf" + quelle="public/rechtliches/$slug-$TERMINE_VERSION.html" + ziel="public/rechtliches/$slug-$TERMINE_VERSION.pdf" + 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 ;; datenschutz) diff --git a/server/services/legalDocs.js b/server/services/legalDocs.js index c60a8cb..460ccd5 100644 --- a/server/services/legalDocs.js +++ b/server/services/legalDocs.js @@ -89,11 +89,21 @@ function funktionSatz(featureKey) { /** * Anhaenge der Bestellmail einer Funktion in der aktuellen Fassung: termine => Satz 'termine', news => []. Wirft bei - * unbekannter Funktion und bei fehlender Datei (der Aufrufer entscheidet, was dann geschieht). + * unbekannter Funktion und bei fehlender Datei (der Aufrufer entscheidet, was dann geschieht). opts.fassung waehlt eine andere + * (ausgelieferte) Fassung, etwa die, der ein Kunde bei der Bestellung zugestimmt hat; sonst gilt die aktuelle. */ -export async function featureLegalAttachments(featureKey, opts) { +export async function featureLegalAttachments(featureKey, { fassung, ...opts } = {}) { const satz = funktionSatz(featureKey) - return satz ? legalPdfAttachments(satz, aktuelleFassung(satz), opts) : [] + return satz ? legalPdfAttachments(satz, fassung ?? aktuelleFassung(satz), opts) : [] +} + +/** + * Adressen der HTML-Seiten der Funktion ({ lb, avv, widerruf }, versioniert) in der aktuellen oder der gewaehlten Fassung + * (opts.fassung); news => null. Wirft bei unbekannter Funktion oder ungueltiger Fassung. + */ +export function featureLegalPaths(featureKey, { fassung } = {}) { + const satz = funktionSatz(featureKey) + return satz ? legalPaths(satz, fassung ?? aktuelleFassung(satz)) : null } /** @@ -116,13 +126,13 @@ async function istLesbaresPdf(pfad) { /** * true, wenn alle Rechtsdokumente der Funktion in der aktuellen Fassung vorliegen: HTML-Seiten (die die Bestellseite * verlinkt) und PDFs (die Mail anhaengt; je PDF stat und die ersten fuenf Bytes, nicht der ganze Inhalt). Unbekannte - * Funktion und jeder Lesefehler: false (fail-closed), nie ein Wurf. + * Funktion und jeder Lesefehler: false (fail-closed), nie ein Wurf. opts.fassung prueft eine andere Fassung als die aktuelle. */ -export async function featureLegalReady(featureKey) { +export async function featureLegalReady(featureKey, { fassung } = {}) { try { const satz = funktionSatz(featureKey) if (!satz) return true - const v = aktuelleFassung(satz) + const v = fassung ?? aktuelleFassung(satz) await Promise.all(Object.values(legalPaths(satz, v)).map((adresse) => access(join(basisDir, adresse)))) const lesbar = await Promise.all(Object.values(legalPdfFiles(satz, v)).map((f) => istLesbaresPdf(f.path))) return lesbar.every(Boolean) diff --git a/server/services/siteFeatures.js b/server/services/siteFeatures.js index 89874a9..8e99257 100644 --- a/server/services/siteFeatures.js +++ b/server/services/siteFeatures.js @@ -1178,41 +1178,58 @@ async function meldeFallZurZahlung(doc, fall, session) { } /** K15 einer Funktion ohne Rechtsdokumente (News): keine Anhaenge, keine Bloecke anlagen und zustimmungen. */ -const OHNE_RECHTSUNTERLAGEN = Object.freeze({ attachments: Object.freeze([]), anlagen: false, anhang: false, zustimmungen: '', fehlt: false }) +const OHNE_RECHTSUNTERLAGEN = Object.freeze({ attachments: Object.freeze([]), unterlagen: null, anhang: false, zustimmungen: '', fehlt: false }) /** - * Rechtsunterlagen der K15 einer Funktion mit Rechtsdokumenten: die PDFs der aktuellen Fassung (legalDocs.featureLegalAttachments, - * erst beim Aufruf geladen; kein Registerhaken) und der Wortlaut der Zustimmungen aus dem Nachweis zur bezahlten Sitzung. Gehoert der - * Nachweis zu einer anderen Fassung als der aktuellen (die Fassung wurde zwischen Klick und Zahlung angehoben), gehen weder PDFs noch - * Links raus: es waeren nicht die Unterlagen, denen der Kunde zugestimmt hat; das Team sendet sie von Hand (fehlt). Nicht lesbare PDFs: - * K15 ohne Anhang, fehlt true, Fehler im Log. Wirft nie. - * @returns {Promise<{ attachments: object[], anlagen: boolean, anhang: boolean, zustimmungen: string, fehlt: boolean }>} + * Rechtsunterlagen der K15 einer Funktion mit Rechtsdokumenten: die PDFs und Adressen der Fassung, der der Kunde bei der Bestellung + * zugestimmt hat (Fassung des Nachweises zur bezahlten Sitzung), und der Wortlaut seiner Zustimmungen. Das ist meist die aktuelle + * Fassung (legalDocs.featureLegalAttachments, erst beim Aufruf geladen; kein Registerhaken). Wurde die Fassung zwischen Klick und + * Zahlung angehoben, gehen die Unterlagen der vereinbarten Fassung raus, wenn es sie gibt; fehlen sie, geht K15 ohne PDFs und Links + * raus (bei der aktuellen Fassung bleiben die Links, nur die PDFs fehlen). Ist der Nachweis nicht lesbar oder seine Fassung nicht bestimmbar, ist die vereinbarte Fassung unbekannt: nie die aktuelle + * ungeprueft anhaengen, K15 geht ohne PDFs und Links raus. Ohne Nachweis (Altbestand) gilt die aktuelle Fassung. In allen Faellen ohne + * Unterlagen ist fehlt true (T16 sagt "von Hand nachsenden") und der Grund steht im Log. Wirft nie. + * @returns {Promise<{ attachments: object[], unterlagen: { fassung: string, lb: string, avv: string, widerruf: string } | null, anhang: boolean, zustimmungen: string, fehlt: boolean }>} */ async function k15Rechtsunterlagen(doc, feature, session) { + const ohne = (zustimmungen) => ({ attachments: [], unterlagen: null, anhang: false, zustimmungen, fehlt: true }) let nachweis = null try { nachweis = await zustimmungFuerSitzung(session.id) } catch (err) { - console.error(`[funktionen] Zustimmungsnachweis zu Sitzung ${session.id} fuer K15 nicht lesbar, der Block zustimmungen entfaellt:`, err.message) + console.error(`[funktionen] K15 (Funktion ${doc.$id}): Zustimmungsnachweis zu Sitzung ${session.id} nicht lesbar, die vereinbarte Fassung ist unbekannt - ohne Unterlagen, das Team sendet sie von Hand:`, err.message) + return ohne('') } const zustimmungen = zustimmungenText(nachweis) - let fassungNachweis = '' - try { - fassungNachweis = nachweis ? JSON.parse(nachweis.fassungen)?.zustimmung ?? '' : '' - } catch { - fassungNachweis = '' - } - if (fassungNachweis && fassungNachweis !== feature.consent.version) { - console.error(`[funktionen] K15 (Funktion ${doc.$id}): Nachweis gehoert zur Fassung ${fassungNachweis}, aktuell ist ${feature.consent.version} - ohne Unterlagen, das Team sendet sie von Hand`) - return { attachments: [], anlagen: false, anhang: false, zustimmungen, fehlt: true } + const aktuell = feature.consent.version + let vereinbart = aktuell + if (nachweis) { + try { + vereinbart = JSON.parse(nachweis.fassungen)?.zustimmung + } catch { + vereinbart = undefined + } + if (typeof vereinbart !== 'string' || vereinbart === '') { + console.error(`[funktionen] K15 (Funktion ${doc.$id}): Fassung im Zustimmungsnachweis nicht bestimmbar - ohne Unterlagen, das Team sendet sie von Hand`) + return ohne(zustimmungen) + } } try { - const { featureLegalAttachments } = await import('./legalDocs.js') - const attachments = await featureLegalAttachments(doc.featureKey) - return { attachments, anlagen: true, anhang: attachments.length > 0, zustimmungen, fehlt: attachments.length === 0 } + const legal = await import('./legalDocs.js') + const eigene = vereinbart === aktuell ? {} : { fassung: vereinbart } + if (vereinbart !== aktuell) { + console.error(`[funktionen] K15 (Funktion ${doc.$id}): Der Kunde hat der Fassung ${vereinbart} zugestimmt, aktuell ist ${aktuell} - die Unterlagen der vereinbarten Fassung gehen raus, wenn es sie gibt`) + if (!(await legal.featureLegalReady(doc.featureKey, eigene))) throw new Error(`Unterlagen der Fassung ${vereinbart} fehlen`) + } + const attachments = await legal.featureLegalAttachments(doc.featureKey, eigene) + if (attachments.length === 0) throw new Error('keine Anhaenge fuer diese Funktion') + const adressen = legal.featureLegalPaths(doc.featureKey, eigene) + const absolut = (pfad) => `${config.publicBaseUrl}${pfad}` + const unterlagen = { fassung: vereinbart, lb: absolut(adressen.lb), avv: absolut(adressen.avv), widerruf: absolut(adressen.widerruf) } + return { attachments, unterlagen, anhang: true, zustimmungen, fehlt: false } } catch (err) { - console.error(`[funktionen] K15-Anhaenge fuer Funktion ${doc.$id} nicht lesbar, K15 geht ohne Anhang raus:`, err.message) - return { attachments: [], anlagen: true, anhang: false, zustimmungen, fehlt: true } + console.error(`[funktionen] K15 (Funktion ${doc.$id}): Unterlagen nicht lesbar, K15 geht ohne PDFs raus:`, err.message) + // die Seiten der aktuellen (= vereinbarten) Fassung bleiben verlinkt; Links auf eine andere Fassung gibt es nur, wenn deren Dateien da sind + return { ...ohne(zustimmungen), unterlagen: vereinbart === aktuell ? unterlagenAdressen(feature) : null } } } @@ -1237,29 +1254,30 @@ async function onFeaturePaid(session) { // K15 mit den Vertragsunterlagen (Terminplaner: drei PDFs als dauerhafter Datentraeger, § 312f BGB). Die Bestaetigung geht vor dem // Anhang: sind die PDFs nicht lesbar oder kommt die Mail nicht an, bleibt die Zahlung nicht ohne Spur, T16 sagt dem Team // "Unterlagen von Hand nachsenden" (Block anhang_fehlt). - const unterlagen = feature ? unterlagenAdressen(feature) : null + const mitRechtsdokumenten = feature ? unterlagenAdressen(feature) !== null : false let anhangFehlt = false if (customer?.email) { try { if (!feature) throw new Error(`Funktion ${doc.featureKey} nicht im Register`) - const rechtlich = unterlagen ? await k15Rechtsunterlagen(doc, feature, session) : OHNE_RECHTSUNTERLAGEN + const rechtlich = mitRechtsdokumenten ? await k15Rechtsunterlagen(doc, feature, session) : OHNE_RECHTSUNTERLAGEN anhangFehlt = rechtlich.fehlt await sendCustomerMail(customer.email, { ...bestellMail(feature, { kundeName: customer.name || '', projekt: await projectName(doc.projectId), invoiceUrl, - unterlagen: rechtlich.anlagen ? unterlagen : null, + unterlagen: rechtlich.unterlagen, anhang: rechtlich.anhang, zustimmungenText: rechtlich.zustimmungen, }), - attachments: rechtlich.attachments, + attachments: [...rechtlich.attachments], }) } catch (err) { console.error('[funktionen] K15 fehlgeschlagen:', err.message) - if (unterlagen) anhangFehlt = true + if (mitRechtsdokumenten) anhangFehlt = true } - } else if (unterlagen) { + } else if (mitRechtsdokumenten) { + console.error(`[funktionen] K15 (Funktion ${doc.$id}) nicht gesendet: beim Kunden ist keine E-Mail-Adresse hinterlegt - die Vertragsunterlagen muessen von Hand nachgesendet werden`) anhangFehlt = true } // Funktionen, die ein laufendes Wartungs-Abo voraussetzen: endete es vor der Zahlung, sagt T16 es dem Team (Block kein_abo). diff --git a/test/buildLegalHtml.test.js b/test/buildLegalHtml.test.js index daeb5fd..54b4d24 100644 --- a/test/buildLegalHtml.test.js +++ b/test/buildLegalHtml.test.js @@ -1,5 +1,6 @@ import { test, after } 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' @@ -256,6 +257,23 @@ test('pdf_dateien: je Dokument ein PDF unter dem Namen aus legalPdfFiles, größ } }) +test('pdf_pruefsumme: neben jedem PDF liegt .sha256 mit der SHA-256 der HTML-Seite, aus der es gedruckt wurde (HTML geändert ohne neues PDF fällt auf)', () => { + const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex') + for (const d of DOKUMENTE) { + const pdf = d.html.replace(/\.html$/, '.pdf') + 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 termine)`) + } + // die Prüfung schlägt an: eine geänderte Seite ergibt eine andere Summe + const d = DOKUMENTE[1] + assert.notEqual(sha256(`${lies(d.html)} `), lies(`${d.html.replace(/\.html$/, '.pdf')}.sha256`).trim()) + // das Skript schreibt die Summe und druckt ein PDF mit gültiger Summe nicht neu + const sh = lies('scripts/build-legal-pdfs.sh') + assert.ok(sh.includes('sha256sum "$quelle"') && sh.includes('echo "$summe" > "$ziel.sha256"')) + assert.ok(sh.includes('(unveraendert)') && sh.includes('FORCE')) +}) + test('anhaenge_drei: legalPdfAttachments("termine", F) liefert lb, avv, widerruf als application/pdf', async () => { const anhaenge = await legalPdfAttachments('termine', V) assert.deepEqual(anhaenge.map((a) => a.filename), [ @@ -294,6 +312,11 @@ test('router_alias: /rechtliches/.html liefert die Bytes der Fassungsdatei assert.match(pdf.headers.get('content-type'), /application\/pdf/) const alt = await fetch(`${app.url}/rechtliches/${slug}-1999-01-01.html`) assert.equal(alt.status, 404, `${slug}-1999-01-01.html darf es nicht geben`) + // die Prüfsummen-Datei neben dem PDF wird einfach mit ausgeliefert, die Adressen der Seiten bleiben davon unberührt + const summe = await fetch(`${app.url}/rechtliches/${slug}-${V}.pdf.sha256`) + assert.equal(summe.status, 200) + assert.match(await summe.text(), /^[0-9a-f]{64}\n$/) + assert.equal((await fetch(`${app.url}/rechtliches/${slug}.pdf.sha256`)).status, 404) } // Fassung der Konstante ohne Datei: der Alias antwortet 404 const vorher = config.termine.avvVersion @@ -329,6 +352,7 @@ test('script_modus: build-legal-pdfs.sh kennt termine und datenschutz, lässt di } // Chrome höchstens 120 Sekunden je Aufruf; Aufbau und Aufrufe nacheinander assert.match(sh, /^ {2}timeout 120 "\$CHROME" /m) + assert.match(sh, /v="\$\(timeout 60 node --max-old-space-size=256 /, 'auch der Node-Aufruf für die Fassung hat Heap-Grenze und Timeout') assert.ok(!/ &$|^\s*wait\b|xargs -P/m.test(sh), 'keine parallelen Läufe') // bash versteht das Skript, und ein unbekannter Modus liefert die Usage mit allen Modi const syntax = spawnSync('bash', ['-n', 'scripts/build-legal-pdfs.sh'], { cwd: ROOT, encoding: 'utf8' }) diff --git a/test/funktionenRecht.test.js b/test/funktionenRecht.test.js index f4b4bd3..afdbee7 100644 --- a/test/funktionenRecht.test.js +++ b/test/funktionenRecht.test.js @@ -1,4 +1,4 @@ -import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs' +import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, copyFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL } from 'node:url' @@ -376,11 +376,153 @@ test('k15_anhang_nicht_lesbar: nicht lesbare PDFs => K15 ohne Anhang, T16 mit Bl } finally { log2.mock.restore() } - // Kunde ohne E-Mail-Adresse: keine K15, aber T16 sagt es dem Team + // Kunde ohne E-Mail-Adresse: keine K15, T16 sagt es dem Team, und das Log nennt den Grund (T16: "Der Grund steht im Log des Portals") aw.seed('customers', { $id: 'c9', name: 'Ohne Adresse', email: '' }) - const ohneAdresse = await bezahleTermine({ customerId: 'c9' }) - assert.equal(ohneAdresse.k15Anzahl, 0) - assert.ok(ohneAdresse.t16.text.includes('Unterlagen von Hand nachsenden')) + const log3 = stumm() + try { + const ohneAdresse = await bezahleTermine({ customerId: 'c9' }) + assert.equal(ohneAdresse.k15Anzahl, 0) + assert.ok(ohneAdresse.t16.text.includes('Unterlagen von Hand nachsenden')) + const zeilen = log3.mock.calls.map((c) => c.arguments.join(' ')) + assert.ok(zeilen.some((z) => z.includes(`K15 (Funktion ${ohneAdresse.doc.$id}) nicht gesendet: beim Kunden ist keine E-Mail-Adresse hinterlegt`)), zeilen.join('\n')) + assert.doesNotMatch(zeilen.join('\n'), /Ohne Adresse/, 'keine Personendaten im Log') + } finally { + log3.mock.restore() + } +}) + +/** Verzeichnis mit Kopien der ausgelieferten Dateien unter dem Namen einer anderen Fassung (HTML und PDF je Dokument). */ +function dateienDerFassung(fassung) { + const dir = mkdtempSync(join(tmpdir(), 'k15-fassung-')) + mkdirSync(join(dir, 'rechtliches')) + const echt = legalPdfFiles('termine', V) + for (const f of Object.values(echt)) { + const slug = f.path.split('/').at(-1).replace(`-${V}.pdf`, '') + copyFileSync(f.path, join(dir, 'rechtliches', `${slug}-${fassung}.pdf`)) + copyFileSync(f.path.replace(/\.pdf$/, '.html'), join(dir, 'rechtliches', `${slug}-${fassung}.html`)) + } + return dir +} + +test('k15_vereinbarte_fassung: wurde die Fassung zwischen Klick und Zahlung angehoben, gehen PDFs und Links der vereinbarten Fassung raus, wenn es sie gibt', async () => { + const alt = '2026-10-01' + const dir = dateienDerFassung(alt) + const log = stumm() + try { + setLegalDirForTests(dir) + const vereinbart = Object.fromEntries(Object.keys(consents.zustimmungsfassungen(T)).map((k) => [k, alt])) + const { k15, t16, doc } = await bezahleTermine({ fassungen: vereinbart }) + assert.deepEqual(k15.attachments.map((a) => a.filename), [`Leistungsbeschreibung-Terminplaner-${alt}.pdf`, `AVV-Terminplaner-${alt}.pdf`, `Widerrufsbelehrung-Muster-Widerrufsformular-${alt}.pdf`]) + for (const a of k15.attachments) assert.equal(Buffer.from(a.content, 'base64').subarray(0, 5).toString('latin1'), '%PDF-') + for (const slug of ['leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'widerrufsbelehrung-funktionen']) { + assert.ok(k15.text.includes(`${BASE}/rechtliches/${slug}-${alt}.html`), `${slug}: Link der vereinbarten Fassung`) + assert.ok(!k15.text.includes(`${slug}-${V}.html`), `${slug}: kein Link der aktuellen Fassung`) + } + assert.ok(k15.text.includes(`Fassung ${alt}: Leistungsbeschreibung`)) + assert.match(k15.text, /Alle drei hängen als PDF an dieser E-Mail\./) + assert.doesNotMatch(t16.text, /Unterlagen von Hand nachsenden/) + assert.equal(doc.state, 'setup') + assert.ok(log.mock.calls.some((c) => String(c.arguments[0]).includes(`zugestimmt, aktuell ist ${V}`)), 'das Log nennt den Fassungswechsel') + } finally { + setLegalDirForTests(null) + log.mock.restore() + rmSync(dir, { recursive: true, force: true }) + } + // gibt es die Dateien der vereinbarten Fassung nicht, geht K15 ohne PDFs und ohne Links raus (Dateien der aktuellen Fassung bleiben ungenutzt) + const log2 = stumm() + try { + const fehlend = await bezahleTermine({ fassungen: { ...consents.zustimmungsfassungen(T), zustimmung: '2026-09-30' } }) + assert.ok(!fehlend.k15.attachments || fehlend.k15.attachments.length === 0) + assert.doesNotMatch(fehlend.k15.text, /Deine Vertragsunterlagen|\/rechtliches\//) + assert.match(fehlend.k15.text, /Deine Zustimmungen/) + assert.ok(fehlend.t16.text.includes('Unterlagen von Hand nachsenden')) + } finally { + log2.mock.restore() + } +}) + +test('k15_nachweis_nicht_lesbar: ohne lesbaren Nachweis ist die vereinbarte Fassung unbekannt: keine PDFs, keine Links, T16 anhang_fehlt, Log', async () => { + // (a) Lesefehler beim Nachweis + zaehler += 1 + const projectId = `ptermine${zaehler}` + const sessionId = `cs_ptermine${zaehler}` + aw.seed('websiteProjects', { $id: projectId, customerId: 'c1', projectName: `Salon ${zaehler}` }) + await sf.markOrdered({ customerId: 'c1', projectId, key: 'termine', sessionId, consent: {} }) + await consents.speichereZustimmung({ + customerId: 'c1', projectId, featureKey: 'termine', stripeSessionId: sessionId, fassungen: consents.zustimmungsfassungen(T), + texte: consents.nachweisTexte(T, { belehrung: widerrufsbelehrungFunktion() }), at: KLICK, + }) + const log = stumm() + try { + aw.failNext({ collection: 'featureConsents', method: 'GET', status: 500, message: 'GEHEIMER-ROHTEXT' }) + const vor = mails.sent.length + await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, 'termine')) + const neu = mails.sent.slice(vor) + const k15 = neu.find((m) => m.subject.startsWith('Deine Bestellung')) + const t16 = neu.find((m) => m.subject.startsWith('Neue Funktion bestellt')) + assert.ok(k15, 'die Bestätigung geht trotzdem raus') + assert.ok(!k15.attachments || k15.attachments.length === 0, 'nie die aktuelle Fassung ungeprüft anhängen') + assert.doesNotMatch(k15.text, /Deine Vertragsunterlagen|Deine Zustimmungen|\/rechtliches\//) + assert.ok(t16.text.includes('Unterlagen von Hand nachsenden')) + assert.equal((await sf.getFeatureDoc('c1', projectId, 'termine')).state, 'setup') + assert.ok(log.mock.calls.some((c) => String(c.arguments[0]).includes('die vereinbarte Fassung ist unbekannt')), 'das Log nennt den Grund') + } finally { + log.mock.restore() + } +}) + +test('k15_nachweis_ohne_fassung: ein Nachweis ohne lesbare Fassung zählt wie ein unlesbarer', async () => { + zaehler += 1 + const projectId = `ptermine${zaehler}` + const sessionId = `cs_ptermine${zaehler}` + aw.seed('websiteProjects', { $id: projectId, customerId: 'c1', projectName: `Salon ${zaehler}` }) + await sf.markOrdered({ customerId: 'c1', projectId, key: 'termine', sessionId, consent: {} }) + aw.seed('featureConsents', { + $id: `fc_kaputt_${zaehler}`, customerId: 'c1', projectId, featureKey: 'termine', stripeSessionId: sessionId, acceptedAt: KLICK, + fassungen: 'kein json', texte: JSON.stringify(consents.nachweisTexte(T, { belehrung: widerrufsbelehrungFunktion() })), textHash: 'x', + }) + const log = stumm() + try { + const vor = mails.sent.length + await sf.handleFeatureCheckoutEvent(paidEvent(sessionId, projectId, 'termine')) + const neu = mails.sent.slice(vor) + const k15 = neu.find((m) => m.subject.startsWith('Deine Bestellung')) + assert.ok(!k15.attachments || k15.attachments.length === 0) + assert.doesNotMatch(k15.text, /Deine Vertragsunterlagen|\/rechtliches\//) + assert.match(k15.text, /Deine Zustimmungen/, 'der Wortlaut der Zustimmungen steht trotzdem in der Mail') + assert.ok(neu.find((m) => m.subject.startsWith('Neue Funktion bestellt')).text.includes('Unterlagen von Hand nachsenden')) + assert.ok(log.mock.calls.some((c) => String(c.arguments[0]).includes('Fassung im Zustimmungsnachweis nicht bestimmbar'))) + } finally { + log.mock.restore() + } +}) + +test('k15_frisches_array: der Mailer bekommt für attachments immer ein eigenes, nicht eingefrorenes Array', async () => { + const roh = [] + setMailTransportForTests({ + async sendMail(mail) { + roh.push(mail) + return mails.sendMail(mail) + }, + }) + try { + aw.seed('websiteProjects', { $id: 'pnews12', customerId: 'c1', projectName: 'Frisch' }) + await sf.markOrdered({ customerId: 'c1', projectId: 'pnews12', key: 'news', sessionId: 'cs_news12', consent: {} }) + await sf.handleFeatureCheckoutEvent(paidEvent('cs_news12', 'pnews12')) + await bezahleTermine() + } finally { + setMailTransportForTests(mails) + } + const k15 = roh.filter((m) => String(m.subject).startsWith('Deine Bestellung')) + assert.equal(k15.length, 2) + for (const mail of k15) { + assert.ok(Array.isArray(mail.attachments)) + assert.equal(Object.isFrozen(mail.attachments), false) + } + assert.notEqual(k15[0].attachments, k15[1].attachments) + k15[0].attachments.push('x') // ein Mailer darf das Array verändern, ohne das gemeinsame Muster zu treffen + const nochmal = await bezahleTermine() + assert.equal(nochmal.k15.attachments.length, 3) }) test('checkout_503_ohne_unterlagen: fehlen die PDFs der aktuellen Fassung, 503 unterlagen_fehlen vor jeder Stripe-Sitzung, ohne ordered-Dokument', async () => { diff --git a/test/legalDocs.test.js b/test/legalDocs.test.js index 4076901..573573e 100644 --- a/test/legalDocs.test.js +++ b/test/legalDocs.test.js @@ -11,7 +11,7 @@ import { useTestEnv } from './support/testEnv.js' useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1' }) const { config } = await import('../server/config.js') const { startTestApp } = await import('./support/testApp.js') -const { legalPaths, legalPdfFiles, legalPdfAttachments, featureLegalAttachments, featureLegalReady, legalRouter, setLegalDirForTests } = await import( +const { legalPaths, legalPdfFiles, legalPdfAttachments, featureLegalAttachments, featureLegalReady, featureLegalPaths, legalRouter, setLegalDirForTests } = await import( '../server/services/legalDocs.js' ) const analyse = await import('../server/services/analyseLegal.js') @@ -200,6 +200,25 @@ test('featureLegalReady("termine"): je PDF stat und "%PDF-", nie den ganzen Inha assert.equal(await featureLegalReady('termine'), true, 'mit den ausgelieferten Dateien bereit') }) +// Fassung wählbar (Task F8, Fix-Runde 1): K15 schickt die Unterlagen der Fassung, der der Kunde zugestimmt hat +test('featureLegalAttachments / featureLegalReady / featureLegalPaths mit opts.fassung', async () => { + assert.deepEqual(featureLegalPaths('termine'), legalPaths('termine', V)) + assert.deepEqual(featureLegalPaths('termine', { fassung: '2026-10-01' }), legalPaths('termine', '2026-10-01')) + assert.equal(featureLegalPaths('news'), null) + assert.throws(() => featureLegalPaths('gibtsnicht'), /Funktion/) + assert.throws(() => featureLegalPaths('termine', { fassung: '2026-10-5' }), /Fassung/) + // ausdrücklich die aktuelle Fassung = ohne Angabe + assert.deepEqual((await featureLegalAttachments('termine', { fassung: V })).map((a) => a.filename), (await featureLegalAttachments('termine')).map((a) => a.filename)) + assert.equal(await featureLegalReady('termine', { fassung: V }), true) + // andere Fassung ohne Dateien: Wurf bei den Anhängen, false bei der Prüfung, news weiter [] + await assert.rejects(featureLegalAttachments('termine', { fassung: '1999-01-01' })) + assert.equal(await featureLegalReady('termine', { fassung: '1999-01-01' }), false) + assert.equal(await featureLegalReady('termine', { fassung: 'kaputt' }), false) + assert.deepEqual(await featureLegalAttachments('news', { fassung: '1999-01-01' }), []) + // opts.ohne wirkt weiter neben fassung + assert.deepEqual((await featureLegalAttachments('termine', { fassung: V, ohne: ['avv'] })).map((a) => a.filename.split('-')[0]), ['Leistungsbeschreibung', 'Widerrufsbelehrung']) +}) + test('config.termine.avvVersion: Datumsmuster, nach der Altfassung der Gästetexte, echter Tag', () => { assert.match(config.termine.avvVersion, /^\d{4}-\d{2}-\d{2}$/) assert.ok(config.termine.avvVersion > ALT, `${config.termine.avvVersion} liegt nicht nach ${ALT}`) diff --git a/test/termineVertragsunterlagen.test.js b/test/termineVertragsunterlagen.test.js index 11ca07a..41ff67d 100644 --- a/test/termineVertragsunterlagen.test.js +++ b/test/termineVertragsunterlagen.test.js @@ -32,6 +32,7 @@ const { setMailTransportForTests } = await import('../server/services/mailer.js' const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js') const { speichereEinstellung, resetKontextForTests } = await import('../server/services/termine/kontext.js') const { halte } = await import('../server/services/termine/ablauf.js') +const { loeschBezugsfeld } = await import('../server/services/termine/buchungen.js') after(() => setMailTransportForTests(null)) // Vertragsunterlagen des Terminplaners als Markdown: Leistungsbeschreibung (Task F4), AVV (Task F5). @@ -450,6 +451,14 @@ test('avv_gleich_datenschutz_absatz: Fristen und Orte stimmen mit dem Datenschut assert.ok(absatz.includes('nicht zustellbare E-Mails nach 48 Stunden')) assert.ok(absatz.includes('Rechenzentren Falkenstein und Nürnberg') && absatz.includes('Eine Übermittlung in Drittländer findet nicht statt.')) assert.ok(absatz.includes('Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen')) + // unbeantwortete Terminanfragen: derselbe Satz im Datenschutz-Absatz des Kunden und im AVV (Nr. 9 Abs. 2), und der Code tut es + assert.ok(absatz.includes('Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt.')) + const nr9abs2 = absatzTexte(abschnitt(AVV, 9))[1] + assert.ok(nr9abs2.startsWith('Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung.')) + assert.ok(nr9abs2.includes('Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.')) + assert.ok(JOBS.includes("angefragt mit Beginn in der Vergangenheit => abgelehnt vom System"), 'jobs.js lehnt verstrichene Anfragen ab') + assert.equal(loeschBezugsfeld('angefragt'), undefined, 'eine Anfrage wird nie unmittelbar gelöscht, erst als abgelehnte nach 30 Tagen') + assert.equal(loeschBezugsfeld('abgelehnt'), 'updatedAt', 'die 30 Tage laufen ab der letzten Änderung, also ab der Ablehnung durch das System') // der AVV nennt dieselben Zahlen const nr3 = avvNr(3) assert.ok(nr3.includes('In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).')) @@ -500,6 +509,12 @@ test('avv_keine_ip_in_buchung: weder das Schema noch eine über halte gehaltene assert.equal(buchungen.length, 1) const nachweis = JSON.parse(buchungen[0].nachweis) assert.ok(nachweis.fassung && nachweis.datenschutz, 'der Nachweis enthält Fassung und Hash des Datenschutz-Hinweises') + // genau die Bestandteile, die AVV Nr. 3 Abs. 1 und Nr. 8 Abs. 5 nennen: Fassung, Zeitpunkt, Knopftext, Anrede, Hashes (keine weiteren Klartexte) + const erlaubt = new Set(['fassung', 'am', 'knopf', 'anrede', 'datenschutz', 'storno', 'widerruf', 'vorzeitig', 'erinnerung']) + assert.deepEqual(Object.keys(nachweis).filter((k) => !erlaubt.has(k)), []) + for (const k of ['fassung', 'am', 'knopf', 'anrede']) assert.equal(typeof nachweis[k], 'string', k) + assert.deepEqual(Object.keys(nachweis.storno), ['version', 'hash', 'haken', 'am']) + assert.deepEqual(Object.keys(nachweis.erinnerung), ['stunden', 'hash', 'am']) const schluessel = (wert) => (wert && typeof wert === 'object' ? Object.entries(wert).flatMap(([k, v]) => [k, ...schluessel(v)]) : []) assert.deepEqual(schluessel(nachweis).filter((k) => nameIp.test(k)), []) // der Nachweis hält Hashes, keine Texte: jeder Hash ist 64 Hexzeichen @@ -516,6 +531,32 @@ test('avv_keine_ip_in_buchung: weder das Schema noch eine über halte gehaltene } }) +/** Jedes Attribut einer Buchung ist in AVV Nr. 3 Abs. 1 genannt (Wortlaut) oder eine technische Kennung/Eigenschaft des Dienstes. */ +const BUCHUNG_IM_AVV = { + gastName: 'Name und E-Mail-Adresse', gastEmail: 'Name und E-Mail-Adresse', gastTelefon: 'Telefonnummer', gastNachricht: 'eine Nachricht', gastZusatz: 'Zusatzfelder', + leistungName: 'die gebuchte Leistung', ressourceName: 'die Person oder der Platz', startsAt: 'der Termin', endsAt: 'der Termin', blockEndsAt: 'der Termin', + status: 'der Status der Buchung mit den Zeitpunkten', heldUntil: 'der Status der Buchung mit den Zeitpunkten', bestaetigtAt: 'der Status der Buchung mit den Zeitpunkten', + angenommenAt: 'der Status der Buchung mit den Zeitpunkten', abgesagtAt: 'der Status der Buchung mit den Zeitpunkten', abgesagtVon: 'der Status der Buchung mit den Zeitpunkten', + widerrufenAt: 'der Status der Buchung mit den Zeitpunkten', createdAt: 'der Status der Buchung mit den Zeitpunkten', updatedAt: 'der Status der Buchung mit den Zeitpunkten', + absageGrund: 'Absagegrund und Vermerke zu einer Absage oder einem Widerruf', nachweis: 'der Nachweis der Zustimmungen', + preisCent: 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde', zahlart: 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde', + zahlStatus: 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde', verbraucher: 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde', + erinnerung: 'der Wunsch nach einer Erinnerung', erinnerungGesendetAt: 'der Wunsch nach einer Erinnerung', +} +const BUCHUNG_TECHNISCH = ['featureId', 'customerId', 'projectId', 'leistungId', 'ressourceId', 'preisArt', 'buchungsart', 'quelle', 'verifyHash', 'manageHash', 'stripeSessionId', 'stripePaymentIntent', 'icsSequenz'] + +test('avv_datenarten_decken_schema: jedes Attribut einer Buchung steht in Nr. 3 Abs. 1 oder ist eine technische Kennung', () => { + const nr3abs1 = norm(absatzTexte(abschnitt(AVV, 3))[0]) + const attribute = TERMINE_COLLECTIONS.termineBookings.attributes.map((a) => a.key) + const unbekannt = attribute.filter((k) => !Object.hasOwn(BUCHUNG_IM_AVV, k) && !BUCHUNG_TECHNISCH.includes(k)) + assert.deepEqual(unbekannt, [], 'neues Attribut der Buchung: in AVV Nr. 3 Abs. 1 aufnehmen oder als technisch einstufen') + for (const [k, wort] of Object.entries(BUCHUNG_IM_AVV)) { + assert.ok(attribute.includes(k), `${k} gibt es nicht mehr im Schema`) + assert.ok(nr3abs1.includes(wort), `${k}: „${wort}“ fehlt in Nr. 3 Abs. 1`) + } + for (const k of BUCHUNG_TECHNISCH) assert.ok(attribute.includes(k), `${k} gibt es nicht mehr im Schema`) +}) + test('avv_verweise_auf_lb: jedes „Nr. N Abs. M der Leistungsbeschreibung“ und jedes „Nummer N“ im AVV zeigt auf Vorhandenes', () => { const text = norm(AVV_MD) @@ -600,7 +641,8 @@ test('avv_inhalt: Abschluss, Zwecke ohne Eigennutz, Datenarten, Weisungen, Daten const nr3 = avvNr(3) for (const wort of [ 'Name und E-Mail-Adresse', 'Telefonnummer', 'Zusatzfelder', 'die gebuchte Leistung, die Person oder der Platz und der Termin', 'der Status der Buchung mit den Zeitpunkten', - 'Zeitpunkt und als Hashes (Prüfsummen) der gezeigten Texte', 'der Wunsch nach einer Erinnerung', 'der Zustellstatus der E-Mails an den Gast', + 'Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte', 'der Wunsch nach einer Erinnerung', 'der Zustellstatus der E-Mails an den Gast', + 'Absagegrund und Vermerke zu einer Absage oder einem Widerruf', 'auch mit dem Namen aus einer Widerrufserklärung', 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde', 'Namen oder Bezeichnungen der Personen oder Plätze', 'Arbeitszeiten und Abwesenheiten', 'geheimer Kalender-Link', ]) assert.ok(nr3.includes(wort), `Nr. 3 nennt ${wort}`) assert.ok(nr3.includes('Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet.')) @@ -611,12 +653,14 @@ test('avv_inhalt: Abschluss, Zwecke ohne Eigennutz, Datenarten, Weisungen, Daten const nr4 = avvNr(4) assert.ok(nr4.includes('Mit der Bestellung weist der Kunde WEBklar an')) assert.ok(nr4.includes('Sonstige Weisungen erteilt der Kunde in Textform')) + assert.ok(nr4.includes('Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.')) const nr8 = avvNr(8) assert.ok(nr8.includes('Gastdaten löschen') && nr8.includes('CSV-Datei')) assert.ok(nr8.includes('möglichst innerhalb von 24 Stunden nach Kenntnis')) assert.ok(nr8.includes('bis drei Jahre nach dem Ende des Terminplaners')) - assert.ok(nr8.includes('nur den Zeitpunkt und die Hashes der Texte')) + assert.ok(nr8.includes('die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte')) + assert.doesNotMatch(nr8, /nur den Zeitpunkt/) const nr11 = avvNr(11) assert.ok(nr11.includes('Art. 13 DSGVO') && nr11.includes('Datenschutz-Absatz') && nr11.includes('Beschäftigten')) @@ -673,6 +717,9 @@ const TOM_FUNDSTELLEN = { { datei: 'server/services/termine/schema.js', hat: /Eine Einstellung je Funktion; Dokument-ID = ID des portalFeatures-Dokuments/ }, { datei: 'server/services/termine/schema.js', hat: /str\('featureId', 64\)/ }, { datei: 'server/services/termine/buchungen.js', hat: /Query\.equal\('featureId', featureId\)/ }, + // die Hintergrundläufe arbeiten über alle Funktionen (Löschlauf ohne Funktionsfilter, Aufräumen der Warteschlange nach Status) und löschen Dokument für Dokument + { datei: 'server/services/termine/buchungen.js', hat: /featureId === null \? \[\] : \[Query\.equal\('featureId', featureId\)\]/ }, + { datei: 'server/services/termine/outbox.js', hat: /Query\.equal\('status', \['gesendet', 'fehler', 'verworfen'\]\)/ }, ], 'Löschung': [ { datei: 'server/index.js', hat: /setInterval\(termineTagesJob, 24 \* 60 \* 60 \* 1000\)/ }, @@ -680,6 +727,9 @@ const TOM_FUNDSTELLEN = { { datei: 'server/services/termine/jobs.js', hat: /im Zweifel wird nichts geloescht/ }, { datei: 'server/services/termine/jobs.js', hat: /ABGESCHALTET_BEHALTEN_MS = LOESCHFRIST_TAGE \* TAG_MS/ }, { datei: 'server/services/termine/entwuerfe.js', hat: /export const ENTWURF_TAGE = 90/ }, + { datei: 'server/services/termine/entwuerfe.js', hat: /const funktion = await holeOderNull\(config\.collections\.portalFeatures, id\)/ }, + // die Warteschlange löscht die gelisteten Einträge ohne erneutes Lesen (nur Buchungen, abgeschaltete Funktionen und Entwürfe lesen frisch) + { datei: 'server/services/termine/outbox.js', hat: /await deleteDocument\(col\(\), eintrag\.\$id\)/ }, ], 'Protokolle': [ { datei: 'test/termineJobs.test.js', hat: /kein Gastname und keine Gastadresse im Log/ }, @@ -730,6 +780,12 @@ test('avv_tom_fundstellen: jede Zeile der Anlage hat ihre Fundstellen im Code, i assert.ok(zeilen['Datensicherung'].includes('die letzten sieben Tagessicherungen')) assert.ok(zeilen['Rechenzentrum'].includes('ISO/IEC 27001') && zeilen['Rechenzentrum'].includes('Falkenstein und Nürnberg')) assert.ok(zeilen['Löschung'].includes('30 Tage nach dem Abschalten')) + assert.ok(zeilen['Löschung'].includes('Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen')) + assert.doesNotMatch(zeilen['Löschung'], /Vor jeder Löschung/) + assert.ok(zeilen['Trennung'].includes('Das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion.')) + assert.ok(zeilen['Trennung'].includes('Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht.')) + assert.doesNotMatch(zeilen['Trennung'], /Abfragen erfolgen nur je Funktion/) + assert.ok(zeilen['Datenminimierung'].includes('der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte')) assert.ok(zeilen['Speicher im Browser'].includes('keine Cookies')) assert.ok(zeilen['Zugriffskontrolle'].includes('konstanter Zeit')) assert.ok(zeilen['Organisation'].includes('Nummer 8 Abs. 3') && zeilen['Organisation'].includes('Nummer 8 Abs. 5'))