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:
@@ -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