added multi-booking

This commit is contained in:
Rajpreet0 2026-10-02 01:28:49 +02:00
parent 55f0163af5
commit 1cd905be78
36 changed files with 1718 additions and 579 deletions

View file

@ -0,0 +1,24 @@
-- Mehrfachbuchung: eine Buchung kann mehrere Plätze umfassen.
-- AlterTable
ALTER TABLE "CourseBooking" ADD COLUMN "refundedFeeAmount" INTEGER,
ADD COLUMN "seats" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "seatsPurchased" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "spotIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "spotLabels" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- Bestehende Platzwahl-Buchungen übernehmen (ein Platz pro Buchung)
UPDATE "CourseBooking"
SET "spotIds" = ARRAY["spotId"],
"spotLabels" = ARRAY[COALESCE("spotLabel", '')]
WHERE "spotId" IS NOT NULL;
-- DropIndex
DROP INDEX "CourseBooking_spotId_idx";
-- AlterTable
ALTER TABLE "CourseBooking" DROP COLUMN "spotId",
DROP COLUMN "spotLabel";
-- CreateIndex
CREATE INDEX "CourseBooking_spotIds_idx" ON "CourseBooking" USING GIN ("spotIds");

View file

@ -228,14 +228,21 @@ model CourseBooking {
platformFeeOnTop Boolean @default(false) // true = Gebühr wurde dem Kunden ZUSÄTZLICH berechnet, amountPaid enthält sie. false = Altbuchung, bei der die prozentuale Fee aus dem Kurspreis einbehalten wurde.
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
refundedAmount Int? // tatsächlich erstatteter Betrag in Cents, kumuliert über alle Teilstornos. Bei "paid" > 0 = einzelne Plätze wurden storniert.
refundedFeeAmount Int? // Anteil von refundedAmount, der Servicegebühr war (nur bei Kursabsage). Kursanteil der Erstattung = refundedAmount - refundedFeeAmount.
refundedAt DateTime? // Zeitpunkt der erfolgreichen Stripe-Rückerstattung
cancelledBy String? // "user" | "manager" | "system" (system = automatischer Überbuchungs-Refund)
cancellationReason String?
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)
spotId String? // Gerät aus dem Raum-Grundriss (Room.floorPlan.items[].id), nur bei Kursen mit Platzwahl. Belegt ist ein Platz, solange die Buchung "paid" ist.
spotLabel String? // Anzeigename des Platzes als Momentaufnahme, z.B. "Reformer 3" - bleibt lesbar, auch wenn der Grundriss sich später ändert
// Mehrfachbuchung: eine Buchung = eine Zahlung, kann aber mehrere Plätze umfassen
// (eingeloggte Kunden buchen für Begleitpersonen mit). Belegte Plätze eines Kurses
// = Summe von seats über alle "paid"-Buchungen (siehe seat-counts.ts), nicht die
// Anzahl der Buchungen.
seats Int @default(1) // aktuell gehaltene Plätze - sinkt, wenn einzelne Plätze storniert werden
seatsPurchased Int @default(1) // ursprünglich bezahlte Plätze - Bezugsgröße für den Betrag pro Platz
spotIds String[] @default([]) // Geräte aus dem Raum-Grundriss (Room.floorPlan.items[].id), nur bei Kursen mit Platzwahl, ein Eintrag pro Platz. Belegt, solange die Buchung "paid" ist.
spotLabels String[] @default([]) // Anzeigenamen als Momentaufnahme, z.B. "Reformer 3", parallel zu spotIds - bleiben lesbar, auch wenn der Grundriss sich ändert
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)
@ -248,7 +255,7 @@ model CourseBooking {
@@index([userId])
@@index([guestLeadId])
@@index([discountCodeId])
@@index([spotId])
@@index([spotIds], type: Gin)
}
// Hält einen Platz frei, während der Kunde im Stripe-Checkout bezahlt - analog