Files

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
}
}