/** * Webshop: Appwrite-Schema der vier Collections (Spezifikation 2026-10-05-webshop-design.md). * 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/shop-setup.mjs legt alles an. Kein Feld heisst data (appwriteAdmin.formatRequestBody * haelt Bodies mit data fuer fertig verpackt), daher heissen die JSON-Felder einstellungen, rechtstexte, produkt, bestellung. * * 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 SHOP_COLLECTIONS = { // Ein Shop je Funktion; Dokument-ID = ID des portalFeatures-Dokuments. Anfrage, Stripe-Konto, Einstellungen und Rechtstexte // als JSON-Text; die grossen Spalten werden in Appwrite Textspalten und zaehlen nicht zur Zeilenbreite. shopConfig: { attributes: [ str('customerId', 36), str('projectId', 36), str('siteKey', 16), str('anfrage', 4000), str('anfrageStatus', 16), dt('anfrageAt'), dt('anfrageEntschiedenAt'), str('anfrageEntschiedenVon', 255), str('anfrageBegruendung', 1000), str('stripeAccountId', 64), str('stripeStatus', 4000), str('einstellungen', 20000), str('rechtstexte', 100000), dt('rechtstexteBestaetigtAt'), str('rechtstexteBestaetigtVon', 255), str('rechtstexteHash', 64), bool('geoeffnet'), dt('geoeffnetAt'), int('version'), ], indexes: [ index('idx_customer', 'customerId'), index('idx_sitekey', 'siteKey'), index('idx_anfrage', 'anfrageStatus'), index('idx_account', 'stripeAccountId'), ], }, // Ein Artikel je Dokument (ID randomUUID); Name und Sortierung stehen zusaetzlich als Spalten, der Rest im JSON-Text produkt shopProducts: { attributes: [str('shopId', 36), str('customerId', 36), bool('aktiv'), int('sort'), str('name', 120), str('produkt', 30000)], indexes: [index('idx_shop', 'shopId', 'sort')], }, // Eine Bestellung je Dokument; enthaelt personenbezogene Daten des Kaeufers (Aufbewahrung und Anonymisierung: Block J) shopOrders: { attributes: [ str('shopId', 36), str('customerId', 36), str('bestellnummer', 16), str('status', 16), str('tokenHash', 64), str('stripeSessionId', 128), str('paymentIntentId', 64), int('summeCent'), dt('erstelltAt'), dt('ablaufAt'), dt('bezahltAt'), str('kaeuferEmail', 320), str('bestellung', 60000), dt('anonymisiertAt'), ], indexes: [ index('idx_shop_status', 'shopId', 'status'), index('idx_nummer', 'shopId', 'bestellnummer'), index('idx_session', 'stripeSessionId'), index('idx_erstellt', 'shopId', 'erstelltAt'), ], }, // Widerruf von Vertraegen (ยง 356a BGB) ueber die oeffentliche Widerrufsseite shopWiderrufe: { attributes: [ str('shopId', 36), str('customerId', 36), str('bestellnummer', 32), str('name', 200), str('email', 320), str('nachricht', 2000), dt('eingangAt'), dt('bestaetigungAt'), dt('bearbeitetAt'), str('orderId', 36), ], indexes: [index('idx_shop', 'shopId', 'eingangAt')], }, } /** Datumsfelder je Collection, damit der Appwrite-Nachbau der Tests sie wie Appwrite formatieren kann. */ export const SHOP_DATETIMES = Object.fromEntries( Object.entries(SHOP_COLLECTIONS).map(([name, { attributes }]) => [ name, attributes.filter((attribute) => attribute.type === 'datetime').map((attribute) => attribute.key), ]), )