added discount codes

This commit is contained in:
Rajpreet0 2026-09-13 18:45:46 +02:00
parent d045ee1600
commit db514248a2
13 changed files with 1408 additions and 43 deletions

View file

@ -42,6 +42,7 @@ model User {
trainings UserTraining[]
courseBookings CourseBooking[]
courseWaitlistEntries CourseWaitlistEntry[]
discountRedemptions DiscountRedemption[]
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
}
@ -128,10 +129,11 @@ model Business {
onboardingStartedAt DateTime? // Wizard erstmals geoeffnet, ab dann kein Redirect mehr
onboardingDismissedAt DateTime? // Checkliste im Dashboard ausgeblendet
onboardingCompletedAt DateTime? // Zeitpunkt, an dem erstmals alle Pflichtschritte erledigt waren
manager Manager @relation(fields: [managerId], references: [id])
manager Manager @relation(fields: [managerId], references: [id])
courses Course[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
discountCodes DiscountCode[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([managerId])
}
@ -195,30 +197,35 @@ model GuestLead {
}
model CourseBooking {
id String @id @default(uuid())
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
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
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())
discountCodeId String? // Gesetzt, sobald bei der Buchung ein gültiger Rabattcode eingelöst wurde
discountAmount Int? // Tatsächlich gewährter Rabatt in Cents (Snapshot, unabhängig von späteren Code-Änderungen)
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)
discountCode DiscountCode? @relation(fields: [discountCodeId], references: [id], onDelete: SetNull)
discountRedemption DiscountRedemption?
createdAt DateTime @default(now())
@@unique([courseId, userId]) // Ein User kann sich nur einmal pro Kurs anmelden
@@index([courseId])
@@index([userId])
@@index([guestLeadId])
@@index([discountCodeId])
}
// Bewusst nur für registrierte User (kein guestEmail-Feld): ein
@ -258,3 +265,49 @@ model Notification {
@@index([recipientId, readAt])
@@index([recipientId, createdAt])
}
// Rabattcode eines Business. scope = "specific_courses" schränkt die Gültigkeit
// über courseIds ein (Array statt Join-Tabelle, analog Course.trainers/sport —
// hier reicht eine Handvoll IDs pro Code, kein eigenes Modell nötig).
// redeemedCount wird ausschließlich im Stripe-Webhook per {increment: 1} erhöht,
// niemals beim Checkout-Start (siehe DiscountRedemption).
model DiscountCode {
id String @id @default(uuid())
businessId String
code String // Anzeigeform, z.B. "SOMMER25" — Eindeutigkeit nur pro Business (siehe @@unique)
type String // "percent" | "fixed"
value Int // percent: 1-100, fixed: Cents
scope String // "all_courses" | "specific_courses"
courseIds String[] // Nur relevant wenn scope = "specific_courses"
maxRedemptions Int? // null = unbegrenzt
redeemedCount Int @default(0)
startsAt DateTime? // null = sofort gültig
expiresAt DateTime? // null = kein Ablauf
isActive Boolean @default(true) // Manuelles An/Aus, unabhängig vom Zeitfenster
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade)
bookings CourseBooking[]
redemptions DiscountRedemption[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([businessId, code])
@@index([businessId])
}
// Ein Eintrag pro tatsächlich eingelöstem Rabattcode (nur bei bezahlter Buchung,
// siehe Webhook handleCheckoutCompleted). Der @@unique-Constraint erzwingt
// "1x pro User" race-condition-sicher auf DB-Ebene, unabhängig von Anwendungslogik.
model DiscountRedemption {
id String @id @default(uuid())
discountCodeId String
userId String
bookingId String @unique
discountCode DiscountCode @relation(fields: [discountCodeId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
booking CourseBooking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([discountCodeId, userId])
@@index([discountCodeId])
@@index([userId])
}