Docker-Images bauen und veroeffentlichen / build-and-push (push) Failing after 1m32s
530 lines
15 KiB
Plaintext
530 lines
15 KiB
Plaintext
// 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")
|
|
}
|