feat: schema for finalist confirmation flow + per-category quotas
Adds 4 new models for grand-finale logistics PR 1: - FinalistSlotQuota: per-category mutable quotas - WaitlistEntry: ranked per-category waitlist - FinalistConfirmation: token-gated confirmation lifecycle (PENDING / CONFIRMED / DECLINED / EXPIRED / SUPERSEDED) with optional decline reason - AttendingMember: who from each team is attending, with visa flag Plus Program.defaultAttendeeCap (default 3) for the per-edition team attendance cap. Migration is purely additive: no DROP/ALTER COLUMN/RENAME on existing schema. All FKs ON DELETE CASCADE only fire on parent deletion.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WaitlistEntryStatus" AS ENUM ('WAITING', 'PROMOTED', 'USED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FinalistConfirmationStatus" AS ENUM ('PENDING', 'CONFIRMED', 'DECLINED', 'EXPIRED', 'SUPERSEDED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Program" ADD COLUMN "defaultAttendeeCap" INTEGER NOT NULL DEFAULT 3;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "FinalistSlotQuota" (
|
||||
"id" TEXT NOT NULL,
|
||||
"programId" TEXT NOT NULL,
|
||||
"category" "CompetitionCategory" NOT NULL,
|
||||
"quota" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "FinalistSlotQuota_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WaitlistEntry" (
|
||||
"id" TEXT NOT NULL,
|
||||
"programId" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"category" "CompetitionCategory" NOT NULL,
|
||||
"rank" INTEGER NOT NULL,
|
||||
"status" "WaitlistEntryStatus" NOT NULL DEFAULT 'WAITING',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WaitlistEntry_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "FinalistConfirmation" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"category" "CompetitionCategory" NOT NULL,
|
||||
"status" "FinalistConfirmationStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"deadline" TIMESTAMP(3) NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"confirmedAt" TIMESTAMP(3),
|
||||
"declinedAt" TIMESTAMP(3),
|
||||
"declineReason" TEXT,
|
||||
"expiredAt" TIMESTAMP(3),
|
||||
"promotedFromWaitlistEntryId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "FinalistConfirmation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AttendingMember" (
|
||||
"id" TEXT NOT NULL,
|
||||
"confirmationId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"needsVisa" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AttendingMember_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "FinalistSlotQuota_programId_idx" ON "FinalistSlotQuota"("programId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "FinalistSlotQuota_programId_category_key" ON "FinalistSlotQuota"("programId", "category");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WaitlistEntry_projectId_key" ON "WaitlistEntry"("projectId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WaitlistEntry_programId_category_status_idx" ON "WaitlistEntry"("programId", "category", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WaitlistEntry_programId_category_rank_key" ON "WaitlistEntry"("programId", "category", "rank");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "FinalistConfirmation_projectId_key" ON "FinalistConfirmation"("projectId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "FinalistConfirmation_token_key" ON "FinalistConfirmation"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "FinalistConfirmation_promotedFromWaitlistEntryId_key" ON "FinalistConfirmation"("promotedFromWaitlistEntryId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "FinalistConfirmation_status_deadline_idx" ON "FinalistConfirmation"("status", "deadline");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "FinalistConfirmation_category_status_idx" ON "FinalistConfirmation"("category", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AttendingMember_userId_idx" ON "AttendingMember"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AttendingMember_confirmationId_userId_key" ON "AttendingMember"("confirmationId", "userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FinalistSlotQuota" ADD CONSTRAINT "FinalistSlotQuota_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WaitlistEntry" ADD CONSTRAINT "WaitlistEntry_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WaitlistEntry" ADD CONSTRAINT "WaitlistEntry_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FinalistConfirmation" ADD CONSTRAINT "FinalistConfirmation_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AttendingMember" ADD CONSTRAINT "AttendingMember_confirmationId_fkey" FOREIGN KEY ("confirmationId") REFERENCES "FinalistConfirmation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AttendingMember" ADD CONSTRAINT "AttendingMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -426,6 +426,9 @@ model User {
|
||||
// AI Ranking
|
||||
rankingSnapshots RankingSnapshot[] @relation("TriggeredRankingSnapshots")
|
||||
|
||||
// Grand-finale logistics
|
||||
finalistAttendances AttendingMember[]
|
||||
|
||||
@@index([role])
|
||||
@@index([status])
|
||||
}
|
||||
@@ -483,6 +486,9 @@ model Program {
|
||||
description String?
|
||||
settingsJson Json? @db.JsonB
|
||||
|
||||
// Grand-finale logistics
|
||||
defaultAttendeeCap Int @default(3) // Max team members allowed at the grand finale per finalist team
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -496,6 +502,10 @@ model Program {
|
||||
mentorMilestones MentorMilestone[]
|
||||
competitions Competition[]
|
||||
|
||||
// Grand-finale logistics
|
||||
finalistSlotQuotas FinalistSlotQuota[]
|
||||
waitlistEntries WaitlistEntry[]
|
||||
|
||||
@@unique([name, year])
|
||||
@@index([status])
|
||||
}
|
||||
@@ -637,6 +647,10 @@ model Project {
|
||||
submissionPromotions SubmissionPromotionEvent[]
|
||||
notificationLogs NotificationLog[]
|
||||
|
||||
// Grand-finale logistics
|
||||
waitlistEntry WaitlistEntry?
|
||||
finalistConfirmation FinalistConfirmation?
|
||||
|
||||
@@index([programId])
|
||||
@@index([status])
|
||||
@@index([tags])
|
||||
@@ -2626,3 +2640,89 @@ model ResultUnlockEvent {
|
||||
@@index([resultLockId])
|
||||
@@index([unlockedById])
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Grand-finale logistics (PR 1: finalist confirmation flow)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
enum WaitlistEntryStatus {
|
||||
WAITING // available for promotion
|
||||
PROMOTED // moved into a finalist slot
|
||||
USED // promoted and confirmation flow completed (declined or accepted)
|
||||
}
|
||||
|
||||
enum FinalistConfirmationStatus {
|
||||
PENDING // sent, awaiting team response
|
||||
CONFIRMED // team accepted, attendees selected
|
||||
DECLINED // team explicitly declined
|
||||
EXPIRED // deadline passed without response
|
||||
SUPERSEDED // admin manually overrode (e.g. unconfirmed to allow quota decrease)
|
||||
}
|
||||
|
||||
model FinalistSlotQuota {
|
||||
id String @id @default(cuid())
|
||||
programId String
|
||||
category CompetitionCategory
|
||||
quota Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([programId, category])
|
||||
@@index([programId])
|
||||
}
|
||||
|
||||
model WaitlistEntry {
|
||||
id String @id @default(cuid())
|
||||
programId String
|
||||
projectId String @unique
|
||||
category CompetitionCategory
|
||||
rank Int
|
||||
status WaitlistEntryStatus @default(WAITING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([programId, category, rank])
|
||||
@@index([programId, category, status])
|
||||
}
|
||||
|
||||
model FinalistConfirmation {
|
||||
id String @id @default(cuid())
|
||||
projectId String @unique
|
||||
category CompetitionCategory
|
||||
status FinalistConfirmationStatus @default(PENDING)
|
||||
deadline DateTime
|
||||
token String @unique
|
||||
confirmedAt DateTime?
|
||||
declinedAt DateTime?
|
||||
declineReason String? // optional free-text on decline
|
||||
expiredAt DateTime?
|
||||
promotedFromWaitlistEntryId String? @unique // null for original finalists
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
attendingMembers AttendingMember[]
|
||||
|
||||
@@index([status, deadline]) // for cron scan
|
||||
@@index([category, status])
|
||||
}
|
||||
|
||||
model AttendingMember {
|
||||
id String @id @default(cuid())
|
||||
confirmationId String
|
||||
userId String // must be a TeamMember of the same project (validated at write time)
|
||||
needsVisa Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
confirmation FinalistConfirmation @relation(fields: [confirmationId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([confirmationId, userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user