mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
added card creation and booking functionality
This commit is contained in:
parent
86330af16e
commit
7aa887c5aa
85 changed files with 7434 additions and 314 deletions
23
prisma/migrations/20261004100000_products/migration.sql
Normal file
23
prisma/migrations/20261004100000_products/migration.sql
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue