This commit is contained in:
commit
59bb5a38a3
132 files changed
+19435
No files matched your search
@@ -0,0 +1,529 @@
|
||||
// ARTISTA MODELCAMP 2027 — Datenmodell
|
||||
//
|
||||
// Geld: ausschliesslich ganzzahlige Cent-Betraege (Int), Waehrung immer EUR.
|
||||
// Zeit: Zeitstempel in UTC; fachliche Grenzen (Preisphasen, Faelligkeiten)
|
||||
// werden in src/lib/time.ts gegen Europe/Berlin berechnet.
|
||||
//
|
||||
// Test- und Livebuchungen liegen in denselben Tabellen, sind aber ueber
|
||||
// "livemode" strikt getrennt: Kapazitaet und Gutscheinkontingente zaehlen nur
|
||||
// Buchungen desselben Modus, damit Stripe-Testbuchungen nie echte Plaetze
|
||||
// blockieren.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Camp und Verkaufsfreigaben
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum PricePhase {
|
||||
PREVIEW
|
||||
EARLY_BIRD
|
||||
REGULAR
|
||||
}
|
||||
|
||||
/// Genau eine Zeile (id = "modelcamp-2027"). Enthaelt die betrieblichen
|
||||
/// Angaben, die den Live-Verkauf freischalten. Die Preise selbst sind
|
||||
/// verbindlich vorgegeben und stehen in src/lib/pricing.ts, nicht hier.
|
||||
model Camp {
|
||||
id String @id
|
||||
|
||||
/// Hauptschalter fuer den Live-Verkauf. Wirkt nur zusammen mit einer
|
||||
/// vollstaendigen Freigabe-Checkliste (src/lib/sales-gate.ts).
|
||||
salesEnabled Boolean @default(false)
|
||||
|
||||
capacity Int @default(10)
|
||||
capacityConfirmed Boolean @default(false)
|
||||
stammmodelQuota Int @default(4)
|
||||
stammmodelQuotaConfirmed Boolean @default(false)
|
||||
|
||||
campStartDate DateTime? @db.Date
|
||||
campEndDate DateTime? @db.Date
|
||||
dateConfirmed Boolean @default(false)
|
||||
|
||||
locationName String?
|
||||
locationRegion String?
|
||||
locationConfirmed Boolean @default(false)
|
||||
|
||||
/// Verbindliche Zimmer- und Bettbeschreibung, wird vor dem Kauf angezeigt
|
||||
/// und muss ausdruecklich bestaetigt werden.
|
||||
accommodationText String?
|
||||
accommodationConfirmed Boolean @default(false)
|
||||
|
||||
/// Bestaetigt, dass die in src/content/site.ts als "confirmed" markierten
|
||||
/// Leistungen verbindlich sind.
|
||||
servicesConfirmed Boolean @default(false)
|
||||
|
||||
organizerName String?
|
||||
organizerAddress String?
|
||||
organizerEmail String?
|
||||
organizerPhone String?
|
||||
organizerConfirmed Boolean @default(false)
|
||||
|
||||
/// Steuerlich gepruefter Preis-Hinweis, z. B. wie die Umsatzsteuer
|
||||
/// ausgewiesen wird. Ohne Freigabe erscheint KEINE Steueraussage.
|
||||
taxNote String?
|
||||
taxConfirmed Boolean @default(false)
|
||||
|
||||
/// Version der freigegebenen Buchungs-, Storno- und Zahlungsbedingungen.
|
||||
termsVersion String?
|
||||
termsUrl String?
|
||||
termsApproved Boolean @default(false)
|
||||
|
||||
travelLawChecked Boolean @default(false)
|
||||
|
||||
installmentsApproved Boolean @default(false)
|
||||
installmentRuleApproved Boolean @default(false)
|
||||
|
||||
/// Reservierungsdauer fuer einen begonnenen Checkout. Stripe verlangt fuer
|
||||
/// expires_at mindestens 30 Minuten.
|
||||
reservationMinutes Int @default(30)
|
||||
finalInstallmentLeadDays Int @default(30)
|
||||
minInstallmentGapDays Int @default(14)
|
||||
|
||||
/// Nur im Stripe-Testmodus wirksam: simuliert eine Preisphase, damit
|
||||
/// Testbuchungen auch vor dem 01.11.2026 moeglich sind.
|
||||
testPhaseOverride PricePhase?
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy String?
|
||||
|
||||
bookings Booking[]
|
||||
reservations SeatReservation[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Buchung
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum BookingStatus {
|
||||
/// Platz reserviert, Zahlung der ersten Rate steht aus.
|
||||
PENDING
|
||||
CONFIRMED
|
||||
CANCELED
|
||||
/// Checkout abgebrochen oder abgelaufen, Platz wieder frei.
|
||||
EXPIRED
|
||||
/// Zahlung eingegangen, aber ein Konflikt (z. B. Platz inzwischen
|
||||
/// vergeben) verlangt eine manuelle Entscheidung.
|
||||
NEEDS_REVIEW
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
UNPAID
|
||||
/// Verzoegerte Zahlungsart (z. B. SEPA) ist unterwegs.
|
||||
PROCESSING
|
||||
PARTIALLY_PAID
|
||||
PAID
|
||||
FAILED
|
||||
PARTIALLY_REFUNDED
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
enum PlanType {
|
||||
FULL
|
||||
TWO_INSTALLMENTS
|
||||
THREE_INSTALLMENTS
|
||||
}
|
||||
|
||||
model Booking {
|
||||
id String @id @default(cuid())
|
||||
/// Oeffentliche Buchungsnummer, z. B. AMC27-7K3Q9P.
|
||||
reference String @unique
|
||||
|
||||
campId String
|
||||
camp Camp @relation(fields: [campId], references: [id])
|
||||
|
||||
status BookingStatus @default(PENDING)
|
||||
paymentStatus PaymentStatus @default(UNPAID)
|
||||
livemode Boolean
|
||||
|
||||
/// Vom Browser erzeugter Schluessel je Buchungsversuch: ein doppelt
|
||||
/// abgeschicktes Formular fuehrt zur selben Buchung statt zu zweien.
|
||||
idempotencyKey String @unique
|
||||
/// SHA-256 des Zugangstokens fuer die Statusseite. Das Token selbst wird
|
||||
/// nie gespeichert.
|
||||
statusTokenHash String @unique
|
||||
|
||||
firstName String
|
||||
lastName String
|
||||
email String
|
||||
phone String
|
||||
country String
|
||||
notes String?
|
||||
|
||||
adultConfirmedAt DateTime
|
||||
termsAcceptedAt DateTime
|
||||
termsVersion String
|
||||
accommodationAcknowledgedAt DateTime
|
||||
privacyAcknowledgedAt DateTime
|
||||
/// Zustimmung zum Einzug spaeterer Raten (nur bei Ratenzahlung).
|
||||
installmentMandateAt DateTime?
|
||||
|
||||
// --- Preis-Snapshot (per Datenbank-Trigger unveraenderlich) -------------
|
||||
pricePhase PricePhase
|
||||
basePriceCents Int
|
||||
phaseDiscountCents Int
|
||||
voucherDiscountCents Int
|
||||
totalCents Int
|
||||
planType PlanType
|
||||
priceSnapshot Json
|
||||
/// Bis hierhin gilt der gebundene Preis (= Ablauf der Stripe-Session).
|
||||
priceLockedUntil DateTime
|
||||
|
||||
voucherId String?
|
||||
voucher Voucher? @relation(fields: [voucherId], references: [id])
|
||||
|
||||
stripeCustomerId String?
|
||||
stripeCheckoutSessionId String? @unique
|
||||
stripeCheckoutUrl String?
|
||||
/// Referenz (keine Kartendaten) auf die fuer Folgeraten gespeicherte
|
||||
/// Zahlungsmethode. Wird nach der letzten Rate bei Stripe getrennt.
|
||||
stripePaymentMethodId String?
|
||||
paymentMethodDetachedAt DateTime?
|
||||
|
||||
paidCents Int @default(0)
|
||||
refundedCents Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
confirmedAt DateTime?
|
||||
canceledAt DateTime?
|
||||
cancelReason String?
|
||||
reviewReason String?
|
||||
|
||||
installments Installment[]
|
||||
reservation SeatReservation?
|
||||
redemption VoucherRedemption?
|
||||
refunds Refund[]
|
||||
emails EmailMessage[]
|
||||
|
||||
@@index([livemode, status])
|
||||
@@index([email])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
enum InstallmentStatus {
|
||||
/// Noch nicht faellig bzw. erste Rate vor dem Checkout.
|
||||
PENDING
|
||||
/// Einzug laeuft (Worker hat die Rate beansprucht).
|
||||
PROCESSING
|
||||
PAID
|
||||
FAILED
|
||||
/// Bank verlangt zusaetzliche Authentifizierung: Kundin zahlt ueber einen
|
||||
/// Link auf der Statusseite selbst.
|
||||
REQUIRES_ACTION
|
||||
CANCELED
|
||||
}
|
||||
|
||||
model Installment {
|
||||
id String @id @default(cuid())
|
||||
bookingId String
|
||||
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
|
||||
|
||||
seq Int
|
||||
amountCents Int
|
||||
/// null = faellig bei Buchung (erste Rate).
|
||||
dueAt DateTime?
|
||||
|
||||
status InstallmentStatus @default(PENDING)
|
||||
attemptCount Int @default(0)
|
||||
lastAttemptAt DateTime?
|
||||
nextAttemptAt DateTime?
|
||||
failureCode String?
|
||||
paidAt DateTime?
|
||||
refundedCents Int @default(0)
|
||||
|
||||
stripePaymentIntentId String? @unique
|
||||
stripeChargeId String? @unique
|
||||
/// Offene Checkout-Session, ueber die die Kundin diese Rate selbst zahlt.
|
||||
checkoutSessionId String? @unique
|
||||
checkoutExpiresAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
refunds Refund[]
|
||||
|
||||
@@unique([bookingId, seq])
|
||||
@@index([status, dueAt])
|
||||
}
|
||||
|
||||
model Refund {
|
||||
id String @id @default(cuid())
|
||||
bookingId String
|
||||
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
|
||||
installmentId String?
|
||||
installment Installment? @relation(fields: [installmentId], references: [id])
|
||||
|
||||
stripeRefundId String @unique
|
||||
amountCents Int
|
||||
status String
|
||||
reason String?
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kapazitaet
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum ReservationStatus {
|
||||
/// Zeitlich begrenzt, verfaellt mit expiresAt.
|
||||
ACTIVE
|
||||
/// Verzoegerte Zahlung unterwegs: Platz bleibt bis zum Ergebnis gehalten.
|
||||
HELD
|
||||
CONVERTED
|
||||
RELEASED
|
||||
}
|
||||
|
||||
model SeatReservation {
|
||||
id String @id @default(cuid())
|
||||
bookingId String @unique
|
||||
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
|
||||
campId String
|
||||
camp Camp @relation(fields: [campId], references: [id])
|
||||
livemode Boolean
|
||||
|
||||
status ReservationStatus @default(ACTIVE)
|
||||
expiresAt DateTime
|
||||
releasedAt DateTime?
|
||||
releaseReason String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([campId, livemode, status, expiresAt])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gutscheine (Stammmodel-Codes)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Voucher {
|
||||
id String @id @default(cuid())
|
||||
/// Immer in Grossbuchstaben normalisiert gespeichert.
|
||||
code String @unique
|
||||
|
||||
active Boolean @default(true)
|
||||
validFrom DateTime?
|
||||
validUntil DateTime?
|
||||
maxRedemptions Int @default(1)
|
||||
/// Optional: nur fuer diese E-Mail-Adresse (klein geschrieben) einloesbar.
|
||||
boundEmail String?
|
||||
/// Interne Zuordnung, z. B. Name des Stammmodels. Nie oeffentlich.
|
||||
internalNote String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
redemptions VoucherRedemption[]
|
||||
bookings Booking[]
|
||||
}
|
||||
|
||||
enum RedemptionStatus {
|
||||
RESERVED
|
||||
COMPLETED
|
||||
RELEASED
|
||||
}
|
||||
|
||||
model VoucherRedemption {
|
||||
id String @id @default(cuid())
|
||||
voucherId String
|
||||
voucher Voucher @relation(fields: [voucherId], references: [id], onDelete: Restrict)
|
||||
bookingId String @unique
|
||||
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
|
||||
livemode Boolean
|
||||
|
||||
status RedemptionStatus @default(RESERVED)
|
||||
releasedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([voucherId, livemode, status])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stripe-Ereignisse, E-Mail-Ausgang, Protokoll, Rate Limits
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum StripeEventStatus {
|
||||
PROCESSING
|
||||
PROCESSED
|
||||
IGNORED
|
||||
FAILED
|
||||
}
|
||||
|
||||
model StripeEvent {
|
||||
/// Stripe-Event-ID (evt_...) — Primaerschluessel macht die Verarbeitung
|
||||
/// idempotent.
|
||||
id String @id
|
||||
type String
|
||||
livemode Boolean
|
||||
status StripeEventStatus @default(PROCESSING)
|
||||
error String?
|
||||
attempts Int @default(1)
|
||||
receivedAt DateTime @default(now())
|
||||
processedAt DateTime?
|
||||
}
|
||||
|
||||
enum EmailStatus {
|
||||
PENDING
|
||||
SENT
|
||||
FAILED
|
||||
}
|
||||
|
||||
/// Transaktionaler Postausgang: E-Mails entstehen in derselben Transaktion
|
||||
/// wie die Statusaenderung und werden danach (bzw. vom Worker) versendet.
|
||||
model EmailMessage {
|
||||
id String @id @default(cuid())
|
||||
/// Verhindert doppelte Mails bei wiederholten Webhooks.
|
||||
dedupeKey String @unique
|
||||
bookingId String?
|
||||
booking Booking? @relation(fields: [bookingId], references: [id])
|
||||
|
||||
toAddress String
|
||||
subject String
|
||||
textBody String
|
||||
htmlBody String
|
||||
|
||||
status EmailStatus @default(PENDING)
|
||||
attempts Int @default(0)
|
||||
lastError String?
|
||||
sentAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status, createdAt])
|
||||
}
|
||||
|
||||
enum ActorType {
|
||||
ADMIN
|
||||
CUSTOMER
|
||||
STRIPE
|
||||
WORKER
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
/// Nachvollziehbarkeit fuer Bestands-, Buchungs- und Zahlungsaktionen.
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
actorType ActorType
|
||||
actorId String?
|
||||
action String
|
||||
entityType String
|
||||
entityId String
|
||||
details Json?
|
||||
|
||||
@@index([entityType, entityId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
/// Gemeinsamer Zaehler fuer Rate Limits — funktioniert ueber mehrere
|
||||
/// Replikate hinweg. Der Schluessel enthaelt nur einen Hash der IP.
|
||||
model RateLimitBucket {
|
||||
key String @id
|
||||
windowStart DateTime
|
||||
count Int
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Better Auth (Adminbereich)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
name String
|
||||
email String @unique
|
||||
emailVerified Boolean @default(false)
|
||||
image String?
|
||||
twoFactorEnabled Boolean? @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
twofactors TwoFactor[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id
|
||||
expiresAt DateTime
|
||||
token String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("session")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id
|
||||
accountId String
|
||||
providerId String
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
accessToken String?
|
||||
refreshToken String?
|
||||
idToken String?
|
||||
accessTokenExpiresAt DateTime?
|
||||
refreshTokenExpiresAt DateTime?
|
||||
scope String?
|
||||
password String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@map("account")
|
||||
}
|
||||
|
||||
model Verification {
|
||||
id String @id
|
||||
identifier String
|
||||
value String
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([identifier])
|
||||
@@map("verification")
|
||||
}
|
||||
|
||||
model TwoFactor {
|
||||
id String @id
|
||||
secret String
|
||||
backupCodes String
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
verified Boolean? @default(true)
|
||||
failedVerificationCount Int? @default(0)
|
||||
lockedUntil DateTime?
|
||||
|
||||
@@index([secret])
|
||||
@@index([userId])
|
||||
@@map("twoFactor")
|
||||
}
|
||||
|
||||
/// Rate Limits der Anmeldung (Better Auth, Modellname vom Adapter vorgegeben).
|
||||
model RateLimit {
|
||||
id String @id
|
||||
key String @unique
|
||||
count Int
|
||||
lastRequest BigInt
|
||||
|
||||
@@map("rateLimit")
|
||||
}
|
||||
Reference in new issue
Block a user