diff --git a/docs/rechtliches/termine-hinweise.md b/docs/rechtliches/termine-hinweise.md index 841be00..e99c7b8 100644 --- a/docs/rechtliches/termine-hinweise.md +++ b/docs/rechtliches/termine-hinweise.md @@ -1,6 +1,6 @@ # 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. +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 geben Texte des Widgets und der Mails wieder (Knopftexte: `knopfText` in `server/services/termine/einstellungen.js`; Widerrufshinweis: Mailvorlage `server/mail-templates/G02 Termin bestaetigt.html`; Eingangsbestätigung: `server/mail-templates/G08 Eingang Widerruf.html`); maßgeblich sind die Mailvorlagen und das Widget, nicht dieses Dokument. Platzhalter: `{{Firma}}` = Betriebsname, `{{Anschrift}}` = Anschrift des Betriebs, `{{n}}` = Stunden vor dem Termin (`erinnerung.stundenVorher`: 2, 12, 24 oder 48). Die Texte zeigen die Vorgabe von 90 Tagen Aufbewahrung nach dem Termin; der Wert kommt aus `aufbewahrungTage` (14 bis 365) der Einstellung. @@ -47,11 +47,11 @@ Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt (Spezifikation, | Anfrage | Termin anfragen | Anfrage absenden | | Widerruf (Funktion) | Vertrag widerrufen | Widerruf bestätigen | -## 5. Widerrufshinweis im Widget und in der Mail (Entwurf, Rahmen) +## 5. Widerrufshinweis in der Bestätigungs-E-Mail Er ersetzt nicht die Widerrufsbelehrung (`termine-widerrufsbelehrung-muster.md`), sondern führt zu ihr. Gilt für Buchungen von Verbrauchern, auch für Anfragen: Der Vertrag kommt mit der Annahme zustande, ist aber ebenfalls ein Fernabsatzvertrag, und die Frist läuft ab der Annahme (R55, R29). Die Widerrufsfunktion steht im Widget als zweiter Knopf im Fuß und im Knopfmodus dauerhaft neben dem Buchungsknopf (R54). -**Mit Widerrufsrecht** (Zusammenfassung vor dem Knopf und G02 vor der Belehrung): +**Mit Widerrufsrecht** (Bestätigungs-E-Mail G02, vor der Belehrung; das Widget zeigt dort die ausführliche Belehrung zum Aufklappen): > **Widerrufsrecht.** Wenn Sie Verbraucher sind, können Sie diesen Vertrag innerhalb von 14 Tagen ab Vertragsschluss ohne Angabe von Gründen widerrufen. Dafür nutzen Sie die Schaltfläche „Vertrag widerrufen“ im Terminbereich auf unserer Website oder den Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail. Die ausführliche Widerrufsbelehrung und das Muster-Widerrufsformular stehen in Ihrer Bestätigungs-E-Mail. @@ -63,7 +63,7 @@ Maßgeblich für die Mail ist die Vorlage G02: Bis zum Beginn des Termins geht d Gäste, die nur Unternehmen sind (`recht.gaesteSind = unternehmer`): kein Hinweis. -## 6. Widerrufsfunktion (Entwurf der Texte) +## 6. Widerrufsfunktion (Texte) - Schaltfläche oder Link: **Vertrag widerrufen** (im Terminbereich der Website, auf der Verwaltungsseite hinter „Termin verwalten“). - Formular ohne Anmeldung: Name · Buchungsnummer · E-Mail-Adresse für die Eingangsbestätigung. Schaltfläche: **Widerruf bestätigen**. diff --git a/scripts/termine-texte-archiv.mjs b/scripts/termine-texte-archiv.mjs index 4fba3b4..844e89c 100644 --- a/scripts/termine-texte-archiv.mjs +++ b/scripts/termine-texte-archiv.mjs @@ -6,6 +6,7 @@ // // node scripts/termine-texte-archiv.mjs schreibt die Datei der aktuellen Fassung // node scripts/termine-texte-archiv.mjs --check vergleicht mit der eingecheckten Datei (Exit 1 bei Abweichung, schreibt nichts) +// node scripts/termine-texte-archiv.mjs --force überschreibt eine abweichende Datei der aktuellen Fassung (ohne --force: Exit 1) // // Die Betriebsangaben stehen als Platzhalter im Text ({{Name des Betriebs}} usw.); die Ausgabe enthält kein Datum und hängt nur von // rechtstexte.js und einstellungen.js ab (zweimal erzeugen ergibt dieselben Bytes). Die Datei einer früheren Fassung wird von diesem @@ -121,12 +122,14 @@ export function baueArchiv() { } /** - * Kommandozeile: `[--check]`. root = Wurzel des Projekts (Tests übergeben ein Verzeichnis). - * => Exit-Code: 0 ok, 1 die Datei weicht ab oder fehlt (nur --check), 2 falscher Aufruf + * Kommandozeile: `[--check] [--force]` (nicht beides). root = Wurzel des Projekts (Tests übergeben ein Verzeichnis). + * Eine Datei, die es schon gibt und deren Inhalt abweicht, wird nur mit --force überschrieben: ist die Fassung ausgeliefert, + * darf ihr Wortlaut nicht still ersetzt werden (neue Fassung, neue Datei). Eine neue oder identische Datei schreibt das Skript immer. + * => Exit-Code: 0 ok, 1 die Datei weicht ab oder fehlt (--check) bzw. weicht ab und --force fehlt, 2 falscher Aufruf */ export function lauf(argv, { root = ROOT, log = console.log, fehler = console.error } = {}) { - if (argv.some((o) => o !== '--check')) { - fehler('Aufruf: node scripts/termine-texte-archiv.mjs [--check]') + if (argv.some((o) => o !== '--check' && o !== '--force') || (argv.includes('--check') && argv.includes('--force'))) { + fehler('Aufruf: node scripts/termine-texte-archiv.mjs [--check] [--force] (nicht beides)') return 2 } const pfad = archivPfad() @@ -137,6 +140,13 @@ export function lauf(argv, { root = ROOT, log = console.log, fehler = console.er log(`${gleich ? 'ok ' : 'ABWEICHT'} ${pfad}`) return gleich ? 0 : 1 } + if (!argv.includes('--force') && existsSync(ziel) && readFileSync(ziel, 'utf8') !== inhalt) { + fehler( + `${pfad} weicht ab (von den Texten in rechtstexte.js) und wird nicht überschrieben. Ist die Fassung ausgeliefert, bekommt eine Textänderung ` + + 'eine neue Fassung (neues Datum, neue Datei). Nur wenn die Fassung noch nicht ausgeliefert ist: mit --force überschreiben.', + ) + return 1 + } writeFileSync(ziel, inhalt) log(`-> ${pfad}`) return 0 diff --git a/server/services/termine/rechtstexte.js b/server/services/termine/rechtstexte.js index 720fb47..3b443b4 100644 --- a/server/services/termine/rechtstexte.js +++ b/server/services/termine/rechtstexte.js @@ -1,6 +1,7 @@ /** * 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 + * Reine Funktionen und Konstanten, ohne Appwrite; sie nutzen Hilfsfunktionen aus einstellungen.js (zeigtWiderruf) und lesen + * config.termine.holdMinutes (Gueltigkeit des Bestaetigungslinks im Datenschutz-Absatz, bei jedem Aufruf gelesen), sonst keine Konfiguration. Die Texte * sind Rechtstexte und stehen deshalb immer in der Sie-Form (auch bei Du-Betrieben); nur der Datenschutz-Hinweis unter dem * Formular und der Datenschutz-Absatz folgen der Anrede. * diff --git a/test/termineRechtstexte.test.js b/test/termineRechtstexte.test.js index 242f001..5dbdc6e 100644 --- a/test/termineRechtstexte.test.js +++ b/test/termineRechtstexte.test.js @@ -546,6 +546,19 @@ test('Hinweise-Dokument: Formular-Hinweis (Sie/Du) und alle Zustimmungstexte ste } }) +test('hinweise_ohne_entwurf: das Hinweise-Dokument nennt kein „Entwurf“ (auch nicht klein) und sagt, dass Vorlagen und Widget für die Abschnitte 4 bis 6 maßgeblich sind', () => { + const md = lies('docs/rechtliches/termine-hinweise.md').replace(/\r/g, '') + assert.ok(!/Entwurf/i.test(md), 'termine-hinweise.md: das Wort Entwurf steht noch im Dokument') + assert.ok(md.includes('## 5. Widerrufshinweis in der Bestätigungs-E-Mail')) + assert.ok(md.includes('## 6. Widerrufsfunktion (Texte)')) + const kopf = md.split('\n')[2] + assert.ok(kopf.includes('Die Abschnitte 4 bis 6 geben Texte des Widgets und der Mails wieder'), 'Zeile 3 sagt, was die Abschnitte 4 bis 6 sind') + assert.ok(kopf.includes('maßgeblich sind die Mailvorlagen und das Widget'), 'Zeile 3 nennt Vorlagen und Widget als maßgeblich') + // die genannten Quellen gibt es + assert.ok(md.includes('server/mail-templates/G02 Termin bestaetigt.html') && existsSync(new URL('../server/mail-templates/G02 Termin bestaetigt.html', import.meta.url))) + assert.ok(md.includes('server/services/termine/einstellungen.js') && existsSync(new URL('../server/services/termine/einstellungen.js', import.meta.url))) +}) + test('Datenschutz-Absatz: Löschfristen der Vorgaben, Sie- und Du-Fassung, Auftragsverarbeiter und Gesundheitshinweis', () => { const md = lies('docs/rechtliches/termine-datenschutz-absatz.md').replace(/\r/g, '') assert.ok(/Fassung Sie/i.test(md) && /Fassung Du/i.test(md)) diff --git a/test/termineRechtstexteArchiv.test.js b/test/termineRechtstexteArchiv.test.js index 60e8047..c9e250a 100644 --- a/test/termineRechtstexteArchiv.test.js +++ b/test/termineRechtstexteArchiv.test.js @@ -60,6 +60,35 @@ test('archiv_aktuelle_fassung: --check vergleicht mit der eingecheckten Datei (E assert.match(meldungen[0], /Aufruf: node scripts\/termine-texte-archiv\.mjs \[--check\]/) }) +test('archiv_schutz: eine abweichende Datei der aktuellen Fassung wird nur mit --force überschrieben; gleiche oder neue Dateien bleiben erlaubt', (t) => { + const root = mitTempRoot(t) + const ziel = join(root, archivPfad()) + const meldungen = [] + const mitFehler = { log: () => {}, fehler: (m) => meldungen.push(m) } + // neue Datei und identische Datei: erlaubt + assert.equal(lauf([], { ...mitFehler, root }), 0) + assert.equal(readFileSync(ziel, 'utf8'), baueArchiv()) + assert.equal(lauf([], { ...mitFehler, root }), 0, 'derselbe Inhalt darf erneut geschrieben werden') + assert.deepEqual(meldungen, []) + // abweichende Datei (zum Beispiel die ausgelieferte Fassung mit einem nachträglich geänderten Text): Exit 1, Datei unverändert + const ausgeliefert = `${baueArchiv()}Nachträglich geändert.\n` + writeFileSync(ziel, ausgeliefert) + assert.equal(lauf([], { ...mitFehler, root }), 1) + assert.equal(readFileSync(ziel, 'utf8'), ausgeliefert, 'ohne --force bleibt die Datei, wie sie war') + assert.match(meldungen[0], /weicht ab/) + assert.match(meldungen[0], /--force/) + assert.ok(meldungen[0].includes(archivPfad()), 'die Meldung nennt die Datei') + // mit --force wird überschrieben + assert.equal(lauf(['--force'], { ...mitFehler, root }), 0) + assert.equal(readFileSync(ziel, 'utf8'), baueArchiv()) + // --check bleibt schreibfrei; --force zusammen mit --check ist ein falscher Aufruf + writeFileSync(ziel, ausgeliefert) + assert.equal(lauf(['--check'], { ...mitFehler, root }), 1) + assert.equal(readFileSync(ziel, 'utf8'), ausgeliefert) + assert.equal(lauf(['--check', '--force'], { ...mitFehler, root }), 2) + assert.equal(readFileSync(ziel, 'utf8'), ausgeliefert) +}) + test('archiv_inhalt: alle Gästetexte stehen wörtlich im Archiv, die Betriebsangaben als Platzhalter', () => { const text = baueArchiv() const zitat = (s) => s.split('\n').map((z) => (z === '' ? '>' : `> ${z}`)).join('\n')