Terminplaner: Abschluss – Server (Widerruf auch bei Anfragen, Gastdaten löschen vollständig, formMinSekunden)

- R55: Widerrufsbelehrung, Anschrift-Pflicht und Knopf "Vertrag widerrufen" gelten für jede
  Verbraucher-Buchung mit sichtbarer Leistung, auch Anfragen (zeigtWiderruf); der Nachweis
  hält die Belehrung auch bei Anfragen fest. Damit bleibt die Annahme einer Anfrage nie mehr
  still ohne G02.
- "Gastdaten löschen" leert auch absageGrund und löscht noch nicht versandte Mails der Buchung
  (neu: verwerfeWartende in der Warteschlange).
- R53: GET /:siteKey/config nennt formMinSekunden.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:44 +00:00
1 parent 4be886a4de
commit bd65c9dc58
11 files changed
+216 -18

No files matched your search

+1 -1
View File
@@ -280,7 +280,7 @@ function baueNachweis(e, g, verbraucher, now) {
const am = iso(now)
const nachweis = { fassung: RECHTSTEXTE_VERSION, am, knopf: knopfText(g.leistung), anrede: e.anrede, datenschutz: sha256(formularHinweis(e)) }
if (g.stornoNoetig) nachweis.storno = { version: e.storno.version, hash: sha256(e.storno.text), haken: sha256(ZUSTIMMUNG_STORNO), am }
if (verbraucher && g.leistung.buchungsart === 'sofort') nachweis.widerruf = { hash: sha256(widerrufsbelehrung(e.betrieb)) }
if (verbraucher) nachweis.widerruf = { hash: sha256(widerrufsbelehrung(e.betrieb)) } // R55: auch bei Anfragen (das Widget zeigt die Belehrung jeder Verbraucher-Buchung)
if (g.vorzeitigNoetig) nachweis.vorzeitig = { hash: sha256(ZUSTIMMUNG_VORZEITIG(e.betrieb.name)), am }
if (g.erinnerung) {
const stunden = e.erinnerung.stundenVorher
+7 -1
View File
@@ -3,6 +3,7 @@ import { withLocks } from '../keyedLock.js'
import { aendere, hole, legeAn, loesche } from './buchungen.js'
import { text as bereinigterText } from './einstellungen.js'
import { anGast } from './mails.js'
import { verwerfeWartende } from './outbox.js'
import {
MELDUNG,
VERWALTBAR,
@@ -172,18 +173,23 @@ async function nichterschienen(_ctx, frisch, _eingabe, now) {
/**
* Gastdaten auf Wunsch loeschen: Name "Gelöscht", E-Mail, Telefon, Nachricht und Zusatzfelder leer, keine Erinnerung,
* Links ungueltig (Hashes leer). Status und Zeiten bleiben. Bei abgesagt/abgelehnt bleibt updatedAt stehen, weil davon
* Links ungueltig (Hashes leer), absageGrund leer (er kann den vom Gast getippten Namen einer Widerrufserklaerung oder
* einen frei getippten Grund des Betriebs enthalten). Noch nicht versandte Mails der Buchung (Outbox, Status wartet)
* werden geloescht, sie halten Adresse und Inhalt; zuerst, damit ein Fehler dort noch nichts geaendert hat und ein
* zweiter Klick alles nachholt. Status und Zeiten bleiben. Bei abgesagt/abgelehnt bleibt updatedAt stehen, weil davon
* die Loeschfrist (30 Tage) laeuft. Eine noch nicht bestaetigte Haltung bleibt unberuehrt (409).
*/
async function anonymisieren(_ctx, frisch, _eingabe, now) {
if (frisch.status === 'gehalten') throw nichtMoeglich(MELDUNG_BETRIEB.nochOffen)
const behalteFrist = frisch.status === 'abgesagt' || frisch.status === 'abgelehnt'
await verwerfeWartende(frisch.$id)
const buchung = await aendere(frisch.$id, {
gastName: GELOESCHT,
gastEmail: '',
gastTelefon: '',
gastNachricht: '',
gastZusatz: '',
absageGrund: '',
erinnerung: false,
verifyHash: '',
manageHash: '',
+3 -2
View File
@@ -572,10 +572,11 @@ function sichtbareLeistungen(einstellung) {
/**
* Zeigt die oeffentliche Sicht eine Widerrufsbelehrung (oeffentlicheEinstellung.widerruf)? Verbraucher-Gaeste und
* mindestens eine sichtbare Leistung, die sofort gebucht wird.
* mindestens eine sichtbare Leistung, gleich ob sofort oder als Anfrage (Ruling R55: auch ein Anfrage-Vertrag, der mit
* der Annahme zustande kommt, ist ein Fernabsatzvertrag; die Belehrung steht im G02 jeder Verbraucher-Buchung).
*/
export function zeigtWiderruf(einstellung) {
return istVerbraucher(einstellung) && sichtbareLeistungen(einstellung).some((l) => l.buchungsart === 'sofort')
return istVerbraucher(einstellung) && sichtbareLeistungen(einstellung).length > 0
}
/**
+36
View File
@@ -316,6 +316,42 @@ export async function raeumeAuf(now) {
return geloescht
}
/**
* Gastdaten loeschen (Portal "Gastdaten löschen"): noch nicht versandte Mails einer Buchung (Status wartet) werden
* geloescht, denn sie halten Adresse und Inhalt. Je Eintrag unter der Sendesperre `termine-mail:<id>` nach neuem Lesen:
* ein Eintrag, der inzwischen gesendet wurde (seine Felder sind dann schon leer), bleibt; ein inzwischen geloeschter
* stoert nicht. Gesendete und fehlgeschlagene Eintraege behandelt raeumeAuf.
* @param {string} bookingId
* @returns {Promise<number>} Anzahl geloeschter Eintraege
*/
export async function verwerfeWartende(bookingId) {
pruefeText(bookingId, 'bookingId')
const wartend = await listAllDocuments(col(), [Query.equal('status', 'wartet'), Query.equal('bookingId', bookingId)])
let geloescht = 0
for (const eintrag of wartend) {
const weg = await withLocks([`termine-mail:${eintrag.$id}`], async () => {
let frisch
try {
frisch = await getDocument(col(), eintrag.$id)
} catch (err) {
if (err.status === 404) return false
throw err
}
if (frisch.status !== 'wartet') return false
try {
await deleteDocument(col(), frisch.$id)
} catch (err) {
if (err.status === 404) return false
throw err
}
unverbucht.delete(frisch.$id)
return true
})
if (weg) geloescht += 1
}
return geloescht
}
/**
* Taeglicher Job, vor raeumeAuf: Eintraege, die laenger als 48 Stunden (ab createdAt) in `wartet` stehen, werden zu
* `fehler` (das Portal zeigt dann den Zustellhinweis) und bekommen als Loeschtermin jetzt + 48 Stunden, damit raeumeAuf