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.
634 lines
20 KiB
Plaintext
634 lines
20 KiB
Plaintext
// 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")
|
|
}
|