/** * 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.; 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), // TP2: Bestell-Fragebogen als JSON-Text. Der Inhalt ist auf 16000 Zeichen begrenzt (FRAGEBOGEN_MAX in // termine/fragebogen.js); die groessere Spalte wird in Appwrite eine Textspalte und zaehlt nicht zur Zeilenbreite. str('fragebogen', 20000), ], 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), // optional: Verweis auf einen Anhang (Buchungskalender: Rechnungs-PDF, Task 20); wird nur gespeichert, das Senden bleibt unveraendert str('anhang', 500), str('status', 16), int('versuche'), dt('naechsterVersuch'), dt('gesendetAt'), str('fehler', 300), dt('createdAt'), ], // idx_status_next: Minutenjob und Aufraeumen; idx_booking: zustellFehler und verwerfeWartende fragen nach der Buchung indexes: [index('idx_status_next', 'status', 'naechsterVersuch'), index('idx_booking', 'bookingId')], }, } /** 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), ]), )