163 lines
8.7 KiB
JavaScript
163 lines
8.7 KiB
JavaScript
#!/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)
|
|
// 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 bei einer Leistung mit Preis, wenn der Betrieb Stornobedingungen hat; bei kostenlosen Leistungen fragt der Terminplaner nicht danach; 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
|
|
}
|
|
}
|