// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { provider = "prisma-client" output = "../src/generated/prisma" engineType = "client" } datasource db { provider = "postgresql" url = env("DATABASE_URL") directUrl = env("DIRECT_URL") } model User { id String @id @default(uuid()) name String? complaints String? birthday DateTime? plz String? city String? street String? houseNumber String? // @unique is Prisma-managed and stays case-sensitive at the schema level — // Prisma has no functional-index syntax. The actual enforced constraint is // a case-insensitive unique index on lower(email), added via raw SQL in // migration 20260817000000_case_insensitive_user_email. Do not remove // @unique here; it keeps Prisma's drift detection from trying to recreate // a duplicate case-sensitive constraint. email String @unique tel String? gender String? is2FA Boolean @default(false) isSendingMail Boolean @default(false) isNewsletter Boolean @default(false) pbSrc String? isValid Boolean @default(false) trainings UserTraining[] courseBookings CourseBooking[] courseWaitlistEntries CourseWaitlistEntry[] createdAt DateTime? @default(now()) updatedAt DateTime? @updatedAt } model Employee { id String @id @default(uuid()) firstName String? lastName String? email String @unique is2FA Boolean @default(false) isSendingMail Boolean @default(false) pbSrc String? status String? roles String[] @default([]) locations String[] @default([]) permissions Json? affiliation String? label String? qualification String? authorization Json? onboardingToken String? @unique onboardingTokenExpiry DateTime? isOnboarded Boolean @default(false) createdBy String? // Manager ID who created the employee trainings UserTraining[] createdAt DateTime? @default(now()) updatedAt DateTime? @updatedAt @@index([createdBy]) } model UserTraining { userId String employeeId String employee Employee @relation(fields: [employeeId], references: [id]) user User @relation(fields: [userId], references: [id]) @@id([userId, employeeId]) } model Manager { id String @id @default(uuid()) firstName String lastName String tel String? email String @unique is2FA Boolean @default(false) isSendingMail Boolean @default(false) pbSrc String? businesses Business[] createdAt DateTime? @default(now()) updatedAt DateTime? @updatedAt } model Business { id String @id @default(uuid()) managerId String name String address String email String title String ustId String? banking String? plan String? subscription Json? slug String? @unique isPublic Boolean @default(false) stripeAccountId String? @unique // Stripe Connect Account ID stripeAccountStatus String? // "pending" | "active" | "restricted" invoiceCounter Int @default(0) // Fortlaufender Zähler für Rechnungsnummern cancellationPolicy Json? // { tiers: [{ hoursBeforeStart: number, refundPercent: number }, ...] }, absteigend sortiert. null = immer 100% Rückerstattung. branding Json? // { companyName, logoUrl, primaryColor, accentColor }. null = Kumasy-Standardbranding (siehe KUMASY_BRANDING). manager Manager @relation(fields: [managerId], references: [id]) courses Course[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([managerId]) } model Course { id String @id @default(uuid()) name String sport String[] level String @default("any") date DateTime timeFrom String // Anfangszeit timeTo String // Endzeit trainers String[] // Array of trainer IDs room String description String // Rich text HTML content maxParticipants Int // Maximum number of participants price Float @default(10.0) // Preis in Euro isStandingOrder Boolean @default(false) frequency String? // daily, weekly, biweekly, monthly, custom weekdays String[] // For custom frequency weekdayTimings Json? // { "monday": { "timeFrom": "08:00", "timeTo": "09:00" }, ... } endDate DateTime? // Required when isStandingOrder = true parentCourseId String? // Null on template; set on every child instance parentCourse Course? @relation("StandingOrderInstances", fields: [parentCourseId], references: [id], onDelete: Cascade) children Course[] @relation("StandingOrderInstances") status String @default("draft") // draft, published, cancelled cancelledAt DateTime? // Zeitpunkt der Kursabsage (nur gesetzt wenn status = "cancelled") createdBy String // Manager ID who created the course businessId String? coverImage String? business Business? @relation(fields: [businessId], references: [id], onDelete: SetNull) bookings CourseBooking[] waitlistEntries CourseWaitlistEntry[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([createdBy]) @@index([date]) @@index([status]) @@index([businessId]) @@index([parentCourseId]) @@index([parentCourseId, date]) } model Room { id String @id @default(uuid()) name String createdBy String // Manager ID who created the room createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([createdBy]) } model GuestLead { id String @id @default(uuid()) email String @unique name String? bookings CourseBooking[] createdAt DateTime @default(now()) } model CourseBooking { id String @id @default(uuid()) courseId String userId String? guestLeadId String? paymentStatus String @default("pending") // "pending" | "paid" | "failed" | "refunded" | "partially_refunded" | "refunding" | "cancelled" (cancelled = storniert ohne Geldbewegung, 0% Erstattung) stripeSessionId String? @unique stripePaymentIntentId String? @unique amountPaid Int? // tatsächlich gezahlter Betrag in Cents platformFee Int? // unsere Service-Fee in Cents invoiceNumber String? @unique // Rechnungsnummer, wird lazy beim ersten PDF-Download vergeben invoiceIssuedAt DateTime? // Zeitpunkt der Rechnungsnummer-Vergabe, damit das Rechnungsdatum bei jedem Download gleich bleibt refundedAmount Int? // tatsächlich erstatteter Betrag in Cents refundedAt DateTime? // Zeitpunkt der erfolgreichen Stripe-Rückerstattung cancelledBy String? // "user" | "manager" | "system" (system = automatischer Überbuchungs-Refund) cancellationReason String? course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: Cascade) guestLead GuestLead? @relation(fields: [guestLeadId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) @@unique([courseId, userId]) // Ein User kann sich nur einmal pro Kurs anmelden @@index([courseId]) @@index([userId]) @@index([guestLeadId]) } // Bewusst nur für registrierte User (kein guestEmail-Feld): ein // unauthentifiziertes Gast-Formular würde eine beliebige, nicht verifizierte // E-Mail-Adresse als Benachrichtigungsempfänger akzeptieren, ohne dass deren // Kontrolle bestätigt wurde. Der Login-Zwang schließt das und wirkt nebenbei // als Anreiz zur Registrierung (Lead-Generierung). model CourseWaitlistEntry { id String @id @default(uuid()) courseId String userId String notifiedAt DateTime? // Zeitpunkt der letzten "Platz frei"-Benachrichtigung, null = noch nie benachrichtigt course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @@unique([courseId, userId]) // Ein User kann sich nur einmal pro Kurs eintragen @@index([courseId]) @@index([userId]) }