I-6: Neue Collection shopRechtstexteArchiv (ID r + 35 Hex aus sha256(shopId + hash)); das Bestaetigen legt die Fassung samt Versand-/Zahlungstext und Gewaehrleistungstext ab, G09 zeigt die Fassung zum rechtstexteHash der Bestellung (Rueckfall mit Warnung auf die aktuell bestaetigte), loescheShopDaten loescht das Archiv mit. I-7: Grenzen AGB 20000, Belehrung 12000, Formular 3000 plus Pruefung der maskierten Laenge beim Speichern; G09 ersetzt die AGB nie mehr durch einen Link, sondern scheitert (Marker bleibt offen, wird wiederholt). Test mit Texten in Hoechstlaenge gegen die Grenzen der Warteschlange. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.7 KiB
JavaScript
117 lines
4.7 KiB
JavaScript
/**
|
|
* Webshop: Appwrite-Schema der fuenf 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.<Name>; 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')],
|
|
},
|
|
|
|
// Archiv der bestaetigten Rechtstexte (eine Fassung je Dokument, ID 'r' + 35 Hex-Zeichen aus sha256(shopId + hash)): G09 zeigt dem
|
|
// Kaeufer die Fassung zum Zeitpunkt seiner Bestellung, und der Haendler kann belegen, welche Belehrung galt. Keine personenbezogenen
|
|
// Daten ausser der Person, die bestaetigt hat; wird mit dem Shop geloescht (loeschen.js).
|
|
shopRechtstexteArchiv: {
|
|
attributes: [str('shopId', 36), str('customerId', 36), str('hash', 64), str('texte', 100000), dt('bestaetigtAt'), str('bestaetigtVon', 255)],
|
|
indexes: [index('idx_shop_hash', 'shopId', 'hash')],
|
|
},
|
|
}
|
|
|
|
/** 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),
|
|
]),
|
|
)
|