Terminplaner: Kunden-Schnittstelle

/api/termine: Einstellungen (Vorlage, PATCH mit Version, Feed-Link), Buchungen
(Liste, Zeiten, von Hand, Sperre, Aktionen), CSV-Export, Protokoll der
Kundenansicht. Pflichtangaben fuer Gaeste (Telefon immer, Anschrift bei
Widerrufsbelehrung) in normalisiereEinstellung, Entwurf aus Vorlagen,
kontextPerSiteKey ohne Pflichtangaben nicht buchbar; speichereEinstellung
nimmt eine Funktion unter der Sperre.

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:43 +00:00
1 parent 9e8fd33713
commit e3e198fb1a
10 files changed
+1766 -40

No files matched your search

+20 -8
View File
@@ -3,6 +3,7 @@ import { createDocument, getDocument, updateDocument } from '../appwriteAdmin.js
import { analyseHosts } from '../analyse.js'
import { withLocks } from '../keyedLock.js'
import { findActiveBySiteKey } from '../siteFeatures.js'
import { fehlendePflichtangaben } from './einstellungen.js'
import { termineAus } from './schalter.js'
import { pruefeNow, pruefeText } from './util.js'
@@ -18,7 +19,8 @@ import { pruefeNow, pruefeText } from './util.js'
* Ein ctx ist nur zum Lesen: kontextPerSiteKey gibt innerhalb von 30 Sekunden dasselbe Objekt zurueck.
*
* - kontextPerSiteKey (oeffentliche Schnittstelle): nur Funktionen im Zustand active und nur mit gespeicherter
* Einstellung; Modus off => null ohne jede Abfrage. Treffer und Fehlschlaege werden 30 Sekunden je siteKey im Speicher
* Einstellung, in der die Pflichtangaben fuer Gaeste stehen (fehlendePflichtangaben, Ruling R41: ein Entwurf aus einer
* Vorlage ist noch nicht buchbar); Modus off => null ohne jede Abfrage. Treffer und Fehlschlaege werden 30 Sekunden je siteKey im Speicher
* gehalten (Zustandswechsel der Funktion wirken so mit hoechstens 30 Sekunden Verzoegerung); speichereEinstellung
* leert den Speicher. Eine Abfrage, die waehrend eines Speicherns lief, landet nie im Speicher (generation).
* - kontextPerFeature (Portal, Gaesteseiten per Token): active und disabled (Gaeste duerfen weiter absagen und widerrufen),
@@ -98,7 +100,7 @@ export async function kontextPerSiteKey(siteKey, now = new Date()) {
const stand = generation
const feature = await findActiveBySiteKey(siteKey, FUNKTION)
let ctx = feature ? await baueKontext(feature) : null
if (ctx && !ctx.einstellung) ctx = null
if (ctx && (!ctx.einstellung || fehlendePflichtangaben(ctx.einstellung).length > 0)) ctx = null
if (stand === generation) merke(siteKey, ctx, nowMs)
return ctx
}
@@ -135,22 +137,32 @@ const veraltet = () =>
* Aenderung beruht (0 = noch nie gespeichert); weicht sie von der gespeicherten ab, passiert nichts und es folgt
* { status: 409, code: 'EINSTELLUNG_VERALTET' }. Lesen, Pruefen und Schreiben laufen unter der Sperre
* termine-cfg:<featureId>. Danach ist der Speicher von kontextPerSiteKey leer.
* Statt der Einstellung darf eine Funktion `async (bisher) => einstellung` uebergeben werden (Ruling R40): sie laeuft
* INNERHALB der Sperre nach der Versionspruefung, bekommt die gespeicherte Einstellung (Objekt) oder null und liefert die
* zu speichernde; sie darf werfen (der Fehler geht unveraendert durch, es wird nichts geschrieben). So laufen Pruefungen,
* die den gespeicherten Stand und die Buchungen brauchen (Entfernen mit kuenftigen Terminen), unter derselben Sperre wie
* das Schreiben. version null ueberspringt die Versionspruefung (nur fuer Aenderungen, die auf dem aktuellen Stand
* aufbauen, z. B. neuer Kalender-Link); die Version zaehlt trotzdem hoch.
* @param {{ $id: string, customerId: string, projectId: string }} feature portalFeatures-Dokument
* @param {object} einstellung
* @param {{ version: number, by?: string, now?: Date }} optionen
* @param {object|((bisher: object|null) => object|Promise<object>)} einstellungOderFn
* @param {{ version: number|null, by?: string, now?: Date }} optionen
* @returns {Promise<{ einstellung: object, version: number }>} wie gespeichert, mit der neuen Version
*/
export async function speichereEinstellung(feature, einstellung, { version, by = '', now = new Date() } = {}) {
export async function speichereEinstellung(feature, einstellungOderFn, { version, by = '', now = new Date() } = {}) {
pruefeText(feature?.$id, 'feature.$id')
if (!istObjekt(einstellung)) throw new TypeError('einstellung fehlt')
const alsFunktion = typeof einstellungOderFn === 'function'
if (!alsFunktion && !istObjekt(einstellungOderFn)) throw new TypeError('einstellung fehlt')
const nowMs = pruefeNow(now, 'speichereEinstellung')
const json = JSON.stringify(einstellung)
return withLocks([`termine-cfg:${feature.$id}`], async () => {
const col = config.collections.termineConfig
const vorhanden = await holeOderNull(col, feature.$id)
const aktuell = vorhanden && Number.isInteger(vorhanden.version) ? vorhanden.version : 0
if (version !== aktuell) throw veraltet()
if (version !== null && version !== aktuell) throw veraltet()
const einstellung = alsFunktion ? await einstellungOderFn(vorhanden ? (liesEinstellung(vorhanden)?.einstellung ?? null) : null) : einstellungOderFn
if (!istObjekt(einstellung)) throw new TypeError('einstellung fehlt')
const json = JSON.stringify(einstellung)
const felder = {
customerId: feature.customerId,