config.shop (SHOP_ENABLED, SHOP_PILOT_EMAILS, SHOP_UPLOAD_DIR, STRIPE_CONNECT_WEBHOOK_SECRET), config.shopSetup (299 € einmalig), Collection-IDs shopConfig/shopProducts/shopOrders/shopWiderrufe, services/shop/schalter.js, Katalogschluessel shopSetup in stripeBilling und stripe-products.mjs, services/shop/schema.js, scripts/shop-setup.mjs (--check), schemaKeys fuer die Test-Mocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.2 KiB
JavaScript
109 lines
4.2 KiB
JavaScript
/**
|
|
* 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.<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')],
|
|
},
|
|
}
|
|
|
|
/** 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),
|
|
]),
|
|
)
|