feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
@@ -0,0 +1,633 @@
|
||||
// Wabenhain — Datenmodell
|
||||
// Alle Geldbetraege werden als Cent-Integer gespeichert, um Rundungsfehler zu vermeiden.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
// "provider" wird NICHT hier per Hand gepflegt: Prisma erlaubt kein env() an
|
||||
// dieser Stelle, daher schreibt scripts/prisma-provider.mjs diese Zeile vor
|
||||
// jedem generate/db push/migrate anhand von DATABASE_PROVIDER ("postgresql"
|
||||
// [Standard] oder "mysql") neu. Siehe README, Abschnitt "Datenbank-Provider
|
||||
// wechseln".
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kunden
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum CustomerRole {
|
||||
KUNDE
|
||||
ADMIN
|
||||
}
|
||||
|
||||
model Customer {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
passwordHash String
|
||||
firstName String
|
||||
lastName String
|
||||
phone String?
|
||||
role CustomerRole @default(KUNDE)
|
||||
|
||||
/// Wird bei sicherheitsrelevanten Ereignissen (Passwort-Reset, "alle
|
||||
/// Geraete abmelden", 2FA an/aus) hochgezaehlt. Im JWT eingebettet — bei
|
||||
/// Abweichung von diesem Wert gilt das Token als widerrufen, obwohl es
|
||||
/// technisch noch nicht abgelaufen ist.
|
||||
sessionVersion Int @default(0)
|
||||
/// Fuer die Konto-Sperre nach wiederholten Fehlversuchen (zusaetzlich zum
|
||||
/// IP-basierten Rate-Limit — persistent und ueberlebt einen Neustart/
|
||||
/// mehrere Server-Instanzen, anders als der In-Memory-Rate-Limiter).
|
||||
failedLoginAttempts Int @default(0)
|
||||
lockedUntil DateTime?
|
||||
|
||||
/// Base32-TOTP-Secret, erst gesetzt wenn 2FA aktiv bestaetigt wurde.
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
/// Zeitschritt (Unix-Zeit / 30s) des zuletzt beim Login akzeptierten
|
||||
/// TOTP-Codes — verhindert, dass ein abgefangener/mitgelesener Code im
|
||||
/// selben oder einem frueheren Zeitfenster ein zweites Mal funktioniert.
|
||||
lastTotpStep Int?
|
||||
|
||||
/// Von einem Admin gesperrtes Konto (Benutzerverwaltung) — unabhaengig von
|
||||
/// der automatischen Fehlversuchs-Sperre in lockedUntil. Verhindert den
|
||||
/// Login bis ein Admin das Konto wieder entsperrt.
|
||||
blockedAt DateTime?
|
||||
|
||||
/// Gesetzt, sobald die E-Mail-Adresse per Bestaetigungslink oder durch
|
||||
/// erfolgreiches Setzen eines Passworts ueber einen zugesandten Reset-Link
|
||||
/// nachweislich unter der Kontrolle des Kontoinhabers ist. Registrierte
|
||||
/// Kund:innen benoetigen dies fuer den Checkout (siehe /api/checkout).
|
||||
emailVerifiedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
orders Order[]
|
||||
addresses Address[]
|
||||
newsletterSubscriber NewsletterSubscriber?
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
emailVerificationTokens EmailVerificationToken[]
|
||||
twoFactorBackupCodes TwoFactorBackupCode[]
|
||||
reviews Review[]
|
||||
wishlistItems WishlistItem[]
|
||||
auditLogs AuditLog[]
|
||||
|
||||
@@map("customers")
|
||||
}
|
||||
|
||||
model EmailVerificationToken {
|
||||
id String @id @default(cuid())
|
||||
customerId String
|
||||
/// SHA-256-Hash des per E-Mail zugesandten Tokens — wie beim
|
||||
/// Passwort-Reset wird nie der Klartext gespeichert.
|
||||
tokenHash String @unique
|
||||
expiresAt DateTime
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("email_verification_tokens")
|
||||
}
|
||||
|
||||
model TwoFactorBackupCode {
|
||||
id String @id @default(cuid())
|
||||
customerId String
|
||||
/// SHA-256-Hash des Codes — der Klartext wird dem Kunden nur einmalig bei
|
||||
/// der Einrichtung angezeigt und nie wieder gespeichert.
|
||||
codeHash String @unique
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("two_factor_backup_codes")
|
||||
}
|
||||
|
||||
model Address {
|
||||
id String @id @default(cuid())
|
||||
customerId String
|
||||
label String @default("Zuhause")
|
||||
firstName String
|
||||
lastName String
|
||||
street String
|
||||
zip String
|
||||
city String
|
||||
country String @default("DE")
|
||||
isDefault Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("addresses")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ernten, Chargen, Lagerbestand, Produkte
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Harvest {
|
||||
id String @id @default(cuid())
|
||||
year Int
|
||||
/// z.B. "Fruehtracht", "Sommertracht"
|
||||
type String
|
||||
slug String @unique
|
||||
harvestDate DateTime
|
||||
location String @default("Altenmuenster")
|
||||
description String
|
||||
/// Liste von Bild-URLs als JSON-Array (String[] ist nicht Provider-uebergreifend:
|
||||
/// MySQL kennt keine nativen Skalar-Arrays) — siehe lib/json-array.ts fuer den
|
||||
/// Normalisierungs-Helfer beim Lesen.
|
||||
images Json @default("[]")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
batches Batch[]
|
||||
products Product[]
|
||||
|
||||
@@unique([year, type])
|
||||
@@map("harvests")
|
||||
}
|
||||
|
||||
model Batch {
|
||||
id String @id @default(cuid())
|
||||
/// z.B. FH-26-01
|
||||
code String @unique
|
||||
harvestId String
|
||||
fillDate DateTime
|
||||
bestBeforeDate DateTime
|
||||
glassCount Int
|
||||
remainingGlasses Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
harvest Harvest @relation(fields: [harvestId], references: [id], onDelete: Restrict)
|
||||
products Product[]
|
||||
orderItems OrderItem[]
|
||||
|
||||
@@map("batches")
|
||||
}
|
||||
|
||||
enum ProductCategory {
|
||||
HONIG
|
||||
KERZEN
|
||||
PROPOLIS
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
name String
|
||||
category ProductCategory @default(HONIG)
|
||||
/// Nur fuer Honig-Produkte gesetzt (Kerzen/Propolis sind keiner Ernte/Charge zugeordnet)
|
||||
harvestId String?
|
||||
batchId String?
|
||||
description String
|
||||
/// Geschmacksprofil-Tags als JSON-Array, z.B. ["mild", "floral", "feincremig"]
|
||||
/// — String[] ist nicht Provider-uebergreifend (MySQL kennt keine nativen
|
||||
/// Skalar-Arrays), siehe lib/json-array.ts fuer den Normalisierungs-Helfer.
|
||||
tasteTags Json @default("[]")
|
||||
/// Intensitaet 1-5 fuer die visuelle Geschmacksskala
|
||||
intensityMild Int @default(3)
|
||||
intensityFloral Int @default(3)
|
||||
intensityKraeftig Int @default(3)
|
||||
consistency String @default("feincremig")
|
||||
weightGrams Int
|
||||
priceCents Int
|
||||
pricePerKgCents Int
|
||||
currency String @default("EUR")
|
||||
/// Liste von Bild-URLs als JSON-Array, siehe tasteTags-Kommentar oben.
|
||||
images Json @default("[]")
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
harvest Harvest? @relation(fields: [harvestId], references: [id], onDelete: Restrict)
|
||||
batch Batch? @relation(fields: [batchId], references: [id], onDelete: Restrict)
|
||||
inventory Inventory?
|
||||
orderItems OrderItem[]
|
||||
reviews Review[]
|
||||
wishlistItems WishlistItem[]
|
||||
stockNotifications StockNotification[]
|
||||
|
||||
@@map("products")
|
||||
}
|
||||
|
||||
model StockNotification {
|
||||
id String @id @default(cuid())
|
||||
productId String
|
||||
email String
|
||||
/// Spracheinstellung bei der Anmeldung — die Benachrichtigungs-E-Mail wird
|
||||
/// spaeter asynchron (beim naechsten Bestandsupdate) versendet, ohne
|
||||
/// Cookie-Kontext der urspruenglichen Anfrage.
|
||||
locale String @default("de")
|
||||
createdAt DateTime @default(now())
|
||||
/// Gesetzt, sobald die Benachrichtigungs-E-Mail versendet wurde — verhindert
|
||||
/// Mehrfachversand, falls der Bestand mehrfach zwischen 0 und >0 wechselt,
|
||||
/// ohne dass sich die Kundin/der Kunde neu angemeldet hat.
|
||||
notifiedAt DateTime?
|
||||
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Eine Anmeldung pro E-Mail-Adresse und Produkt.
|
||||
@@unique([productId, email])
|
||||
@@map("stock_notifications")
|
||||
}
|
||||
|
||||
model WishlistItem {
|
||||
id String @id @default(cuid())
|
||||
customerId String
|
||||
productId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Ein Produkt kann pro Kunde nur einmal auf der Wunschliste stehen.
|
||||
@@unique([customerId, productId])
|
||||
@@map("wishlist_items")
|
||||
}
|
||||
|
||||
model Review {
|
||||
id String @id @default(cuid())
|
||||
productId String
|
||||
customerId String
|
||||
/// 1-5
|
||||
rating Int
|
||||
comment String?
|
||||
/// Nur freigeschaltete Bewertungen werden auf der Produktseite angezeigt —
|
||||
/// einfache Missbrauchskontrolle durch den Admin vor Veroeffentlichung.
|
||||
isApproved Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Ein Kunde kann ein Produkt nur einmal bewerten.
|
||||
@@unique([productId, customerId])
|
||||
@@map("reviews")
|
||||
}
|
||||
|
||||
model Inventory {
|
||||
id String @id @default(cuid())
|
||||
productId String @unique
|
||||
quantity Int @default(0)
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("inventory")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bestellungen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum OrderStatus {
|
||||
NEU
|
||||
BEZAHLT
|
||||
IN_VORBEREITUNG
|
||||
VERSANDBEREIT
|
||||
VERSENDET
|
||||
ABHOLBEREIT
|
||||
ABGEHOLT
|
||||
STORNIERT
|
||||
}
|
||||
|
||||
enum FulfillmentMethod {
|
||||
VERSAND
|
||||
ABHOLUNG
|
||||
}
|
||||
|
||||
model Order {
|
||||
id String @id @default(cuid())
|
||||
orderNumber String @unique
|
||||
customerId String?
|
||||
status OrderStatus @default(NEU)
|
||||
fulfillmentMethod FulfillmentMethod @default(VERSAND)
|
||||
/// Spracheinstellung beim Checkout ("de"/"en") — wird fuer spaeter
|
||||
/// asynchron versendete E-Mails (Stripe-Webhook) benoetigt, da dort kein
|
||||
/// Cookie-/Request-Kontext mehr vorhanden ist, um die Sprache erneut zu bestimmen.
|
||||
locale String @default("de")
|
||||
|
||||
// Kontakt- und Versanddaten werden am Bestellzeitpunkt gespeichert,
|
||||
// damit Gastbestellungen unabhaengig von einem Customer-Datensatz funktionieren.
|
||||
email String
|
||||
firstName String
|
||||
lastName String
|
||||
phone String?
|
||||
street String
|
||||
zip String
|
||||
city String
|
||||
country String @default("DE")
|
||||
|
||||
subtotalCents Int
|
||||
shippingCents Int @default(0)
|
||||
discountCents Int @default(0)
|
||||
totalCents Int
|
||||
currency String @default("EUR")
|
||||
|
||||
discountCodeId String?
|
||||
|
||||
stripeCheckoutSessionId String?
|
||||
notes String?
|
||||
/// Optionale persoenliche Nachricht bei Geschenkbestellungen — wird beim
|
||||
/// Verpacken von Hand beigelegt (siehe Admin-Bestelldetail).
|
||||
giftMessage String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
discountCode DiscountCode? @relation(fields: [discountCodeId], references: [id], onDelete: SetNull)
|
||||
items OrderItem[]
|
||||
payment Payment?
|
||||
shipment Shipment?
|
||||
pickup Pickup?
|
||||
invoice Invoice?
|
||||
returnRequest ReturnRequest?
|
||||
|
||||
@@map("orders")
|
||||
}
|
||||
|
||||
enum ReturnStatus {
|
||||
ANGEFRAGT
|
||||
ANGENOMMEN
|
||||
ABGELEHNT
|
||||
ERSTATTET
|
||||
}
|
||||
|
||||
/// Eine Rueckgabeanfrage pro Bestellung (orderId @unique) — deckt den Fall
|
||||
/// "gesamte Bestellung zurueckschicken" ab, keine positionsweise Teil-Retoure.
|
||||
model ReturnRequest {
|
||||
id String @id @default(cuid())
|
||||
orderId String @unique
|
||||
reason String
|
||||
status ReturnStatus @default(ANGEFRAGT)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("return_requests")
|
||||
}
|
||||
|
||||
model OrderItem {
|
||||
id String @id @default(cuid())
|
||||
orderId String
|
||||
productId String
|
||||
/// Nur gesetzt, wenn das Produkt einer Charge zugeordnet ist (Honig)
|
||||
batchId String?
|
||||
|
||||
// Snapshot der Produktdaten zum Bestellzeitpunkt (Preisaenderungen duerfen
|
||||
// bestehende Bestellungen nicht rueckwirkend veraendern).
|
||||
productName String
|
||||
unitPriceCents Int
|
||||
quantity Int
|
||||
weightGrams Int
|
||||
totalCents Int
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
|
||||
batch Batch? @relation(fields: [batchId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@map("order_items")
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
PENDING
|
||||
SUCCEEDED
|
||||
FAILED
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
orderId String @unique
|
||||
provider String @default("stripe")
|
||||
stripePaymentIntentId String?
|
||||
status PaymentStatus @default(PENDING)
|
||||
amountCents Int
|
||||
currency String @default("EUR")
|
||||
paidAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("payments")
|
||||
}
|
||||
|
||||
model Shipment {
|
||||
id String @id @default(cuid())
|
||||
orderId String @unique
|
||||
carrier String?
|
||||
trackingNumber String?
|
||||
shippedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("shipments")
|
||||
}
|
||||
|
||||
model Pickup {
|
||||
id String @id @default(cuid())
|
||||
orderId String @unique
|
||||
location String @default("Altenmuenster")
|
||||
readyAt DateTime?
|
||||
pickedUpAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("pickups")
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
id String @id @default(cuid())
|
||||
orderId String @unique
|
||||
invoiceNumber String @unique
|
||||
issuedAt DateTime @default(now())
|
||||
totalCents Int
|
||||
pdfUrl String?
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("invoices")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Marketing / Kontakt
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model NewsletterSubscriber {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
customerId String? @unique
|
||||
confirmed Boolean @default(false)
|
||||
/// Dient sowohl der Bestaetigung (Double-Opt-In) als auch als dauerhafter
|
||||
/// Abmelde-Link fuer Abonnenten ohne Kundenkonto.
|
||||
confirmationToken String? @unique
|
||||
subscribedAt DateTime @default(now())
|
||||
unsubscribedAt DateTime?
|
||||
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("newsletter_subscribers")
|
||||
}
|
||||
|
||||
model PasswordResetToken {
|
||||
id String @id @default(cuid())
|
||||
customerId String
|
||||
/// SHA-256-Hash des dem Kunden per E-Mail zugesandten Tokens — wie bei
|
||||
/// Passwoertern wird nie der Klartext gespeichert.
|
||||
tokenHash String @unique
|
||||
expiresAt DateTime
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("password_reset_tokens")
|
||||
}
|
||||
|
||||
enum BusinessPackage {
|
||||
KLASSIK
|
||||
GESCHENK
|
||||
INDIVIDUELL
|
||||
}
|
||||
|
||||
enum InquiryStatus {
|
||||
NEU
|
||||
IN_BEARBEITUNG
|
||||
ABGESCHLOSSEN
|
||||
ABGELEHNT
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Versand
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model ShippingTier {
|
||||
id String @id @default(cuid())
|
||||
/// Menge (Stueck ueber alle Artikel im Warenkorb zusammengezaehlt) ab der
|
||||
/// diese Staffel gilt.
|
||||
minQuantity Int
|
||||
/// Bis einschliesslich dieser Menge — null bedeutet "und mehr" (offene,
|
||||
/// oberste Staffel).
|
||||
maxQuantity Int?
|
||||
priceCents Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("shipping_tiers")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rabattcodes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum DiscountType {
|
||||
PERCENT
|
||||
FIXED
|
||||
}
|
||||
|
||||
model DiscountCode {
|
||||
id String @id @default(cuid())
|
||||
code String @unique
|
||||
type DiscountType
|
||||
/// PERCENT: Prozentpunkte (1-100). FIXED: Cent-Betrag.
|
||||
value Int
|
||||
|
||||
/// Mindestbestellwert (Cent) fuer die Anwendung, 0 = keine Mindestsumme.
|
||||
minOrderCents Int @default(0)
|
||||
/// Mindestmenge (Summe aller Artikel im Warenkorb) fuer die Anwendung —
|
||||
/// null = keine Mengenschwelle. Gesetzt macht aus dem Code einen
|
||||
/// Mengenrabatt: Checkout wendet ihn automatisch an, sobald die Schwelle
|
||||
/// erreicht ist (siehe api/checkout/route.ts), ohne dass der Code
|
||||
/// eingegeben werden muss.
|
||||
minQuantity Int?
|
||||
/// Nutzungslimit insgesamt ueber alle Kunden — null = unbegrenzt.
|
||||
maxUses Int?
|
||||
usedCount Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
validFrom DateTime?
|
||||
validUntil DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
orders Order[]
|
||||
|
||||
@@map("discount_codes")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Admin-Audit-Log
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Protokolliert sicherheitsrelevante Admin-Mutationen (wer hat wann was
|
||||
/// geaendert) — wichtig bei mehreren Admin-Konten, um Aenderungen
|
||||
/// nachvollziehen zu koennen. adminEmail ist ein Snapshot, damit der
|
||||
/// Eintrag auch nach Loeschung des Admin-Kontos lesbar bleibt.
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
adminId String?
|
||||
adminEmail String
|
||||
/// z.B. "product.update", "order.status_change", "discount_code.delete"
|
||||
action String
|
||||
entityType String
|
||||
entityId String?
|
||||
detail String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
admin Customer? @relation(fields: [adminId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("audit_logs")
|
||||
}
|
||||
|
||||
/// Admin-editierbare Website-Inhalte (Firmendaten, Homepage-Texte,
|
||||
/// Rechtstexte) als generisches Key-Value-Overlay ueber den statischen
|
||||
/// i18n-Dictionaries — siehe lib/content-registry.ts fuer die Liste aller
|
||||
/// editierbaren Schluessel und deren Fallback-Werte. Fehlt eine Zeile (kein
|
||||
/// Admin-Override gepflegt), liefert getContentValue() den urspruenglichen
|
||||
/// Dictionary-/site-config-Text — die Seite bricht also nie, auch bevor die
|
||||
/// erste Bearbeitung stattgefunden hat.
|
||||
model SiteContent {
|
||||
/// z.B. "company.email", "home.philosophy.items", "legal.impressum"
|
||||
key String @id
|
||||
/// Bei nicht lokalisierten Feldern (Firmendaten) einziger Speicherort;
|
||||
/// bei lokalisierten Feldern (Homepage/Rechtstexte) die deutsche Fassung.
|
||||
valueDe String @db.Text
|
||||
/// Nur bei lokalisierten Feldern gesetzt — null bei Firmendaten.
|
||||
valueEn String? @db.Text
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("site_content")
|
||||
}
|
||||
|
||||
model BusinessInquiry {
|
||||
id String @id @default(cuid())
|
||||
companyName String
|
||||
contactPerson String
|
||||
email String
|
||||
phone String?
|
||||
desiredQuantity Int
|
||||
desiredDeliveryDate DateTime?
|
||||
package BusinessPackage @default(KLASSIK)
|
||||
message String?
|
||||
status InquiryStatus @default(NEU)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("business_inquiries")
|
||||
}
|
||||
Reference in New Issue
Block a user