Files
Webklar-Kundenbereich/server/services/termine/schema.js
T
ClaudeandClaude Sonnet 5.5 5c95dfc322 Terminplaner: Aufbewahrungs-Loeschung seitenweise (Grundlagen, verfallen/abgesagt/abgelehnt)
loeschKandidaten liest eine Seite (Standard 100, aelteste Bezugszeit zuerst, optional je Funktion)
ohne Gastfelder; die Frist steht in der Abfrage. arbeiteSeiten (Seite lesen, bearbeiten, neu lesen)
endet bei leerer Seite, am Deckel (config.termine.loeschDeckel, 2000 je Status und Lauf, Rest an den
Folgetagen, gezaehlt als gedeckelt) oder bei einer Seite ohne Fortschritt. verfallen, abgesagt und
abgelehnt laufen darueber, unter der Buchungssperre werden Status und Bezugszeit frisch geprueft.
taeglicherJob liefert gedeckelt getrennt vom Loeschzaehler. Neue Indizes idx_status_ends und
idx_status_updated auf termineBookings (scripts/termine-setup.mjs legt sie an).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 08:55:32 +00:00

128 lines
4.5 KiB
JavaScript

/**
* Terminplaner: Appwrite-Schema der vier Collections (Spezifikation 2026-10-02-terminplaner-design.md, Abschnitt 2).
* Reine Daten, absichtlich ohne Import von server/config.js: test/support/schema.js liest sie vor useTestEnv(),
* ein frueh geladenes config.js wuerde den Endpunkt des Appwrite-Nachbaus verfehlen. Die Collection-IDs stehen in
* config.collections.<Name>; scripts/termine-setup.mjs legt alles an.
*
* Attribut: { key, type: 'string' | 'integer' | 'boolean' | 'datetime', size (nur Text), required: false }
* Index: { key, type: 'key', attributes: [...] }
* Datumswerte sind UTC (toISOString()); Appwrite liefert sie als "+00:00" zurueck, daher immer mit new Date() lesen.
*/
const str = (key, size) => ({ key, type: 'string', size, required: false })
const int = (key) => ({ key, type: 'integer', required: false })
const bool = (key) => ({ key, type: 'boolean', required: false })
const dt = (key) => ({ key, type: 'datetime', required: false })
const index = (key, ...attributes) => ({ key, type: 'key', attributes })
export const TERMINE_COLLECTIONS = {
// Eine Einstellung je Funktion; Dokument-ID = ID des portalFeatures-Dokuments. Das Feld heisst einstellung,
// nicht data: appwriteAdmin.formatRequestBody haelt Bodies mit data fuer fertig verpackt.
termineConfig: {
attributes: [
str('customerId', 64),
str('projectId', 64),
str('einstellung', 200000),
int('version'),
dt('updatedAt'),
str('updatedBy', 100),
],
indexes: [],
},
// Eine Buchung je Dokument (ID randomUUID)
termineBookings: {
attributes: [
str('featureId', 64),
str('customerId', 64),
str('projectId', 64),
str('leistungId', 64),
str('leistungName', 120),
str('ressourceId', 64),
str('ressourceName', 80),
dt('startsAt'),
dt('endsAt'),
dt('blockEndsAt'),
str('status', 16),
str('quelle', 16),
str('gastName', 100),
str('gastEmail', 254),
str('gastTelefon', 40),
str('gastNachricht', 500),
str('gastZusatz', 2000),
bool('erinnerung'),
dt('erinnerungGesendetAt'),
dt('heldUntil'),
str('verifyHash', 64),
str('manageHash', 64),
dt('bestaetigtAt'),
dt('angenommenAt'),
dt('abgesagtAt'),
str('abgesagtVon', 16),
str('absageGrund', 300),
dt('widerrufenAt'),
str('nachweis', 4000),
int('preisCent'),
str('preisArt', 8),
str('buchungsart', 8),
bool('verbraucher'),
str('zahlart', 8),
str('zahlStatus', 16),
str('stripeSessionId', 128),
str('stripePaymentIntent', 64),
int('icsSequenz'),
dt('createdAt'),
dt('updatedAt'),
],
indexes: [
index('idx_feature_start', 'featureId', 'startsAt'),
index('idx_status_held', 'status', 'heldUntil'),
index('idx_feature_email', 'featureId', 'gastEmail'),
index('idx_verify', 'verifyHash'),
index('idx_manage', 'manageHash'),
index('idx_status_start', 'status', 'startsAt'),
// Aufbewahrungs-Loeschung (Tagesjob): Status + Bezugszeit, verfallen laeuft ueber idx_status_held
index('idx_status_ends', 'status', 'endsAt'),
index('idx_status_updated', 'status', 'updatedAt'),
],
},
// Belegungszellen gegen Doppelbuchung; Dokument-ID c_ + 34 Hex von sha1(featureId|ressourceId|zelleIso)
termineClaims: {
attributes: [str('featureId', 64), str('ressourceId', 64), str('bookingId', 64), dt('zelle'), dt('bis')],
indexes: [index('idx_booking', 'bookingId'), index('idx_bis', 'bis')],
},
// Mail-Warteschlange (einziger Sendepfad); enthaelt personenbezogene Daten, daher kurze Aufbewahrung
termineOutbox: {
attributes: [
str('featureId', 64),
str('bookingId', 64),
str('art', 16),
str('an', 254),
str('fromName', 120),
str('replyTo', 254),
str('subject', 300),
str('html', 100000),
str('text', 50000),
str('ics', 20000),
str('icsMethod', 8),
str('status', 16),
int('versuche'),
dt('naechsterVersuch'),
dt('gesendetAt'),
str('fehler', 300),
dt('createdAt'),
],
indexes: [index('idx_status_next', 'status', 'naechsterVersuch')],
},
}
/** Datumsfelder je Collection, damit der Appwrite-Nachbau der Tests sie wie Appwrite formatieren kann. */
export const TERMINE_DATETIMES = Object.fromEntries(
Object.entries(TERMINE_COLLECTIONS).map(([name, { attributes }]) => [
name,
attributes.filter((attribute) => attribute.type === 'datetime').map((attribute) => attribute.key),
]),
)