mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
see comments
This commit is contained in:
parent
db514248a2
commit
f6f1973086
11 changed files with 512 additions and 184 deletions
|
|
@ -55,6 +55,16 @@ CREATE UNIQUE INDEX "DiscountRedemption_discountCodeId_userId_key" ON "DiscountR
|
|||
CREATE INDEX "CourseBooking_discountCodeId_idx" ON "CourseBooking"("discountCodeId");
|
||||
|
||||
-- AddForeignKey
|
||||
-- Bewusst OHNE CONCURRENTLY/NOT VALID: Postgres erlaubt CREATE INDEX
|
||||
-- CONCURRENTLY nie innerhalb einer Transaktion, `prisma migrate dev` führt
|
||||
-- aber jede Migration transaktional gegen eine Shadow-Database aus - dort
|
||||
-- würde CONCURRENTLY hart fehlschlagen und dauerhaft JEDE künftige
|
||||
-- `migrate dev`-Ausführung blockieren (nicht nur diese eine Migration), weil
|
||||
-- die Shadow-DB alle Migrationen der Reihe nach durchspielt. Der Trade-off
|
||||
-- ist hier vertretbar: CourseBooking existiert zu diesem Zeitpunkt zwar
|
||||
-- schon, aber discountCodeId ist eine brandneue Spalte ohne bestehende
|
||||
-- Nicht-NULL-Werte - Index-Build und FK-Validierung laufen daher in
|
||||
-- Sekundenbruchteilen, unabhängig von der Zeilenzahl der Tabelle.
|
||||
ALTER TABLE "CourseBooking" ADD CONSTRAINT "CourseBooking_discountCodeId_fkey" FOREIGN KEY ("discountCodeId") REFERENCES "DiscountCode"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
-- DropForeignKey
|
||||
ALTER TABLE "DiscountRedemption" DROP CONSTRAINT "DiscountRedemption_bookingId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "DiscountRedemption_bookingId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "DiscountRedemption"
|
||||
ADD COLUMN "stripeSessionId" TEXT,
|
||||
ADD COLUMN "status" TEXT NOT NULL DEFAULT 'reserved',
|
||||
ALTER COLUMN "bookingId" DROP NOT NULL;
|
||||
|
||||
-- Tabelle ist zu diesem Zeitpunkt leer (siehe vorherige Migration), daher
|
||||
-- ist ein Backfill für die neue NOT NULL-Spalte stripeSessionId nicht nötig -
|
||||
-- UPDATE würde ohnehin keine Zeile treffen. Erst danach kann die Spalte auf
|
||||
-- NOT NULL gesetzt werden, weil ALTER COLUMN ... SET NOT NULL sonst bereits
|
||||
-- vorhandene NULL-Werte prüfen würde.
|
||||
ALTER TABLE "DiscountRedemption" ALTER COLUMN "stripeSessionId" SET NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "DiscountRedemption_stripeSessionId_key" ON "DiscountRedemption"("stripeSessionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "DiscountRedemption_bookingId_key" ON "DiscountRedemption"("bookingId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "DiscountRedemption_status_idx" ON "DiscountRedemption"("status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DiscountRedemption" ADD CONSTRAINT "DiscountRedemption_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "CourseBooking"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -269,8 +269,11 @@ model Notification {
|
|||
// 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).
|
||||
// redeemedCount zählt RESERVIERTE Slots (status "reserved" ODER "confirmed"),
|
||||
// nicht nur bestätigte Zahlungen - die Reservierung passiert bereits beim
|
||||
// Checkout-Start, um Race Conditions zwischen mehreren gleichzeitigen
|
||||
// Checkouts zu verhindern (siehe DiscountRedemption). Der Zähler wird wieder
|
||||
// dekrementiert, wenn eine Reservierung storniert/abgelaufen wird.
|
||||
model DiscountCode {
|
||||
id String @id @default(uuid())
|
||||
businessId String
|
||||
|
|
@ -294,20 +297,33 @@ model DiscountCode {
|
|||
@@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.
|
||||
// Reserviert einen Rabattcode-Slot BEIM CHECKOUT-START, nicht erst nach
|
||||
// Zahlungsbestätigung - sonst könnten mehrere gleichzeitige Checkouts (versch.
|
||||
// User bei knappem Kontingent, oder ein User mit mehreren offenen Sessions)
|
||||
// alle den rabattierten Preis bekommen, obwohl nur ein Slot frei ist. status:
|
||||
// "reserved" (Checkout-Session erstellt, Zahlung ausstehend) → "confirmed"
|
||||
// (Webhook hat die Zahlung bestätigt, bookingId gesetzt) oder gelöscht (Session
|
||||
// abgelaufen/storniert, siehe handleCheckoutExpired - gibt den Slot wieder frei).
|
||||
// bookingId ist erst ab "confirmed" gesetzt, da beim Reservieren noch keine
|
||||
// CourseBooking-Zeile existiert. Der @@unique([discountCodeId, userId])
|
||||
// Constraint erzwingt "1x pro User" bereits ab der Reservierung, nicht erst
|
||||
// ab Zahlungsbestätigung - das schließt die zweite Race Condition
|
||||
// (ein User bucht mehrere Kurse mit demselben Code, bevor eine Zahlung
|
||||
// durchläuft) auf DB-Ebene.
|
||||
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())
|
||||
id String @id @default(uuid())
|
||||
discountCodeId String
|
||||
userId String
|
||||
stripeSessionId String @unique
|
||||
bookingId String? @unique
|
||||
status String @default("reserved") // "reserved" | "confirmed"
|
||||
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: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([discountCodeId, userId])
|
||||
@@index([discountCodeId])
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue