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
|
// AI Ranking
|
||||||
rankingSnapshots RankingSnapshot[] @relation("TriggeredRankingSnapshots")
|
rankingSnapshots RankingSnapshot[] @relation("TriggeredRankingSnapshots")
|
||||||
|
|
||||||
|
// Grand-finale logistics
|
||||||
|
finalistAttendances AttendingMember[]
|
||||||
|
|
||||||
@@index([role])
|
@@index([role])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
}
|
}
|
||||||
@@ -483,6 +486,9 @@ model Program {
|
|||||||
description String?
|
description String?
|
||||||
settingsJson Json? @db.JsonB
|
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())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -496,6 +502,10 @@ model Program {
|
|||||||
mentorMilestones MentorMilestone[]
|
mentorMilestones MentorMilestone[]
|
||||||
competitions Competition[]
|
competitions Competition[]
|
||||||
|
|
||||||
|
// Grand-finale logistics
|
||||||
|
finalistSlotQuotas FinalistSlotQuota[]
|
||||||
|
waitlistEntries WaitlistEntry[]
|
||||||
|
|
||||||
@@unique([name, year])
|
@@unique([name, year])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
}
|
}
|
||||||
@@ -637,6 +647,10 @@ model Project {
|
|||||||
submissionPromotions SubmissionPromotionEvent[]
|
submissionPromotions SubmissionPromotionEvent[]
|
||||||
notificationLogs NotificationLog[]
|
notificationLogs NotificationLog[]
|
||||||
|
|
||||||
|
// Grand-finale logistics
|
||||||
|
waitlistEntry WaitlistEntry?
|
||||||
|
finalistConfirmation FinalistConfirmation?
|
||||||
|
|
||||||
@@index([programId])
|
@@index([programId])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
@@index([tags])
|
@@index([tags])
|
||||||
@@ -2626,3 +2640,89 @@ model ResultUnlockEvent {
|
|||||||
@@index([resultLockId])
|
@@index([resultLockId])
|
||||||
@@index([unlockedById])
|
@@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