Gaestetexte: Archiv-Skript und eingefrorene Fassung 2026-10-02
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
01bf05cc35
commit
1a91a331b4
4 files changed
+412
No files matched your search
@@ -0,0 +1,152 @@
|
||||
#!/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-<Fassung>.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)
|
||||
//
|
||||
// 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]`. root = Wurzel des Projekts (Tests übergeben ein Verzeichnis).
|
||||
* => Exit-Code: 0 ok, 1 die Datei weicht ab oder fehlt (nur --check), 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]')
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user