#!/usr/bin/env node // Archiv der Gästetexte des Terminplaners: schreibt den Wortlaut der Texte, die ein Gast in der aktuellen Fassung // (RECHTSTEXTE_VERSION in server/services/termine/rechtstexte.js) sieht, nach // docs/rechtliches/archiv/termine-gaestetexte-.md (AVV Terminplaner Nr. 8 Abs. 5: der Nachweis einer Buchung speichert // nur Hashes, das Archiv hält den Wortlaut zu jeder Fassung fest). // // 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 // Skript nie angefasst: sie bleibt, wie sie war (test/termineRechtstexteArchiv.test.js pinnt ihren Hash). import { existsSync, readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { knopfText } from '../server/services/termine/einstellungen.js' import { GESUNDHEIT_HINWEIS, RECHTSTEXTE_VERSION, ZUSTIMMUNG_ERINNERUNG, ZUSTIMMUNG_STORNO, ZUSTIMMUNG_VORZEITIG, datenschutzAbsatz, formularHinweis, widerrufsbelehrung, } from '../server/services/termine/rechtstexte.js' const ROOT = fileURLToPath(new URL('..', import.meta.url)) /** Betriebsangaben als Platzhalter (wie in docs/rechtliches/termine-widerrufsbelehrung-muster.md). */ export const PLATZHALTER_BETRIEB = Object.freeze({ name: '{{Name des Betriebs}}', adresse: '{{Anschrift}}', telefon: '{{Telefon}}', email: '{{E-Mail}}' }) const PLATZHALTER_STUNDEN = '{{Stunden}}' /** Pfad der Archivdatei einer Fassung, relativ zur Wurzel des Projekts. */ export const archivPfad = (version = RECHTSTEXTE_VERSION) => `docs/rechtliches/archiv/termine-gaestetexte-${version}.md` /** Einstellung mit Platzhalter-Betrieb und allen Schaltern an: der Datenschutz-Absatz erscheint mit allen bedingten Sätzen. */ function einstellung(anrede) { return { anrede, aufbewahrungTage: 90, betrieb: PLATZHALTER_BETRIEB, gastFelder: { telefon: 'optional', nachricht: 'optional', zusatz: [] }, erinnerung: { anbieten: true, stundenVorher: 24 }, recht: { gaesteSind: 'verbraucher', freizeit: false }, ressourcen: [{ id: 'r1', aktiv: true }], leistungen: [{ id: 'l1', aktiv: true, ressourcen: ['r1'] }], } } /** Text als Blockquote: jede Zeile mit "> ", Leerzeilen als ">". */ const zitat = (text) => text.split('\n').map((zeile) => (zeile === '' ? '>' : `> ${zeile}`)).join('\n') const SOFORT = { buchungsart: 'sofort', preisArt: 'keiner', preisCent: 0 } const MIT_PREIS = { buchungsart: 'sofort', preisArt: 'fest', preisCent: 1 } const ANFRAGE = { buchungsart: 'anfrage', preisArt: 'keiner', preisCent: 0 } function knopfTabelle() { const zeilen = [ ['Sofort buchbar, ohne Preis', SOFORT], ['Sofort buchbar, mit Preis', MIT_PREIS], ['Terminanfrage', ANFRAGE], ] return [ '| Leistung | Knopf im Widget | Knopf auf der Bestätigungsseite |', '|---|---|---|', ...zeilen.map(([name, leistung]) => `| ${name} | ${knopfText(leistung)} | ${knopfText(leistung, { bestaetigen: true })} |`), ].join('\n') } /** Der ganze Inhalt der Archivdatei der aktuellen Fassung. */ export function baueArchiv() { const teile = [ `# Gästetexte des Terminplaners, Fassung ${RECHTSTEXTE_VERSION}`, [ 'Dieses Archiv hält den Wortlaut der Gästetexte fest, die ein Gast in dieser Fassung sieht (AVV Terminplaner Nr. 8 Abs. 5). ' + 'Der Nachweis einer Buchung speichert die Fassung, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes (SHA-256) der übrigen Texte, nicht die Texte selbst.', `Fassung der Gästetexte: **${RECHTSTEXTE_VERSION}** (\`RECHTSTEXTE_VERSION\` in \`server/services/termine/rechtstexte.js\`). ` + 'Der Zeitraum der Verwendung ergibt sich aus „gültig ab“ dieser Fassung und der nächsten Fassung in der Tabelle „Terminplaner-Gästetexte“ in `README.md` dieses Ordners.', 'Erzeugt mit `node scripts/termine-texte-archiv.mjs`, geprüft mit `--check`. Nicht von Hand ändern: eine Textänderung bekommt eine neue Fassung und eine neue Datei; diese Datei bleibt, wie sie war.', 'Die Betriebsangaben stehen als Platzhalter: `{{Name des Betriebs}}`, `{{Anschrift}}`, `{{Telefon}}` und `{{E-Mail}}` stammen aus der Einstellung des Betriebs, `{{Stunden}}` ist die gewählte Vorlaufzeit der Erinnerung (der Nachweis speichert sie), ' + 'die Aufbewahrung nach dem Termin ist mit der Vorgabe von 90 Tagen dargestellt (einstellbar von 14 bis 365 Tagen).', ].join('\n\n'), '## 1. Hinweis unter dem Buchungsformular (Datenschutz)', ['### Sie-Fassung', zitat(formularHinweis(einstellung('sie'))), '### Du-Fassung', zitat(formularHinweis(einstellung('du')))].join('\n\n'), '## 2. Haken', [ 'Die Haken stehen immer in der Sie-Form, auch bei Betrieben, die ihre Gäste duzen.', '### Vorzeitiger Beginn (Pflicht bei einer Leistung mit Preis für Verbraucher, wenn der Termin vor Ablauf der Widerrufsfrist beginnt, bei Terminanfragen immer)', zitat(ZUSTIMMUNG_VORZEITIG(PLATZHALTER_BETRIEB.name)), '### Stornobedingungen (Pflicht, wenn der Betrieb Stornobedingungen hat; der Text der Bedingungen steht beim Betrieb und trägt eine eigene Version)', zitat(ZUSTIMMUNG_STORNO), '### Erinnerung (freiwillig, nie vorangekreuzt, nur wenn der Betrieb Erinnerungen anbietet)', zitat(ZUSTIMMUNG_ERINNERUNG(PLATZHALTER_STUNDEN)), ].join('\n\n'), '## 3. Hinweis zu Gesundheitsangaben', ['Steht über dem Nachrichtenfeld des Formulars.', zitat(GESUNDHEIT_HINWEIS)].join('\n\n'), '## 4. Knopftexte', [knopfTabelle(), 'Der Text des Buchungsknopfs im Widget steht im Nachweis (`knopf`).'].join('\n\n'), '## 5. Widerrufsbelehrung mit Muster-Widerrufsformular für Gäste', ['Sie-Form, mit den Betriebsangaben als Platzhalter.', zitat(widerrufsbelehrung(PLATZHALTER_BETRIEB))].join('\n\n'), '## 6. Datenschutz-Absatz „Online-Terminbuchung“ für die Datenschutzerklärung der Website des Betriebs', [ 'Hier mit allen bedingten Sätzen (Telefon-Feld, Nachricht oder Zusatzfelder, Erinnerung, Widerruf, Telefonnummer des Betriebs); ein Betrieb ohne die jeweilige Funktion hat den Satz nicht.', '### Sie-Fassung', zitat(datenschutzAbsatz(einstellung('sie'), 'sie')), '### Du-Fassung', zitat(datenschutzAbsatz(einstellung('du'), 'du')), ].join('\n\n'), '## 7. Nachweis', 'Der Nachweis speichert Hashes der Texte mit den Betriebsangaben zum Buchungszeitpunkt (⚖ Q26).', ] return `${teile.join('\n\n')}\n` } /** * 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' && 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() const ziel = join(root, pfad) const inhalt = baueArchiv() if (argv.includes('--check')) { const gleich = existsSync(ziel) && readFileSync(ziel, 'utf8') === inhalt 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 } if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { try { process.exitCode = lauf(process.argv.slice(2)) } catch (err) { console.error(err.message) process.exitCode = 1 } }