added card creation and booking functionality

This commit is contained in:
Rajpreet0 2026-10-02 14:30:54 +02:00
parent 86330af16e
commit 7aa887c5aa
85 changed files with 7434 additions and 314 deletions

View file

@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"businessId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"credits" INTEGER NOT NULL,
"priceCents" INTEGER NOT NULL,
"validityMonths" INTEGER,
"sports" TEXT[],
"design" JSONB NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Product_businessId_idx" ON "Product"("businessId");
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,92 @@
-- AlterTable
ALTER TABLE "CourseBooking" ADD COLUMN "passId" TEXT;
-- CreateTable
CREATE TABLE "CustomerPass" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"businessId" TEXT NOT NULL,
"userId" TEXT,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"sports" TEXT[],
"creditsTotal" INTEGER NOT NULL,
"creditsRemaining" INTEGER NOT NULL,
"validUntil" TIMESTAMP(3),
"status" TEXT NOT NULL DEFAULT 'active',
"source" TEXT NOT NULL,
"priceCents" INTEGER,
"stripeAccountId" TEXT,
"stripeSessionId" TEXT,
"stripePaymentIntentId" TEXT,
"amountPaid" INTEGER,
"platformFee" INTEGER,
"stripeFee" INTEGER,
"billingAddress" JSONB,
"invoiceNumber" TEXT,
"invoiceIssuedAt" TIMESTAMP(3),
"refundedAmount" INTEGER,
"refundedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CustomerPass_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PassAdjustment" (
"id" TEXT NOT NULL,
"passId" TEXT NOT NULL,
"managerId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"creditsDelta" INTEGER,
"previousValidUntil" TIMESTAMP(3),
"newValidUntil" TIMESTAMP(3),
"reason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PassAdjustment_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "CustomerPass_stripeSessionId_key" ON "CustomerPass"("stripeSessionId");
-- CreateIndex
CREATE UNIQUE INDEX "CustomerPass_stripePaymentIntentId_key" ON "CustomerPass"("stripePaymentIntentId");
-- CreateIndex
CREATE UNIQUE INDEX "CustomerPass_invoiceNumber_key" ON "CustomerPass"("invoiceNumber");
-- CreateIndex
CREATE INDEX "CustomerPass_businessId_idx" ON "CustomerPass"("businessId");
-- CreateIndex
CREATE INDEX "CustomerPass_userId_idx" ON "CustomerPass"("userId");
-- CreateIndex
CREATE INDEX "CustomerPass_email_idx" ON "CustomerPass"("email");
-- CreateIndex
CREATE INDEX "CustomerPass_productId_idx" ON "CustomerPass"("productId");
-- CreateIndex
CREATE INDEX "PassAdjustment_passId_idx" ON "PassAdjustment"("passId");
-- CreateIndex
CREATE INDEX "CourseBooking_passId_idx" ON "CourseBooking"("passId");
-- AddForeignKey
ALTER TABLE "CourseBooking" ADD CONSTRAINT "CourseBooking_passId_fkey" FOREIGN KEY ("passId") REFERENCES "CustomerPass"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerPass" ADD CONSTRAINT "CustomerPass_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE NO ACTION ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerPass" ADD CONSTRAINT "CustomerPass_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerPass" ADD CONSTRAINT "CustomerPass_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PassAdjustment" ADD CONSTRAINT "PassAdjustment_passId_fkey" FOREIGN KEY ("passId") REFERENCES "CustomerPass"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -43,6 +43,7 @@ model User {
courseBookings CourseBooking[]
courseWaitlistEntries CourseWaitlistEntry[]
discountRedemptions DiscountRedemption[]
passes CustomerPass[]
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
}
@ -140,6 +141,8 @@ model Business {
manager Manager @relation(fields: [managerId], references: [id])
courses Course[]
discountCodes DiscountCode[]
products Product[]
customerPasses CustomerPass[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -243,6 +246,13 @@ model CourseBooking {
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
// Mit einer Karte gebucht: kein Geld, keine Stripe-Felder, keine Rechnung (die
// gehört zum Kartenkauf). Pro Platz ist eine Einheit von der Karte abgegangen.
// Status wie bei bezahlten Buchungen ("paid" = gebucht), damit Auslastung und
// Platzwahl unverändert über seat-counts.ts laufen. Storno: "refunded" = Einheit
// zurück auf der Karte, "cancelled" = Einheit verfallen.
passId String?
pass CustomerPass? @relation(fields: [passId], references: [id], onDelete: SetNull)
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)
@ -256,6 +266,7 @@ model CourseBooking {
@@index([guestLeadId])
@@index([discountCodeId])
@@index([spotIds], type: Gin)
@@index([passId])
}
// Hält einen Platz frei, während der Kunde im Stripe-Checkout bezahlt - analog
@ -400,3 +411,92 @@ model DiscountRedemption {
@@index([userId])
@@index([status])
}
// Karte, die ein Studio verkauft, z.B. eine 10er-Karte. Eingeloggte Kunden
// kaufen sie auf der öffentlichen Seite und lösen die Einheiten beim Buchen ein:
// 1 Einheit = 1 Platz in einem Kurs. Zum Preis kommt die Servicegebühr pro
// Einheit dazu (siehe src/modules/products/lib/card-pricing.ts).
model Product {
id String @id @default(uuid())
businessId String
name String
description String?
credits Int // Einheiten pro Karte
priceCents Int // Preis der Karte in Cents, ohne Servicegebühr
validityMonths Int? // Gültigkeit ab Kauf in Monaten, null = unbegrenzt gültig
sports String[] // Kursarten (Course.sport), für die die Karte gilt. Leer = alle Kurse.
design Json // Kartendesign, Format siehe src/modules/products/lib/card-design.ts
isActive Boolean @default(true) // false = nicht mehr kaufbar
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade)
passes CustomerPass[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([businessId])
}
// Eine Karte, die ein Kunde gekauft oder vom Studio bekommen hat. Name, Kursarten,
// Einheiten und Ablaufdatum sind Momentaufnahmen beim Kauf - spätere Änderungen am
// Produkt gelten nur für neue Käufe. Das Design kommt live vom Produkt.
//
// creditsRemaining ist ein Zähler: Buchen zieht ab (atomar, nur wenn genug übrig
// ist), Storno innerhalb der Frist und Kursabsage geben zurück, der Manager passt
// ihn im Kunden-Detail an. Jede manuelle Änderung steht in PassAdjustment.
model CustomerPass {
id String @id @default(uuid())
productId String
businessId String
// null = vom Studio an eine E-Mail vergeben, die noch kein Konto hat. Die Karte
// hängt sich an das Konto, sobald sich jemand mit dieser Adresse anmeldet.
userId String?
email String // Empfänger, immer lowercase
name String
sports String[] // leer = alle Kurse
creditsTotal Int // alle Einheiten, die die Karte je bekommen hat (inkl. Gutschriften)
creditsRemaining Int
validUntil DateTime? // null = unbegrenzt gültig
status String @default("active") // "active" | "ended" (vom Studio beendet) | "refunded"
source String // "purchase" | "manual" (vom Studio vergeben, ohne Zahlung)
// Zahlung - nur bei source = "purchase"
priceCents Int?
stripeAccountId String?
stripeSessionId String? @unique
stripePaymentIntentId String? @unique
amountPaid Int? // inkl. Servicegebühr
platformFee Int?
stripeFee Int?
billingAddress Json?
invoiceNumber String? @unique
invoiceIssuedAt DateTime?
refundedAmount Int?
refundedAt DateTime?
product Product @relation(fields: [productId], references: [id], onDelete: NoAction)
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
bookings CourseBooking[]
adjustments PassAdjustment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([businessId])
@@index([userId])
@@index([email])
@@index([productId])
}
// Protokoll jeder Änderung durch das Studio: Karte vergeben, Einheiten
// gutgeschrieben oder abgezogen, verlängert, beendet, erstattet.
model PassAdjustment {
id String @id @default(uuid())
passId String
managerId String
type String // "issue" | "credits" | "extend" | "end" | "refund"
creditsDelta Int?
previousValidUntil DateTime?
newValidUntil DateTime?
reason String?
pass CustomerPass @relation(fields: [passId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([passId])
}