first commit
Docker-Images bauen und veroeffentlichen / build-and-push (push) Failing after 1m32s

This commit is contained in:
Nick Szaunig committed 2026-09-27 06:06:56 +02:00
commit 59bb5a38a3
132 files changed
+19435

No files matched your search

@@ -0,0 +1,528 @@
-- CreateEnum
CREATE TYPE "PricePhase" AS ENUM ('PREVIEW', 'EARLY_BIRD', 'REGULAR');
-- CreateEnum
CREATE TYPE "BookingStatus" AS ENUM ('PENDING', 'CONFIRMED', 'CANCELED', 'EXPIRED', 'NEEDS_REVIEW');
-- CreateEnum
CREATE TYPE "PaymentStatus" AS ENUM ('UNPAID', 'PROCESSING', 'PARTIALLY_PAID', 'PAID', 'FAILED', 'PARTIALLY_REFUNDED', 'REFUNDED');
-- CreateEnum
CREATE TYPE "PlanType" AS ENUM ('FULL', 'TWO_INSTALLMENTS', 'THREE_INSTALLMENTS');
-- CreateEnum
CREATE TYPE "InstallmentStatus" AS ENUM ('PENDING', 'PROCESSING', 'PAID', 'FAILED', 'REQUIRES_ACTION', 'CANCELED');
-- CreateEnum
CREATE TYPE "ReservationStatus" AS ENUM ('ACTIVE', 'HELD', 'CONVERTED', 'RELEASED');
-- CreateEnum
CREATE TYPE "RedemptionStatus" AS ENUM ('RESERVED', 'COMPLETED', 'RELEASED');
-- CreateEnum
CREATE TYPE "StripeEventStatus" AS ENUM ('PROCESSING', 'PROCESSED', 'IGNORED', 'FAILED');
-- CreateEnum
CREATE TYPE "EmailStatus" AS ENUM ('PENDING', 'SENT', 'FAILED');
-- CreateEnum
CREATE TYPE "ActorType" AS ENUM ('ADMIN', 'CUSTOMER', 'STRIPE', 'WORKER', 'SYSTEM');
-- CreateTable
CREATE TABLE "Camp" (
"id" TEXT NOT NULL,
"salesEnabled" BOOLEAN NOT NULL DEFAULT false,
"capacity" INTEGER NOT NULL DEFAULT 10,
"capacityConfirmed" BOOLEAN NOT NULL DEFAULT false,
"stammmodelQuota" INTEGER NOT NULL DEFAULT 4,
"stammmodelQuotaConfirmed" BOOLEAN NOT NULL DEFAULT false,
"campStartDate" DATE,
"campEndDate" DATE,
"dateConfirmed" BOOLEAN NOT NULL DEFAULT false,
"locationName" TEXT,
"locationRegion" TEXT,
"locationConfirmed" BOOLEAN NOT NULL DEFAULT false,
"accommodationText" TEXT,
"accommodationConfirmed" BOOLEAN NOT NULL DEFAULT false,
"servicesConfirmed" BOOLEAN NOT NULL DEFAULT false,
"organizerName" TEXT,
"organizerAddress" TEXT,
"organizerEmail" TEXT,
"organizerPhone" TEXT,
"organizerConfirmed" BOOLEAN NOT NULL DEFAULT false,
"taxNote" TEXT,
"taxConfirmed" BOOLEAN NOT NULL DEFAULT false,
"termsVersion" TEXT,
"termsUrl" TEXT,
"termsApproved" BOOLEAN NOT NULL DEFAULT false,
"travelLawChecked" BOOLEAN NOT NULL DEFAULT false,
"installmentsApproved" BOOLEAN NOT NULL DEFAULT false,
"installmentRuleApproved" BOOLEAN NOT NULL DEFAULT false,
"reservationMinutes" INTEGER NOT NULL DEFAULT 30,
"finalInstallmentLeadDays" INTEGER NOT NULL DEFAULT 30,
"minInstallmentGapDays" INTEGER NOT NULL DEFAULT 14,
"testPhaseOverride" "PricePhase",
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedBy" TEXT,
CONSTRAINT "Camp_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Booking" (
"id" TEXT NOT NULL,
"reference" TEXT NOT NULL,
"campId" TEXT NOT NULL,
"status" "BookingStatus" NOT NULL DEFAULT 'PENDING',
"paymentStatus" "PaymentStatus" NOT NULL DEFAULT 'UNPAID',
"livemode" BOOLEAN NOT NULL,
"idempotencyKey" TEXT NOT NULL,
"statusTokenHash" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"email" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"country" TEXT NOT NULL,
"notes" TEXT,
"adultConfirmedAt" TIMESTAMP(3) NOT NULL,
"termsAcceptedAt" TIMESTAMP(3) NOT NULL,
"termsVersion" TEXT NOT NULL,
"accommodationAcknowledgedAt" TIMESTAMP(3) NOT NULL,
"privacyAcknowledgedAt" TIMESTAMP(3) NOT NULL,
"installmentMandateAt" TIMESTAMP(3),
"pricePhase" "PricePhase" NOT NULL,
"basePriceCents" INTEGER NOT NULL,
"phaseDiscountCents" INTEGER NOT NULL,
"voucherDiscountCents" INTEGER NOT NULL,
"totalCents" INTEGER NOT NULL,
"planType" "PlanType" NOT NULL,
"priceSnapshot" JSONB NOT NULL,
"priceLockedUntil" TIMESTAMP(3) NOT NULL,
"voucherId" TEXT,
"stripeCustomerId" TEXT,
"stripeCheckoutSessionId" TEXT,
"stripeCheckoutUrl" TEXT,
"stripePaymentMethodId" TEXT,
"paymentMethodDetachedAt" TIMESTAMP(3),
"paidCents" INTEGER NOT NULL DEFAULT 0,
"refundedCents" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"confirmedAt" TIMESTAMP(3),
"canceledAt" TIMESTAMP(3),
"cancelReason" TEXT,
"reviewReason" TEXT,
CONSTRAINT "Booking_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Installment" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"seq" INTEGER NOT NULL,
"amountCents" INTEGER NOT NULL,
"dueAt" TIMESTAMP(3),
"status" "InstallmentStatus" NOT NULL DEFAULT 'PENDING',
"attemptCount" INTEGER NOT NULL DEFAULT 0,
"lastAttemptAt" TIMESTAMP(3),
"nextAttemptAt" TIMESTAMP(3),
"failureCode" TEXT,
"paidAt" TIMESTAMP(3),
"refundedCents" INTEGER NOT NULL DEFAULT 0,
"stripePaymentIntentId" TEXT,
"stripeChargeId" TEXT,
"checkoutSessionId" TEXT,
"checkoutExpiresAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Installment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Refund" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"installmentId" TEXT,
"stripeRefundId" TEXT NOT NULL,
"amountCents" INTEGER NOT NULL,
"status" TEXT NOT NULL,
"reason" TEXT,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Refund_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SeatReservation" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"campId" TEXT NOT NULL,
"livemode" BOOLEAN NOT NULL,
"status" "ReservationStatus" NOT NULL DEFAULT 'ACTIVE',
"expiresAt" TIMESTAMP(3) NOT NULL,
"releasedAt" TIMESTAMP(3),
"releaseReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SeatReservation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Voucher" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"validFrom" TIMESTAMP(3),
"validUntil" TIMESTAMP(3),
"maxRedemptions" INTEGER NOT NULL DEFAULT 1,
"boundEmail" TEXT,
"internalNote" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Voucher_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VoucherRedemption" (
"id" TEXT NOT NULL,
"voucherId" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"livemode" BOOLEAN NOT NULL,
"status" "RedemptionStatus" NOT NULL DEFAULT 'RESERVED',
"releasedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "VoucherRedemption_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StripeEvent" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"livemode" BOOLEAN NOT NULL,
"status" "StripeEventStatus" NOT NULL DEFAULT 'PROCESSING',
"error" TEXT,
"attempts" INTEGER NOT NULL DEFAULT 1,
"receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"processedAt" TIMESTAMP(3),
CONSTRAINT "StripeEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EmailMessage" (
"id" TEXT NOT NULL,
"dedupeKey" TEXT NOT NULL,
"bookingId" TEXT,
"toAddress" TEXT NOT NULL,
"subject" TEXT NOT NULL,
"textBody" TEXT NOT NULL,
"htmlBody" TEXT NOT NULL,
"status" "EmailStatus" NOT NULL DEFAULT 'PENDING',
"attempts" INTEGER NOT NULL DEFAULT 0,
"lastError" TEXT,
"sentAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmailMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"actorType" "ActorType" NOT NULL,
"actorId" TEXT,
"action" TEXT NOT NULL,
"entityType" TEXT NOT NULL,
"entityId" TEXT NOT NULL,
"details" JSONB,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RateLimitBucket" (
"key" TEXT NOT NULL,
"windowStart" TIMESTAMP(3) NOT NULL,
"count" INTEGER NOT NULL,
CONSTRAINT "RateLimitBucket_pkey" PRIMARY KEY ("key")
);
-- CreateTable
CREATE TABLE "user" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
"image" TEXT,
"twoFactorEnabled" BOOLEAN DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "session" (
"id" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"userId" TEXT NOT NULL,
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "account" (
"id" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"providerId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"accessToken" TEXT,
"refreshToken" TEXT,
"idToken" TEXT,
"accessTokenExpiresAt" TIMESTAMP(3),
"refreshTokenExpiresAt" TIMESTAMP(3),
"scope" TEXT,
"password" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verification" (
"id" TEXT NOT NULL,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "twoFactor" (
"id" TEXT NOT NULL,
"secret" TEXT NOT NULL,
"backupCodes" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "twoFactor_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "rateLimit" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"count" INTEGER NOT NULL,
"lastRequest" BIGINT NOT NULL,
CONSTRAINT "rateLimit_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Booking_reference_key" ON "Booking"("reference");
-- CreateIndex
CREATE UNIQUE INDEX "Booking_idempotencyKey_key" ON "Booking"("idempotencyKey");
-- CreateIndex
CREATE UNIQUE INDEX "Booking_statusTokenHash_key" ON "Booking"("statusTokenHash");
-- CreateIndex
CREATE UNIQUE INDEX "Booking_stripeCheckoutSessionId_key" ON "Booking"("stripeCheckoutSessionId");
-- CreateIndex
CREATE INDEX "Booking_livemode_status_idx" ON "Booking"("livemode", "status");
-- CreateIndex
CREATE INDEX "Booking_email_idx" ON "Booking"("email");
-- CreateIndex
CREATE INDEX "Booking_createdAt_idx" ON "Booking"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "Installment_stripePaymentIntentId_key" ON "Installment"("stripePaymentIntentId");
-- CreateIndex
CREATE UNIQUE INDEX "Installment_stripeChargeId_key" ON "Installment"("stripeChargeId");
-- CreateIndex
CREATE UNIQUE INDEX "Installment_checkoutSessionId_key" ON "Installment"("checkoutSessionId");
-- CreateIndex
CREATE INDEX "Installment_status_dueAt_idx" ON "Installment"("status", "dueAt");
-- CreateIndex
CREATE UNIQUE INDEX "Installment_bookingId_seq_key" ON "Installment"("bookingId", "seq");
-- CreateIndex
CREATE UNIQUE INDEX "Refund_stripeRefundId_key" ON "Refund"("stripeRefundId");
-- CreateIndex
CREATE UNIQUE INDEX "SeatReservation_bookingId_key" ON "SeatReservation"("bookingId");
-- CreateIndex
CREATE INDEX "SeatReservation_campId_livemode_status_expiresAt_idx" ON "SeatReservation"("campId", "livemode", "status", "expiresAt");
-- CreateIndex
CREATE UNIQUE INDEX "Voucher_code_key" ON "Voucher"("code");
-- CreateIndex
CREATE UNIQUE INDEX "VoucherRedemption_bookingId_key" ON "VoucherRedemption"("bookingId");
-- CreateIndex
CREATE INDEX "VoucherRedemption_voucherId_livemode_status_idx" ON "VoucherRedemption"("voucherId", "livemode", "status");
-- CreateIndex
CREATE UNIQUE INDEX "EmailMessage_dedupeKey_key" ON "EmailMessage"("dedupeKey");
-- CreateIndex
CREATE INDEX "EmailMessage_status_createdAt_idx" ON "EmailMessage"("status", "createdAt");
-- CreateIndex
CREATE INDEX "AuditLog_entityType_entityId_idx" ON "AuditLog"("entityType", "entityId");
-- CreateIndex
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
-- CreateIndex
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
-- CreateIndex
CREATE INDEX "session_userId_idx" ON "session"("userId");
-- CreateIndex
CREATE INDEX "account_userId_idx" ON "account"("userId");
-- CreateIndex
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
-- CreateIndex
CREATE INDEX "twoFactor_secret_idx" ON "twoFactor"("secret");
-- CreateIndex
CREATE INDEX "twoFactor_userId_idx" ON "twoFactor"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "rateLimit_key_key" ON "rateLimit"("key");
-- AddForeignKey
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_campId_fkey" FOREIGN KEY ("campId") REFERENCES "Camp"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_voucherId_fkey" FOREIGN KEY ("voucherId") REFERENCES "Voucher"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Installment" ADD CONSTRAINT "Installment_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_installmentId_fkey" FOREIGN KEY ("installmentId") REFERENCES "Installment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SeatReservation" ADD CONSTRAINT "SeatReservation_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SeatReservation" ADD CONSTRAINT "SeatReservation_campId_fkey" FOREIGN KEY ("campId") REFERENCES "Camp"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VoucherRedemption" ADD CONSTRAINT "VoucherRedemption_voucherId_fkey" FOREIGN KEY ("voucherId") REFERENCES "Voucher"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VoucherRedemption" ADD CONSTRAINT "VoucherRedemption_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmailMessage" ADD CONSTRAINT "EmailMessage_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "twoFactor" ADD CONSTRAINT "twoFactor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ---------------------------------------------------------------------------
-- Unveraenderlicher Preis-Snapshot und Zahlungsplan (Briefing Abschnitt 9).
-- Der bei Buchung vereinbarte Preis darf auch durch einen Programmfehler oder
-- einen direkten Datenbankzugriff nicht nachtraeglich veraendert werden.
-- Eine Vertragsaenderung heisst: stornieren und neu buchen.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION booking_snapshot_immutable() RETURNS trigger AS $$
BEGIN
IF NEW."reference" IS DISTINCT FROM OLD."reference"
OR NEW."livemode" IS DISTINCT FROM OLD."livemode"
OR NEW."idempotencyKey" IS DISTINCT FROM OLD."idempotencyKey"
OR NEW."pricePhase" IS DISTINCT FROM OLD."pricePhase"
OR NEW."basePriceCents" IS DISTINCT FROM OLD."basePriceCents"
OR NEW."phaseDiscountCents" IS DISTINCT FROM OLD."phaseDiscountCents"
OR NEW."voucherDiscountCents" IS DISTINCT FROM OLD."voucherDiscountCents"
OR NEW."totalCents" IS DISTINCT FROM OLD."totalCents"
OR NEW."planType" IS DISTINCT FROM OLD."planType"
OR NEW."priceSnapshot" IS DISTINCT FROM OLD."priceSnapshot"
OR NEW."priceLockedUntil" IS DISTINCT FROM OLD."priceLockedUntil"
OR NEW."voucherId" IS DISTINCT FROM OLD."voucherId"
THEN
RAISE EXCEPTION 'Preis-Snapshot der Buchung % ist unveraenderlich', OLD."id";
END IF;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER booking_snapshot_immutable
BEFORE UPDATE ON "Booking"
FOR EACH ROW EXECUTE FUNCTION booking_snapshot_immutable();
CREATE OR REPLACE FUNCTION installment_plan_immutable() RETURNS trigger AS $$
BEGIN
IF NEW."bookingId" IS DISTINCT FROM OLD."bookingId"
OR NEW."seq" IS DISTINCT FROM OLD."seq"
OR NEW."amountCents" IS DISTINCT FROM OLD."amountCents"
OR NEW."dueAt" IS DISTINCT FROM OLD."dueAt"
THEN
RAISE EXCEPTION 'Rate % ist Teil des vereinbarten Zahlungsplans und unveraenderlich', OLD."id";
END IF;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER installment_plan_immutable
BEFORE UPDATE ON "Installment"
FOR EACH ROW EXECUTE FUNCTION installment_plan_immutable();
-- Plausibilitaet auf Datenbankebene.
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_amounts_check"
CHECK ("totalCents" > 0 AND "basePriceCents" > 0 AND "phaseDiscountCents" >= 0
AND "voucherDiscountCents" >= 0 AND "paidCents" >= 0 AND "refundedCents" >= 0
AND "totalCents" = "basePriceCents" - "phaseDiscountCents" - "voucherDiscountCents");
ALTER TABLE "Installment" ADD CONSTRAINT "Installment_amount_check"
CHECK ("amountCents" > 0 AND "seq" BETWEEN 1 AND 3 AND "refundedCents" >= 0);
ALTER TABLE "Camp" ADD CONSTRAINT "Camp_limits_check"
CHECK ("capacity" >= 0 AND "stammmodelQuota" >= 0 AND "reservationMinutes" BETWEEN 30 AND 1440
AND "finalInstallmentLeadDays" >= 0 AND "minInstallmentGapDays" >= 1);
ALTER TABLE "Voucher" ADD CONSTRAINT "Voucher_limits_check"
CHECK ("maxRedemptions" >= 1 AND "code" = upper("code"));
@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "twoFactor" ADD COLUMN "failedVerificationCount" INTEGER DEFAULT 0,
ADD COLUMN "lockedUntil" TIMESTAMP(3),
ADD COLUMN "verified" BOOLEAN DEFAULT true;
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+529
View File
@@ -0,0 +1,529 @@
// ARTISTA MODELCAMP 2027 — Datenmodell
//
// Geld: ausschliesslich ganzzahlige Cent-Betraege (Int), Waehrung immer EUR.
// Zeit: Zeitstempel in UTC; fachliche Grenzen (Preisphasen, Faelligkeiten)
// werden in src/lib/time.ts gegen Europe/Berlin berechnet.
//
// Test- und Livebuchungen liegen in denselben Tabellen, sind aber ueber
// "livemode" strikt getrennt: Kapazitaet und Gutscheinkontingente zaehlen nur
// Buchungen desselben Modus, damit Stripe-Testbuchungen nie echte Plaetze
// blockieren.
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
// ---------------------------------------------------------------------------
// Camp und Verkaufsfreigaben
// ---------------------------------------------------------------------------
enum PricePhase {
PREVIEW
EARLY_BIRD
REGULAR
}
/// Genau eine Zeile (id = "modelcamp-2027"). Enthaelt die betrieblichen
/// Angaben, die den Live-Verkauf freischalten. Die Preise selbst sind
/// verbindlich vorgegeben und stehen in src/lib/pricing.ts, nicht hier.
model Camp {
id String @id
/// Hauptschalter fuer den Live-Verkauf. Wirkt nur zusammen mit einer
/// vollstaendigen Freigabe-Checkliste (src/lib/sales-gate.ts).
salesEnabled Boolean @default(false)
capacity Int @default(10)
capacityConfirmed Boolean @default(false)
stammmodelQuota Int @default(4)
stammmodelQuotaConfirmed Boolean @default(false)
campStartDate DateTime? @db.Date
campEndDate DateTime? @db.Date
dateConfirmed Boolean @default(false)
locationName String?
locationRegion String?
locationConfirmed Boolean @default(false)
/// Verbindliche Zimmer- und Bettbeschreibung, wird vor dem Kauf angezeigt
/// und muss ausdruecklich bestaetigt werden.
accommodationText String?
accommodationConfirmed Boolean @default(false)
/// Bestaetigt, dass die in src/content/site.ts als "confirmed" markierten
/// Leistungen verbindlich sind.
servicesConfirmed Boolean @default(false)
organizerName String?
organizerAddress String?
organizerEmail String?
organizerPhone String?
organizerConfirmed Boolean @default(false)
/// Steuerlich gepruefter Preis-Hinweis, z. B. wie die Umsatzsteuer
/// ausgewiesen wird. Ohne Freigabe erscheint KEINE Steueraussage.
taxNote String?
taxConfirmed Boolean @default(false)
/// Version der freigegebenen Buchungs-, Storno- und Zahlungsbedingungen.
termsVersion String?
termsUrl String?
termsApproved Boolean @default(false)
travelLawChecked Boolean @default(false)
installmentsApproved Boolean @default(false)
installmentRuleApproved Boolean @default(false)
/// Reservierungsdauer fuer einen begonnenen Checkout. Stripe verlangt fuer
/// expires_at mindestens 30 Minuten.
reservationMinutes Int @default(30)
finalInstallmentLeadDays Int @default(30)
minInstallmentGapDays Int @default(14)
/// Nur im Stripe-Testmodus wirksam: simuliert eine Preisphase, damit
/// Testbuchungen auch vor dem 01.11.2026 moeglich sind.
testPhaseOverride PricePhase?
updatedAt DateTime @updatedAt
updatedBy String?
bookings Booking[]
reservations SeatReservation[]
}
// ---------------------------------------------------------------------------
// Buchung
// ---------------------------------------------------------------------------
enum BookingStatus {
/// Platz reserviert, Zahlung der ersten Rate steht aus.
PENDING
CONFIRMED
CANCELED
/// Checkout abgebrochen oder abgelaufen, Platz wieder frei.
EXPIRED
/// Zahlung eingegangen, aber ein Konflikt (z. B. Platz inzwischen
/// vergeben) verlangt eine manuelle Entscheidung.
NEEDS_REVIEW
}
enum PaymentStatus {
UNPAID
/// Verzoegerte Zahlungsart (z. B. SEPA) ist unterwegs.
PROCESSING
PARTIALLY_PAID
PAID
FAILED
PARTIALLY_REFUNDED
REFUNDED
}
enum PlanType {
FULL
TWO_INSTALLMENTS
THREE_INSTALLMENTS
}
model Booking {
id String @id @default(cuid())
/// Oeffentliche Buchungsnummer, z. B. AMC27-7K3Q9P.
reference String @unique
campId String
camp Camp @relation(fields: [campId], references: [id])
status BookingStatus @default(PENDING)
paymentStatus PaymentStatus @default(UNPAID)
livemode Boolean
/// Vom Browser erzeugter Schluessel je Buchungsversuch: ein doppelt
/// abgeschicktes Formular fuehrt zur selben Buchung statt zu zweien.
idempotencyKey String @unique
/// SHA-256 des Zugangstokens fuer die Statusseite. Das Token selbst wird
/// nie gespeichert.
statusTokenHash String @unique
firstName String
lastName String
email String
phone String
country String
notes String?
adultConfirmedAt DateTime
termsAcceptedAt DateTime
termsVersion String
accommodationAcknowledgedAt DateTime
privacyAcknowledgedAt DateTime
/// Zustimmung zum Einzug spaeterer Raten (nur bei Ratenzahlung).
installmentMandateAt DateTime?
// --- Preis-Snapshot (per Datenbank-Trigger unveraenderlich) -------------
pricePhase PricePhase
basePriceCents Int
phaseDiscountCents Int
voucherDiscountCents Int
totalCents Int
planType PlanType
priceSnapshot Json
/// Bis hierhin gilt der gebundene Preis (= Ablauf der Stripe-Session).
priceLockedUntil DateTime
voucherId String?
voucher Voucher? @relation(fields: [voucherId], references: [id])
stripeCustomerId String?
stripeCheckoutSessionId String? @unique
stripeCheckoutUrl String?
/// Referenz (keine Kartendaten) auf die fuer Folgeraten gespeicherte
/// Zahlungsmethode. Wird nach der letzten Rate bei Stripe getrennt.
stripePaymentMethodId String?
paymentMethodDetachedAt DateTime?
paidCents Int @default(0)
refundedCents Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
confirmedAt DateTime?
canceledAt DateTime?
cancelReason String?
reviewReason String?
installments Installment[]
reservation SeatReservation?
redemption VoucherRedemption?
refunds Refund[]
emails EmailMessage[]
@@index([livemode, status])
@@index([email])
@@index([createdAt])
}
enum InstallmentStatus {
/// Noch nicht faellig bzw. erste Rate vor dem Checkout.
PENDING
/// Einzug laeuft (Worker hat die Rate beansprucht).
PROCESSING
PAID
FAILED
/// Bank verlangt zusaetzliche Authentifizierung: Kundin zahlt ueber einen
/// Link auf der Statusseite selbst.
REQUIRES_ACTION
CANCELED
}
model Installment {
id String @id @default(cuid())
bookingId String
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
seq Int
amountCents Int
/// null = faellig bei Buchung (erste Rate).
dueAt DateTime?
status InstallmentStatus @default(PENDING)
attemptCount Int @default(0)
lastAttemptAt DateTime?
nextAttemptAt DateTime?
failureCode String?
paidAt DateTime?
refundedCents Int @default(0)
stripePaymentIntentId String? @unique
stripeChargeId String? @unique
/// Offene Checkout-Session, ueber die die Kundin diese Rate selbst zahlt.
checkoutSessionId String? @unique
checkoutExpiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
refunds Refund[]
@@unique([bookingId, seq])
@@index([status, dueAt])
}
model Refund {
id String @id @default(cuid())
bookingId String
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
installmentId String?
installment Installment? @relation(fields: [installmentId], references: [id])
stripeRefundId String @unique
amountCents Int
status String
reason String?
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ---------------------------------------------------------------------------
// Kapazitaet
// ---------------------------------------------------------------------------
enum ReservationStatus {
/// Zeitlich begrenzt, verfaellt mit expiresAt.
ACTIVE
/// Verzoegerte Zahlung unterwegs: Platz bleibt bis zum Ergebnis gehalten.
HELD
CONVERTED
RELEASED
}
model SeatReservation {
id String @id @default(cuid())
bookingId String @unique
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
campId String
camp Camp @relation(fields: [campId], references: [id])
livemode Boolean
status ReservationStatus @default(ACTIVE)
expiresAt DateTime
releasedAt DateTime?
releaseReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([campId, livemode, status, expiresAt])
}
// ---------------------------------------------------------------------------
// Gutscheine (Stammmodel-Codes)
// ---------------------------------------------------------------------------
model Voucher {
id String @id @default(cuid())
/// Immer in Grossbuchstaben normalisiert gespeichert.
code String @unique
active Boolean @default(true)
validFrom DateTime?
validUntil DateTime?
maxRedemptions Int @default(1)
/// Optional: nur fuer diese E-Mail-Adresse (klein geschrieben) einloesbar.
boundEmail String?
/// Interne Zuordnung, z. B. Name des Stammmodels. Nie oeffentlich.
internalNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
redemptions VoucherRedemption[]
bookings Booking[]
}
enum RedemptionStatus {
RESERVED
COMPLETED
RELEASED
}
model VoucherRedemption {
id String @id @default(cuid())
voucherId String
voucher Voucher @relation(fields: [voucherId], references: [id], onDelete: Restrict)
bookingId String @unique
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
livemode Boolean
status RedemptionStatus @default(RESERVED)
releasedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([voucherId, livemode, status])
}
// ---------------------------------------------------------------------------
// Stripe-Ereignisse, E-Mail-Ausgang, Protokoll, Rate Limits
// ---------------------------------------------------------------------------
enum StripeEventStatus {
PROCESSING
PROCESSED
IGNORED
FAILED
}
model StripeEvent {
/// Stripe-Event-ID (evt_...) — Primaerschluessel macht die Verarbeitung
/// idempotent.
id String @id
type String
livemode Boolean
status StripeEventStatus @default(PROCESSING)
error String?
attempts Int @default(1)
receivedAt DateTime @default(now())
processedAt DateTime?
}
enum EmailStatus {
PENDING
SENT
FAILED
}
/// Transaktionaler Postausgang: E-Mails entstehen in derselben Transaktion
/// wie die Statusaenderung und werden danach (bzw. vom Worker) versendet.
model EmailMessage {
id String @id @default(cuid())
/// Verhindert doppelte Mails bei wiederholten Webhooks.
dedupeKey String @unique
bookingId String?
booking Booking? @relation(fields: [bookingId], references: [id])
toAddress String
subject String
textBody String
htmlBody String
status EmailStatus @default(PENDING)
attempts Int @default(0)
lastError String?
sentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, createdAt])
}
enum ActorType {
ADMIN
CUSTOMER
STRIPE
WORKER
SYSTEM
}
/// Nachvollziehbarkeit fuer Bestands-, Buchungs- und Zahlungsaktionen.
model AuditLog {
id String @id @default(cuid())
createdAt DateTime @default(now())
actorType ActorType
actorId String?
action String
entityType String
entityId String
details Json?
@@index([entityType, entityId])
@@index([createdAt])
}
/// Gemeinsamer Zaehler fuer Rate Limits — funktioniert ueber mehrere
/// Replikate hinweg. Der Schluessel enthaelt nur einen Hash der IP.
model RateLimitBucket {
key String @id
windowStart DateTime
count Int
}
// ---------------------------------------------------------------------------
// Better Auth (Adminbereich)
// ---------------------------------------------------------------------------
model User {
id String @id
name String
email String @unique
emailVerified Boolean @default(false)
image String?
twoFactorEnabled Boolean? @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]
twofactors TwoFactor[]
@@map("user")
}
model Session {
id String @id
expiresAt DateTime
token String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("session")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@map("account")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([identifier])
@@map("verification")
}
model TwoFactor {
id String @id
secret String
backupCodes String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
verified Boolean? @default(true)
failedVerificationCount Int? @default(0)
lockedUntil DateTime?
@@index([secret])
@@index([userId])
@@map("twoFactor")
}
/// Rate Limits der Anmeldung (Better Auth, Modellname vom Adapter vorgegeben).
model RateLimit {
id String @id
key String @unique
count Int
lastRequest BigInt
@@map("rateLimit")
}