diff --git a/prisma/migrations/20260522155652_multi_mentor_per_team/migration.sql b/prisma/migrations/20260522155652_multi_mentor_per_team/migration.sql new file mode 100644 index 0000000..52f10c7 --- /dev/null +++ b/prisma/migrations/20260522155652_multi_mentor_per_team/migration.sql @@ -0,0 +1,78 @@ +-- Hand-written migration for PR8 (multi-mentor per team). +-- +-- All DDL guarded with IF EXISTS / IF NOT EXISTS so the docker-entrypoint +-- retry loop is safe to re-run. No regex (the 2026-05-07 prod incident was +-- caused by Prisma 6 generating regex-based DDL that Postgres rejected). +-- No BEGIN/COMMIT blocks — Prisma wraps the migration in a transaction. + +-- Phase 1: MentorAssignment — drop unique, add composite, add notification field +ALTER TABLE "MentorAssignment" DROP CONSTRAINT IF EXISTS "MentorAssignment_projectId_key"; +DROP INDEX IF EXISTS "MentorAssignment_projectId_key"; +CREATE UNIQUE INDEX IF NOT EXISTS "MentorAssignment_projectId_mentorId_key" + ON "MentorAssignment"("projectId", "mentorId"); +CREATE INDEX IF NOT EXISTS "MentorAssignment_projectId_idx" + ON "MentorAssignment"("projectId"); +ALTER TABLE "MentorAssignment" ADD COLUMN IF NOT EXISTS "notificationSentAt" TIMESTAMP(3); + +-- Phase 2: MentorFile — re-scope to project (two-phase backfill) +ALTER TABLE "MentorFile" ADD COLUMN IF NOT EXISTS "projectId" TEXT; +UPDATE "MentorFile" mf + SET "projectId" = ma."projectId" + FROM "MentorAssignment" ma + WHERE mf."mentorAssignmentId" = ma."id" + AND mf."projectId" IS NULL; +ALTER TABLE "MentorFile" ALTER COLUMN "projectId" SET NOT NULL; +ALTER TABLE "MentorFile" DROP CONSTRAINT IF EXISTS "MentorFile_projectId_fkey"; +ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_projectId_fkey" + FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; +CREATE INDEX IF NOT EXISTS "MentorFile_projectId_idx" ON "MentorFile"("projectId"); + +-- Phase 2b: Make MentorFile.mentorAssignmentId nullable + switch its FK to SetNull +ALTER TABLE "MentorFile" ALTER COLUMN "mentorAssignmentId" DROP NOT NULL; +ALTER TABLE "MentorFile" DROP CONSTRAINT IF EXISTS "MentorFile_mentorAssignmentId_fkey"; +ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_mentorAssignmentId_fkey" + FOREIGN KEY ("mentorAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- Phase 3: MentorChangeRequest table +-- Postgres < 14 doesn't support CREATE TYPE ... IF NOT EXISTS, so wrap in a +-- DO block that swallows duplicate_object errors (idempotent for re-runs). +DO $$ BEGIN + CREATE TYPE "MentorChangeRequestStatus" AS ENUM ('PENDING', 'RESOLVED', 'DISMISSED'); +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; + +CREATE TABLE IF NOT EXISTS "MentorChangeRequest" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "targetAssignmentId" TEXT, + "requestedByUserId" TEXT, + "reason" TEXT NOT NULL, + "status" "MentorChangeRequestStatus" NOT NULL DEFAULT 'PENDING', + "resolvedByUserId" TEXT, + "resolvedAt" TIMESTAMP(3), + "resolutionNote" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "MentorChangeRequest_pkey" PRIMARY KEY ("id") +); + +ALTER TABLE "MentorChangeRequest" DROP CONSTRAINT IF EXISTS "MentorChangeRequest_projectId_fkey"; +ALTER TABLE "MentorChangeRequest" ADD CONSTRAINT "MentorChangeRequest_projectId_fkey" + FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "MentorChangeRequest" DROP CONSTRAINT IF EXISTS "MentorChangeRequest_targetAssignmentId_fkey"; +ALTER TABLE "MentorChangeRequest" ADD CONSTRAINT "MentorChangeRequest_targetAssignmentId_fkey" + FOREIGN KEY ("targetAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "MentorChangeRequest" DROP CONSTRAINT IF EXISTS "MentorChangeRequest_requestedByUserId_fkey"; +ALTER TABLE "MentorChangeRequest" ADD CONSTRAINT "MentorChangeRequest_requestedByUserId_fkey" + FOREIGN KEY ("requestedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "MentorChangeRequest" DROP CONSTRAINT IF EXISTS "MentorChangeRequest_resolvedByUserId_fkey"; +ALTER TABLE "MentorChangeRequest" ADD CONSTRAINT "MentorChangeRequest_resolvedByUserId_fkey" + FOREIGN KEY ("resolvedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +CREATE INDEX IF NOT EXISTS "MentorChangeRequest_projectId_idx" ON "MentorChangeRequest"("projectId"); +CREATE INDEX IF NOT EXISTS "MentorChangeRequest_status_idx" ON "MentorChangeRequest"("status"); +CREATE INDEX IF NOT EXISTS "MentorChangeRequest_targetAssignmentId_idx" ON "MentorChangeRequest"("targetAssignmentId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c30f01b..b448f7b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -118,7 +118,6 @@ enum NotificationChannel { NONE } - enum PartnerVisibility { ADMIN_ONLY JURY_VISIBLE @@ -133,7 +132,6 @@ enum PartnerType { OTHER } - // ============================================================================= // COMPETITION / ROUND ENGINE ENUMS // ============================================================================= @@ -171,7 +169,6 @@ enum ProjectRoundStateValue { WITHDRAWN } - enum CapMode { HARD SOFT @@ -328,8 +325,8 @@ model User { inviteTokenExpiresAt DateTime? // Password reset token - passwordResetToken String? @unique - passwordResetExpiresAt DateTime? + passwordResetToken String? @unique + passwordResetExpiresAt DateTime? // Digest & availability preferences digestFrequency String @default("none") // 'none' | 'daily' | 'weekly' @@ -363,9 +360,9 @@ model User { filteringOverrides FilteringResult[] @relation("FilteringOverriddenBy") // Award overrides - awardEligibilityOverrides AwardEligibility[] @relation("AwardEligibilityOverriddenBy") - awardEligibilityConfirms AwardEligibility[] @relation("AwardEligibilityConfirmer") - awardWinnerOverrides SpecialAward[] @relation("AwardOverriddenBy") + awardEligibilityOverrides AwardEligibility[] @relation("AwardEligibilityOverriddenBy") + awardEligibilityConfirms AwardEligibility[] @relation("AwardEligibilityConfirmer") + awardWinnerOverrides SpecialAward[] @relation("AwardOverriddenBy") // In-app notifications notifications InAppNotification[] @relation("UserNotifications") @@ -413,20 +410,24 @@ model User { sessions Session[] // ── Competition/Round architecture relations ── - juryGroupMemberships JuryGroupMember[] - mentorFilesUploaded MentorFile[] @relation("MentorFileUploader") - mentorFilesPromoted MentorFile[] @relation("MentorFilePromoter") - mentorFileComments MentorFileComment[] @relation("MentorFileCommentAuthor") - resultLocksCreated ResultLock[] @relation("ResultLockCreator") - resultUnlockEvents ResultUnlockEvent[] @relation("ResultUnlocker") - submissionPromotions SubmissionPromotionEvent[] @relation("SubmissionPromoter") - deliberationReplacements DeliberationParticipant[] @relation("DeliberationReplacement") + juryGroupMemberships JuryGroupMember[] + mentorFilesUploaded MentorFile[] @relation("MentorFileUploader") + mentorFilesPromoted MentorFile[] @relation("MentorFilePromoter") + mentorFileComments MentorFileComment[] @relation("MentorFileCommentAuthor") + resultLocksCreated ResultLock[] @relation("ResultLockCreator") + resultUnlockEvents ResultUnlockEvent[] @relation("ResultUnlocker") + submissionPromotions SubmissionPromotionEvent[] @relation("SubmissionPromoter") + deliberationReplacements DeliberationParticipant[] @relation("DeliberationReplacement") // AI Ranking - rankingSnapshots RankingSnapshot[] @relation("TriggeredRankingSnapshots") + rankingSnapshots RankingSnapshot[] @relation("TriggeredRankingSnapshots") // Grand-finale logistics - finalistAttendances AttendingMember[] + finalistAttendances AttendingMember[] + + // Mentor change requests + mentorChangeRequestsRequested MentorChangeRequest[] @relation("MentorChangeRequester") + mentorChangeRequestsResolved MentorChangeRequest[] @relation("MentorChangeResolver") @@index([role]) @@index([status]) @@ -629,7 +630,9 @@ model Project { assignments Assignment[] submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull) teamMembers TeamMember[] - mentorAssignment MentorAssignment? + mentorAssignments MentorAssignment[] + mentorFiles MentorFile[] + mentorChangeRequests MentorChangeRequest[] filteringResults FilteringResult[] awardEligibilities AwardEligibility[] awardVotes AwardVote[] @@ -642,12 +645,12 @@ model Project { cohortProjects CohortProject[] // ── Competition/Round architecture relations ── - projectRoundStates ProjectRoundState[] - assignmentIntents AssignmentIntent[] - deliberationVotes DeliberationVote[] - deliberationResults DeliberationResult[] - submissionPromotions SubmissionPromotionEvent[] - notificationLogs NotificationLog[] + projectRoundStates ProjectRoundState[] + assignmentIntents AssignmentIntent[] + deliberationVotes DeliberationVote[] + deliberationResults DeliberationResult[] + submissionPromotions SubmissionPromotionEvent[] + notificationLogs NotificationLog[] // Grand-finale logistics waitlistEntry WaitlistEntry? @@ -699,9 +702,9 @@ model ProjectFile { // Document analysis (optional, populated by document-analyzer service) textPreview String? @db.Text // First ~2000 chars of extracted text - detectedLang String? // ISO 639-3 code (e.g. 'eng', 'fra', 'und') - langConfidence Float? // 0.0–1.0 confidence - analyzedAt DateTime? // When analysis last ran + detectedLang String? // ISO 639-3 code (e.g. 'eng', 'fra', 'und') + langConfidence Float? // 0.0–1.0 confidence + analyzedAt DateTime? // When analysis last ran // MinIO location bucket String @@ -714,7 +717,7 @@ model ProjectFile { replacedById String? // FK to the newer file that replaced this one // ── Competition/Round architecture fields ── - submissionWindowId String? // FK to SubmissionWindow + submissionWindowId String? // FK to SubmissionWindow submissionFileRequirementId String? // FK to SubmissionFileRequirement createdAt DateTime @default(now()) @@ -763,10 +766,10 @@ model Assignment { juryGroupId String? // Relations - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) - round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) - juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) + juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) evaluation Evaluation? conflictOfInterest ConflictOfInterest? @@ -1026,12 +1029,12 @@ model NotificationEmailSetting { // ============================================================================= model LearningResource { - id String @id @default(cuid()) - programId String? // null = global resource - title String - description String? @db.Text - contentJson Json? @db.JsonB // BlockNote document structure - accessJson Json? @db.JsonB // Fine-grained access rules + id String @id @default(cuid()) + programId String? // null = global resource + title String + description String? @db.Text + contentJson Json? @db.JsonB // BlockNote document structure + accessJson Json? @db.JsonB // Fine-grained access rules // File storage (for uploaded resources) fileName String? @@ -1270,7 +1273,7 @@ model TeamMember { model MentorAssignment { id String @id @default(cuid()) - projectId String @unique // One mentor per project + projectId String // Team can have multiple mentors; uniqueness enforced via composite below mentorId String // User with MENTOR role or expertise // Assignment tracking @@ -1278,6 +1281,9 @@ model MentorAssignment { assignedAt DateTime @default(now()) assignedBy String? // Admin who assigned + // Per-assignment email idempotency: stamped once the assignment notification email is sent. + notificationSentAt DateTime? + // AI assignment metadata aiConfidenceScore Float? expertiseMatchScore Float? @@ -1304,11 +1310,47 @@ model MentorAssignment { milestoneCompletions MentorMilestoneCompletion[] messages MentorMessage[] files MentorFile[] + changeRequests MentorChangeRequest[] @relation("MentorChangeRequestTarget") + @@unique([projectId, mentorId]) + @@index([projectId]) @@index([mentorId]) @@index([method]) } +// ============================================================================= +// MENTOR CHANGE REQUESTS +// ============================================================================= + +enum MentorChangeRequestStatus { + PENDING + RESOLVED + DISMISSED +} + +model MentorChangeRequest { + id String @id @default(cuid()) + projectId String + targetAssignmentId String? // Optional: a specific co-mentor the request is about + requestedByUserId String? + reason String @db.Text + status MentorChangeRequestStatus @default(PENDING) + resolvedByUserId String? + resolvedAt DateTime? + resolutionNote String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + targetAssignment MentorAssignment? @relation("MentorChangeRequestTarget", fields: [targetAssignmentId], references: [id], onDelete: SetNull) + requestedBy User? @relation("MentorChangeRequester", fields: [requestedByUserId], references: [id], onDelete: SetNull) + resolvedBy User? @relation("MentorChangeResolver", fields: [resolvedByUserId], references: [id]) + + @@index([projectId]) + @@index([status]) + @@index([targetAssignmentId]) +} + // ============================================================================= // FILTERING ROUND SYSTEM // ============================================================================= @@ -1443,17 +1485,17 @@ enum AssignmentJobStatus { // ============================================================================= enum RankingTriggerType { - MANUAL // Admin clicked "Run ranking" - AUTO // Auto-triggered by assignment completion + MANUAL // Admin clicked "Run ranking" + AUTO // Auto-triggered by assignment completion RETROACTIVE // Retroactive scan on deployment - QUICK // Quick-rank mode (no preview) + QUICK // Quick-rank mode (no preview) } enum RankingMode { - PREVIEW // Parsed rules shown to admin (not yet applied) + PREVIEW // Parsed rules shown to admin (not yet applied) CONFIRMED // Admin confirmed rules, ranking applied - QUICK // Quick-rank: parse + apply without preview - FORMULA // Formula-only: no LLM, pure math ranking + QUICK // Quick-rank: parse + apply without preview + FORMULA // Formula-only: no LLM, pure math ranking } enum RankingSnapshotStatus { @@ -1470,7 +1512,7 @@ model RankingSnapshot { roundId String // Trigger metadata - triggeredById String? // null = auto-triggered + triggeredById String? // null = auto-triggered triggerType RankingTriggerType @default(MANUAL) // Criteria used @@ -1599,7 +1641,7 @@ model SpecialAward { evaluationRoundId String? juryGroupId String? eligibilityMode AwardEligibilityMode @default(STAY_IN_MAIN) - decisionMode String? // "JURY_VOTE" | "ADMIN_DECISION" + decisionMode String? // "JURY_VOTE" | "ADMIN_DECISION" shortlistSize Int @default(10) // Eligibility job tracking @@ -1621,10 +1663,10 @@ model SpecialAward { votes AwardVote[] // Competition/Round architecture relations - competition Competition? @relation(fields: [competitionId], references: [id], onDelete: SetNull) - evaluationRound Round? @relation(fields: [evaluationRoundId], references: [id], onDelete: SetNull) - awardJuryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) - rounds Round[] @relation("AwardRounds") + competition Competition? @relation(fields: [competitionId], references: [id], onDelete: SetNull) + evaluationRound Round? @relation(fields: [evaluationRoundId], references: [id], onDelete: SetNull) + awardJuryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) + rounds Round[] @relation("AwardRounds") @@index([programId]) @@index([status]) @@ -1688,12 +1730,12 @@ model AwardJuror { } model AwardVote { - id String @id @default(cuid()) - awardId String - userId String - projectId String - rank Int? // For RANKED mode - justification String? @db.Text + id String @id @default(cuid()) + awardId String + userId String + projectId String + rank Int? // For RANKED mode + justification String? @db.Text votedAt DateTime @default(now()) // Relations @@ -1810,7 +1852,7 @@ model MentorMessage { createdAt DateTime @default(now()) // ── Competition/Round architecture fields ── - workspaceId String? // FK to MentorAssignment (used as workspace) + workspaceId String? // FK to MentorAssignment (used as workspace) senderRole MentorMessageRole? // Relations @@ -2146,9 +2188,9 @@ model Competition { status CompetitionStatus @default(DRAFT) // Competition-wide settings - categoryMode String @default("SHARED") - startupFinalistCount Int @default(3) - conceptFinalistCount Int @default(3) + categoryMode String @default("SHARED") + startupFinalistCount Int @default(3) + conceptFinalistCount Int @default(3) // Notification preferences notifyOnRoundAdvance Boolean @default(true) @@ -2159,7 +2201,7 @@ model Competition { updatedAt DateTime @updatedAt // Relations - program Program @relation(fields: [programId], references: [id], onDelete: Cascade) + program Program @relation(fields: [programId], references: [id], onDelete: Cascade) rounds Round[] juryGroups JuryGroup[] submissionWindows SubmissionWindow[] @@ -2204,10 +2246,10 @@ model Round { updatedAt DateTime @updatedAt // Relations - competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade) - specialAward SpecialAward? @relation("AwardRounds", fields: [specialAwardId], references: [id], onDelete: SetNull) - juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) - submissionWindow SubmissionWindow? @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull) + competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade) + specialAward SpecialAward? @relation("AwardRounds", fields: [specialAwardId], references: [id], onDelete: SetNull) + juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) + submissionWindow SubmissionWindow? @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull) projectRoundStates ProjectRoundState[] visibleSubmissionWindows RoundSubmissionVisibility[] assignmentIntents AssignmentIntent[] @@ -2226,7 +2268,7 @@ model Round { filteringResults FilteringResult[] filteringJobs FilteringJob[] assignmentJobs AssignmentJob[] - rankingSnapshots RankingSnapshot[] @relation("RoundRankingSnapshots") + rankingSnapshots RankingSnapshot[] @relation("RoundRankingSnapshots") reminderLogs ReminderLog[] evaluationSummaries EvaluationSummary[] evaluationDiscussions EvaluationDiscussion[] @@ -2272,7 +2314,7 @@ model ProjectRoundState { // ============================================================================= model JuryGroup { - id String @id @default(cuid()) + id String @id @default(cuid()) competitionId String name String slug String @@ -2330,8 +2372,8 @@ model JuryGroupMember { updatedAt DateTime @updatedAt // Relations - juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) assignmentIntents AssignmentIntent[] deliberationVotes DeliberationVote[] deliberationParticipations DeliberationParticipant[] @@ -2369,7 +2411,7 @@ model SubmissionWindow { updatedAt DateTime @updatedAt // Relations - competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade) + competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade) fileRequirements SubmissionFileRequirement[] projectFiles ProjectFile[] rounds Round[] @@ -2403,7 +2445,7 @@ model SubmissionFileRequirement { } model RoundSubmissionVisibility { - id String @id @default(cuid()) + id String @id @default(cuid()) roundId String submissionWindowId String canView Boolean @default(true) @@ -2448,8 +2490,9 @@ model AssignmentIntent { // ============================================================================= model MentorFile { - id String @id @default(cuid()) - mentorAssignmentId String + id String @id @default(cuid()) + projectId String // Primary access scope: files belong to the team + mentorAssignmentId String? // Nullable audit FK: which assignment uploaded; survives mentor drop uploadedByUserId String fileName String @@ -2468,13 +2511,15 @@ model MentorFile { createdAt DateTime @default(now()) // Relations - mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade) - uploadedBy User @relation("MentorFileUploader", fields: [uploadedByUserId], references: [id]) - promotedBy User? @relation("MentorFilePromoter", fields: [promotedByUserId], references: [id]) - promotedFile ProjectFile? @relation("PromotedFromMentorFile", fields: [promotedToFileId], references: [id], onDelete: SetNull) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + mentorAssignment MentorAssignment? @relation(fields: [mentorAssignmentId], references: [id], onDelete: SetNull) + uploadedBy User @relation("MentorFileUploader", fields: [uploadedByUserId], references: [id]) + promotedBy User? @relation("MentorFilePromoter", fields: [promotedByUserId], references: [id]) + promotedFile ProjectFile? @relation("PromotedFromMentorFile", fields: [promotedToFileId], references: [id], onDelete: SetNull) comments MentorFileComment[] promotionEvents SubmissionPromotionEvent[] + @@index([projectId]) @@index([mentorAssignmentId]) @@index([uploadedByUserId]) } @@ -2492,9 +2537,9 @@ model MentorFileComment { updatedAt DateTime @updatedAt // Relations - mentorFile MentorFile @relation(fields: [mentorFileId], references: [id], onDelete: Cascade) - author User @relation("MentorFileCommentAuthor", fields: [authorId], references: [id]) - parentComment MentorFileComment? @relation("CommentThread", fields: [parentCommentId], references: [id], onDelete: Cascade) + mentorFile MentorFile @relation(fields: [mentorFileId], references: [id], onDelete: Cascade) + author User @relation("MentorFileCommentAuthor", fields: [authorId], references: [id]) + parentComment MentorFileComment? @relation("CommentThread", fields: [parentCommentId], references: [id], onDelete: Cascade) replies MentorFileComment[] @relation("CommentThread") @@index([mentorFileId]) @@ -2503,14 +2548,14 @@ model MentorFileComment { } model SubmissionPromotionEvent { - id String @id @default(cuid()) + id String @id @default(cuid()) projectId String roundId String slotKey String sourceType SubmissionPromotionSource sourceFileId String? promotedById String - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)