Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s

Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 23:04:15 +01:00
parent 9ab4717f96
commit 6ca39c976b
349 changed files with 69938 additions and 28767 deletions

View File

@@ -169,27 +169,26 @@ async function runChecks(): Promise<CheckResult[]> {
// 12. Every FileRequirement has a valid stageId
const badFileReqs = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "FileRequirement" fr
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = fr."stageId")
WHERE NOT EXISTS (SELECT 1 FROM "Round" r WHERE r.id = fr."roundId")
`
const badFileReqCount = Number(badFileReqs[0]?.count ?? 0)
results.push({
name: 'Every FileRequirement references valid stage',
name: 'Every FileRequirement references valid round',
passed: badFileReqCount === 0,
details: badFileReqCount === 0
? 'All file requirements reference valid stages'
: `Found ${badFileReqCount} file requirements with invalid stage references`,
? 'All file requirements reference valid rounds'
: `Found ${badFileReqCount} file requirements with invalid round references`,
})
// 13. Count validation
const projectCountResult = await prisma.project.count()
const stageCount = await prisma.stage.count()
const trackCount = await prisma.track.count()
const pipelineCount = await prisma.pipeline.count()
const pssCount = await prisma.projectStageState.count()
const roundCount = await prisma.round.count()
const competitionCount = await prisma.competition.count()
const prsCount = await prisma.projectRoundState.count()
results.push({
name: 'Count validation',
passed: projectCountResult > 0 && stageCount > 0 && trackCount > 0,
details: `Pipelines: ${pipelineCount}, Tracks: ${trackCount}, Stages: ${stageCount}, Projects: ${projectCountResult}, StageStates: ${pssCount}`,
passed: projectCountResult > 0 && roundCount > 0 && competitionCount > 0,
details: `Competitions: ${competitionCount}, Rounds: ${roundCount}, Projects: ${projectCountResult}, RoundStates: ${prsCount}`,
})
return results

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +1,91 @@
-- Universal Apply Page: Make Project.roundId nullable and add programId FK
-- This migration enables projects to be submitted to a program/edition without being assigned to a specific round
-- NOTE: Written to be idempotent (safe to re-run if partially applied)
-- Step 1: Add Program.slug for edition-wide apply URLs (nullable for existing programs)
ALTER TABLE "Program" ADD COLUMN IF NOT EXISTS "slug" TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS "Program_slug_key" ON "Program"("slug");
-- Step 2: Add programId column (nullable initially to handle existing data)
ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "programId" TEXT;
-- Step 3: Backfill programId from existing round relationships
-- Only update rows where programId is still NULL (idempotent)
UPDATE "Project" p
SET "programId" = r."programId"
FROM "Round" r
WHERE p."roundId" = r.id
AND p."programId" IS NULL;
-- Step 4: Handle orphaned projects (no roundId = no way to derive programId)
-- Assign them to the first available program, or delete them if no program exists
DO $$
DECLARE
null_count INTEGER;
fallback_program_id TEXT;
BEGIN
SELECT COUNT(*) INTO null_count FROM "Project" WHERE "programId" IS NULL;
IF null_count > 0 THEN
SELECT id INTO fallback_program_id FROM "Program" ORDER BY "createdAt" ASC LIMIT 1;
IF fallback_program_id IS NOT NULL THEN
UPDATE "Project" SET "programId" = fallback_program_id WHERE "programId" IS NULL;
RAISE NOTICE 'Assigned % orphaned projects to fallback program %', null_count, fallback_program_id;
ELSE
DELETE FROM "Project" WHERE "programId" IS NULL;
RAISE NOTICE 'Deleted % orphaned projects (no program exists to assign them to)', null_count;
END IF;
END IF;
END $$;
-- Step 5: Make programId required (NOT NULL constraint) - safe if already NOT NULL
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Project' AND column_name = 'programId' AND is_nullable = 'YES'
) THEN
ALTER TABLE "Project" ALTER COLUMN "programId" SET NOT NULL;
END IF;
END $$;
-- Step 6: Add foreign key constraint for programId (skip if already exists)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Project_programId_fkey' AND table_name = 'Project'
) THEN
ALTER TABLE "Project" ADD CONSTRAINT "Project_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE;
END IF;
END $$;
-- Step 7: Make roundId nullable (allow projects without round assignment)
-- Safe: DROP NOT NULL is idempotent if already nullable
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Project' AND column_name = 'roundId' AND is_nullable = 'NO'
) THEN
ALTER TABLE "Project" ALTER COLUMN "roundId" DROP NOT NULL;
END IF;
END $$;
-- Step 8: Update round FK to SET NULL on delete (instead of CASCADE)
-- Projects should remain in the database if their round is deleted
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Project_roundId_fkey' AND table_name = 'Project'
) THEN
ALTER TABLE "Project" DROP CONSTRAINT "Project_roundId_fkey";
END IF;
ALTER TABLE "Project" ADD CONSTRAINT "Project_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL;
END $$;
-- Step 9: Add performance indexes
CREATE INDEX IF NOT EXISTS "Project_programId_idx" ON "Project"("programId");
CREATE INDEX IF NOT EXISTS "Project_programId_roundId_idx" ON "Project"("programId", "roundId");
-- Universal Apply Page: Make Project.roundId nullable and add programId FK
-- This migration enables projects to be submitted to a program/edition without being assigned to a specific round
-- NOTE: Written to be idempotent (safe to re-run if partially applied)
-- Step 1: Add Program.slug for edition-wide apply URLs (nullable for existing programs)
ALTER TABLE "Program" ADD COLUMN IF NOT EXISTS "slug" TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS "Program_slug_key" ON "Program"("slug");
-- Step 2: Add programId column (nullable initially to handle existing data)
ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "programId" TEXT;
-- Step 3: Backfill programId from existing round relationships
-- Only update rows where programId is still NULL (idempotent)
UPDATE "Project" p
SET "programId" = r."programId"
FROM "Round" r
WHERE p."roundId" = r.id
AND p."programId" IS NULL;
-- Step 4: Handle orphaned projects (no roundId = no way to derive programId)
-- Assign them to the first available program, or delete them if no program exists
DO $$
DECLARE
null_count INTEGER;
fallback_program_id TEXT;
BEGIN
SELECT COUNT(*) INTO null_count FROM "Project" WHERE "programId" IS NULL;
IF null_count > 0 THEN
SELECT id INTO fallback_program_id FROM "Program" ORDER BY "createdAt" ASC LIMIT 1;
IF fallback_program_id IS NOT NULL THEN
UPDATE "Project" SET "programId" = fallback_program_id WHERE "programId" IS NULL;
RAISE NOTICE 'Assigned % orphaned projects to fallback program %', null_count, fallback_program_id;
ELSE
DELETE FROM "Project" WHERE "programId" IS NULL;
RAISE NOTICE 'Deleted % orphaned projects (no program exists to assign them to)', null_count;
END IF;
END IF;
END $$;
-- Step 5: Make programId required (NOT NULL constraint) - safe if already NOT NULL
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Project' AND column_name = 'programId' AND is_nullable = 'YES'
) THEN
ALTER TABLE "Project" ALTER COLUMN "programId" SET NOT NULL;
END IF;
END $$;
-- Step 6: Add foreign key constraint for programId (skip if already exists)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Project_programId_fkey' AND table_name = 'Project'
) THEN
ALTER TABLE "Project" ADD CONSTRAINT "Project_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE;
END IF;
END $$;
-- Step 7: Make roundId nullable (allow projects without round assignment)
-- Safe: DROP NOT NULL is idempotent if already nullable
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Project' AND column_name = 'roundId' AND is_nullable = 'NO'
) THEN
ALTER TABLE "Project" ALTER COLUMN "roundId" DROP NOT NULL;
END IF;
END $$;
-- Step 8: Update round FK to SET NULL on delete (instead of CASCADE)
-- Projects should remain in the database if their round is deleted
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Project_roundId_fkey' AND table_name = 'Project'
) THEN
ALTER TABLE "Project" DROP CONSTRAINT "Project_roundId_fkey";
END IF;
ALTER TABLE "Project" ADD CONSTRAINT "Project_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL;
END $$;
-- Step 9: Add performance indexes
CREATE INDEX IF NOT EXISTS "Project_programId_idx" ON "Project"("programId");
CREATE INDEX IF NOT EXISTS "Project_programId_roundId_idx" ON "Project"("programId", "roundId");

View File

@@ -1,41 +1,41 @@
-- Reconciliation migration: Add missing foreign keys and indexes
-- The add_15_features migration omitted some FKs and indexes that the schema expects
-- This migration brings the database in line with the Prisma schema
-- =====================================================
-- Missing Foreign Keys
-- =====================================================
-- RoundTemplate → Program
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RoundTemplate → User (creator)
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Message → Round
ALTER TABLE "Message" ADD CONSTRAINT "Message_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- EvaluationDiscussion → Round
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ProjectFile → ProjectFile (self-relation for file versioning)
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_replacedById_fkey"
FOREIGN KEY ("replacedById") REFERENCES "ProjectFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- =====================================================
-- Missing Indexes
-- =====================================================
CREATE INDEX "RoundTemplate_roundType_idx" ON "RoundTemplate"("roundType");
CREATE INDEX "MentorNote_authorId_idx" ON "MentorNote"("authorId");
CREATE INDEX "MentorMilestoneCompletion_completedById_idx" ON "MentorMilestoneCompletion"("completedById");
CREATE INDEX "Webhook_createdById_idx" ON "Webhook"("createdById");
CREATE INDEX "WebhookDelivery_event_idx" ON "WebhookDelivery"("event");
CREATE INDEX "Message_roundId_idx" ON "Message"("roundId");
CREATE INDEX "EvaluationDiscussion_closedById_idx" ON "EvaluationDiscussion"("closedById");
CREATE INDEX "DiscussionComment_discussionId_idx" ON "DiscussionComment"("discussionId");
CREATE INDEX "DiscussionComment_userId_idx" ON "DiscussionComment"("userId");
-- Reconciliation migration: Add missing foreign keys and indexes
-- The add_15_features migration omitted some FKs and indexes that the schema expects
-- This migration brings the database in line with the Prisma schema
-- =====================================================
-- Missing Foreign Keys
-- =====================================================
-- RoundTemplate → Program
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RoundTemplate → User (creator)
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Message → Round
ALTER TABLE "Message" ADD CONSTRAINT "Message_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- EvaluationDiscussion → Round
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ProjectFile → ProjectFile (self-relation for file versioning)
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_replacedById_fkey"
FOREIGN KEY ("replacedById") REFERENCES "ProjectFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- =====================================================
-- Missing Indexes
-- =====================================================
CREATE INDEX "RoundTemplate_roundType_idx" ON "RoundTemplate"("roundType");
CREATE INDEX "MentorNote_authorId_idx" ON "MentorNote"("authorId");
CREATE INDEX "MentorMilestoneCompletion_completedById_idx" ON "MentorMilestoneCompletion"("completedById");
CREATE INDEX "Webhook_createdById_idx" ON "Webhook"("createdById");
CREATE INDEX "WebhookDelivery_event_idx" ON "WebhookDelivery"("event");
CREATE INDEX "Message_roundId_idx" ON "Message"("roundId");
CREATE INDEX "EvaluationDiscussion_closedById_idx" ON "EvaluationDiscussion"("closedById");
CREATE INDEX "DiscussionComment_discussionId_idx" ON "DiscussionComment"("discussionId");
CREATE INDEX "DiscussionComment_userId_idx" ON "DiscussionComment"("userId");

View File

@@ -1,13 +1,13 @@
-- Fix round deletion FK constraint errors
-- Add CASCADE on Evaluation.formId so deleting EvaluationForm cascades to Evaluations
-- Add SET NULL on ProjectFile.roundId so deleting Round nullifies the reference
-- AlterTable: Evaluation.formId -> onDelete CASCADE
ALTER TABLE "Evaluation" DROP CONSTRAINT "Evaluation_formId_fkey";
ALTER TABLE "Evaluation" ADD CONSTRAINT "Evaluation_formId_fkey"
FOREIGN KEY ("formId") REFERENCES "EvaluationForm"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable: ProjectFile.roundId -> onDelete SET NULL
ALTER TABLE "ProjectFile" DROP CONSTRAINT "ProjectFile_roundId_fkey";
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- Fix round deletion FK constraint errors
-- Add CASCADE on Evaluation.formId so deleting EvaluationForm cascades to Evaluations
-- Add SET NULL on ProjectFile.roundId so deleting Round nullifies the reference
-- AlterTable: Evaluation.formId -> onDelete CASCADE
ALTER TABLE "Evaluation" DROP CONSTRAINT "Evaluation_formId_fkey";
ALTER TABLE "Evaluation" ADD CONSTRAINT "Evaluation_formId_fkey"
FOREIGN KEY ("formId") REFERENCES "EvaluationForm"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable: ProjectFile.roundId -> onDelete SET NULL
ALTER TABLE "ProjectFile" DROP CONSTRAINT "ProjectFile_roundId_fkey";
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,30 +1,30 @@
-- CreateTable
CREATE TABLE "FileRequirement" (
"id" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"acceptedMimeTypes" TEXT[],
"maxSizeMB" INTEGER,
"isRequired" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FileRequirement_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "FileRequirement_roundId_idx" ON "FileRequirement"("roundId");
-- AddForeignKey
ALTER TABLE "FileRequirement" ADD CONSTRAINT "FileRequirement_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable: add requirementId to ProjectFile
ALTER TABLE "ProjectFile" ADD COLUMN "requirementId" TEXT;
-- CreateIndex
CREATE INDEX "ProjectFile_requirementId_idx" ON "ProjectFile"("requirementId");
-- AddForeignKey
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_requirementId_fkey" FOREIGN KEY ("requirementId") REFERENCES "FileRequirement"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- CreateTable
CREATE TABLE "FileRequirement" (
"id" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"acceptedMimeTypes" TEXT[],
"maxSizeMB" INTEGER,
"isRequired" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FileRequirement_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "FileRequirement_roundId_idx" ON "FileRequirement"("roundId");
-- AddForeignKey
ALTER TABLE "FileRequirement" ADD CONSTRAINT "FileRequirement_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable: add requirementId to ProjectFile
ALTER TABLE "ProjectFile" ADD COLUMN "requirementId" TEXT;
-- CreateIndex
CREATE INDEX "ProjectFile_requirementId_idx" ON "ProjectFile"("requirementId");
-- AddForeignKey
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_requirementId_fkey" FOREIGN KEY ("requirementId") REFERENCES "FileRequirement"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,129 +1,129 @@
-- Migration: Add all missing schema elements not covered by previous migrations
-- This brings the database fully in line with prisma/schema.prisma
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
-- =============================================================================
-- 1. MISSING TABLE: WizardTemplate
-- =============================================================================
CREATE TABLE IF NOT EXISTS "WizardTemplate" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"config" JSONB NOT NULL,
"isGlobal" BOOLEAN NOT NULL DEFAULT false,
"programId" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WizardTemplate_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "WizardTemplate_programId_idx" ON "WizardTemplate"("programId");
CREATE INDEX IF NOT EXISTS "WizardTemplate_isGlobal_idx" ON "WizardTemplate"("isGlobal");
DO $$ BEGIN
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- 2. MISSING COLUMNS ON SpecialAward: eligibility job tracking fields
-- =============================================================================
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStatus" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobTotal" INTEGER;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobDone" INTEGER;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobError" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStarted" TIMESTAMP(3);
-- =============================================================================
-- 3. Project.referralSource: Already in init migration. No action needed.
-- Round.slug: Already in init migration. No action needed.
-- =============================================================================
-- =============================================================================
-- 5. MISSING INDEXES
-- =============================================================================
-- 5a. Assignment: @@index([projectId, userId])
CREATE INDEX IF NOT EXISTS "Assignment_projectId_userId_idx" ON "Assignment"("projectId", "userId");
-- 5b. AuditLog: @@index([sessionId])
CREATE INDEX IF NOT EXISTS "AuditLog_sessionId_idx" ON "AuditLog"("sessionId");
-- 5c. ProjectFile: @@index([projectId, roundId])
CREATE INDEX IF NOT EXISTS "ProjectFile_projectId_roundId_idx" ON "ProjectFile"("projectId", "roundId");
-- 5d. MessageRecipient: @@index([userId])
CREATE INDEX IF NOT EXISTS "MessageRecipient_userId_idx" ON "MessageRecipient"("userId");
-- 5e. MessageRecipient: @@unique([messageId, userId, channel])
CREATE UNIQUE INDEX IF NOT EXISTS "MessageRecipient_messageId_userId_channel_key" ON "MessageRecipient"("messageId", "userId", "channel");
-- 5f. AwardEligibility: @@index([awardId, eligible]) - composite index
CREATE INDEX IF NOT EXISTS "AwardEligibility_awardId_eligible_idx" ON "AwardEligibility"("awardId", "eligible");
-- =============================================================================
-- 6. REMOVE STALE INDEX: Message_scheduledAt_idx
-- The schema does NOT have @@index([scheduledAt]) on Message.
-- The add_15_features migration created it, but the schema doesn't list it.
-- Leaving it as-is since it's harmless and could be useful.
-- =============================================================================
-- =============================================================================
-- 7. VERIFY: All models from add_15_features are present
-- DigestLog, RoundTemplate, MentorNote, MentorMilestone,
-- MentorMilestoneCompletion, Message, MessageTemplate, MessageRecipient,
-- Webhook, WebhookDelivery, EvaluationDiscussion, DiscussionComment
-- -> All confirmed created in 20260205223133_add_15_features migration.
-- -> All FKs confirmed in add_15_features + 20260208000000_add_missing_fks_indexes.
-- =============================================================================
-- =============================================================================
-- 8. VERIFY: Existing tables from init and subsequent migrations
-- All core tables (User, Account, Session, VerificationToken, Program, Round,
-- EvaluationForm, Project, ProjectFile, Assignment, Evaluation, GracePeriod,
-- SystemSettings, AuditLog, AIUsageLog, NotificationLog, InAppNotification,
-- NotificationEmailSetting, LearningResource, ResourceAccess, Partner,
-- ExpertiseTag, ProjectTag, LiveVotingSession, LiveVote, TeamMember,
-- MentorAssignment, FilteringRule, FilteringResult, FilteringJob,
-- AssignmentJob, TaggingJob, SpecialAward, AwardEligibility, AwardJuror,
-- AwardVote, ReminderLog, ConflictOfInterest, EvaluationSummary,
-- ProjectStatusHistory, MentorMessage, FileRequirement)
-- -> All confirmed present in migrations.
-- =============================================================================
-- =============================================================================
-- SUMMARY OF CHANGES IN THIS MIGRATION:
--
-- NEW TABLE:
-- - WizardTemplate (with programId FK, createdBy FK, indexes)
--
-- NEW COLUMNS:
-- - SpecialAward.eligibilityJobStatus (TEXT, nullable)
-- - SpecialAward.eligibilityJobTotal (INTEGER, nullable)
-- - SpecialAward.eligibilityJobDone (INTEGER, nullable)
-- - SpecialAward.eligibilityJobError (TEXT, nullable)
-- - SpecialAward.eligibilityJobStarted (TIMESTAMP, nullable)
--
-- NEW INDEXES:
-- - Assignment_projectId_userId_idx
-- - AuditLog_sessionId_idx
-- - ProjectFile_projectId_roundId_idx
-- - MessageRecipient_userId_idx
-- - MessageRecipient_messageId_userId_channel_key (UNIQUE)
-- - AwardEligibility_awardId_eligible_idx
-- - WizardTemplate_programId_idx
-- - WizardTemplate_isGlobal_idx
--
-- NEW FOREIGN KEYS:
-- - WizardTemplate_programId_fkey -> Program(id) ON DELETE CASCADE
-- - WizardTemplate_createdBy_fkey -> User(id) ON DELETE RESTRICT
-- =============================================================================
-- Migration: Add all missing schema elements not covered by previous migrations
-- This brings the database fully in line with prisma/schema.prisma
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
-- =============================================================================
-- 1. MISSING TABLE: WizardTemplate
-- =============================================================================
CREATE TABLE IF NOT EXISTS "WizardTemplate" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"config" JSONB NOT NULL,
"isGlobal" BOOLEAN NOT NULL DEFAULT false,
"programId" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WizardTemplate_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "WizardTemplate_programId_idx" ON "WizardTemplate"("programId");
CREATE INDEX IF NOT EXISTS "WizardTemplate_isGlobal_idx" ON "WizardTemplate"("isGlobal");
DO $$ BEGIN
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- 2. MISSING COLUMNS ON SpecialAward: eligibility job tracking fields
-- =============================================================================
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStatus" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobTotal" INTEGER;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobDone" INTEGER;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobError" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStarted" TIMESTAMP(3);
-- =============================================================================
-- 3. Project.referralSource: Already in init migration. No action needed.
-- Round.slug: Already in init migration. No action needed.
-- =============================================================================
-- =============================================================================
-- 5. MISSING INDEXES
-- =============================================================================
-- 5a. Assignment: @@index([projectId, userId])
CREATE INDEX IF NOT EXISTS "Assignment_projectId_userId_idx" ON "Assignment"("projectId", "userId");
-- 5b. AuditLog: @@index([sessionId])
CREATE INDEX IF NOT EXISTS "AuditLog_sessionId_idx" ON "AuditLog"("sessionId");
-- 5c. ProjectFile: @@index([projectId, roundId])
CREATE INDEX IF NOT EXISTS "ProjectFile_projectId_roundId_idx" ON "ProjectFile"("projectId", "roundId");
-- 5d. MessageRecipient: @@index([userId])
CREATE INDEX IF NOT EXISTS "MessageRecipient_userId_idx" ON "MessageRecipient"("userId");
-- 5e. MessageRecipient: @@unique([messageId, userId, channel])
CREATE UNIQUE INDEX IF NOT EXISTS "MessageRecipient_messageId_userId_channel_key" ON "MessageRecipient"("messageId", "userId", "channel");
-- 5f. AwardEligibility: @@index([awardId, eligible]) - composite index
CREATE INDEX IF NOT EXISTS "AwardEligibility_awardId_eligible_idx" ON "AwardEligibility"("awardId", "eligible");
-- =============================================================================
-- 6. REMOVE STALE INDEX: Message_scheduledAt_idx
-- The schema does NOT have @@index([scheduledAt]) on Message.
-- The add_15_features migration created it, but the schema doesn't list it.
-- Leaving it as-is since it's harmless and could be useful.
-- =============================================================================
-- =============================================================================
-- 7. VERIFY: All models from add_15_features are present
-- DigestLog, RoundTemplate, MentorNote, MentorMilestone,
-- MentorMilestoneCompletion, Message, MessageTemplate, MessageRecipient,
-- Webhook, WebhookDelivery, EvaluationDiscussion, DiscussionComment
-- -> All confirmed created in 20260205223133_add_15_features migration.
-- -> All FKs confirmed in add_15_features + 20260208000000_add_missing_fks_indexes.
-- =============================================================================
-- =============================================================================
-- 8. VERIFY: Existing tables from init and subsequent migrations
-- All core tables (User, Account, Session, VerificationToken, Program, Round,
-- EvaluationForm, Project, ProjectFile, Assignment, Evaluation, GracePeriod,
-- SystemSettings, AuditLog, AIUsageLog, NotificationLog, InAppNotification,
-- NotificationEmailSetting, LearningResource, ResourceAccess, Partner,
-- ExpertiseTag, ProjectTag, LiveVotingSession, LiveVote, TeamMember,
-- MentorAssignment, FilteringRule, FilteringResult, FilteringJob,
-- AssignmentJob, TaggingJob, SpecialAward, AwardEligibility, AwardJuror,
-- AwardVote, ReminderLog, ConflictOfInterest, EvaluationSummary,
-- ProjectStatusHistory, MentorMessage, FileRequirement)
-- -> All confirmed present in migrations.
-- =============================================================================
-- =============================================================================
-- SUMMARY OF CHANGES IN THIS MIGRATION:
--
-- NEW TABLE:
-- - WizardTemplate (with programId FK, createdBy FK, indexes)
--
-- NEW COLUMNS:
-- - SpecialAward.eligibilityJobStatus (TEXT, nullable)
-- - SpecialAward.eligibilityJobTotal (INTEGER, nullable)
-- - SpecialAward.eligibilityJobDone (INTEGER, nullable)
-- - SpecialAward.eligibilityJobError (TEXT, nullable)
-- - SpecialAward.eligibilityJobStarted (TIMESTAMP, nullable)
--
-- NEW INDEXES:
-- - Assignment_projectId_userId_idx
-- - AuditLog_sessionId_idx
-- - ProjectFile_projectId_roundId_idx
-- - MessageRecipient_userId_idx
-- - MessageRecipient_messageId_userId_channel_key (UNIQUE)
-- - AwardEligibility_awardId_eligible_idx
-- - WizardTemplate_programId_idx
-- - WizardTemplate_isGlobal_idx
--
-- NEW FOREIGN KEYS:
-- - WizardTemplate_programId_fkey -> Program(id) ON DELETE CASCADE
-- - WizardTemplate_createdBy_fkey -> User(id) ON DELETE RESTRICT
-- =============================================================================

View File

@@ -1,2 +1,2 @@
-- CreateIndex
CREATE INDEX "AwardVote_awardId_userId_idx" ON "AwardVote"("awardId", "userId");
-- CreateIndex
CREATE INDEX "AwardVote_awardId_userId_idx" ON "AwardVote"("awardId", "userId");

View File

@@ -1,99 +1,99 @@
-- Migration: Add live voting enhancements (criteria voting, audience voting, AudienceVoter)
-- Brings LiveVotingSession, LiveVote, and new AudienceVoter model in sync with schema.prisma
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
-- =============================================================================
-- 1. LiveVotingSession: Add criteria-based & audience voting columns
-- =============================================================================
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "votingMode" TEXT NOT NULL DEFAULT 'simple';
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "criteriaJson" JSONB;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingMode" TEXT NOT NULL DEFAULT 'disabled';
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceMaxFavorites" INTEGER NOT NULL DEFAULT 3;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceRequireId" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingDuration" INTEGER;
-- =============================================================================
-- 2. LiveVote: Add criteria scores, audience voter link, make userId nullable
-- =============================================================================
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "criterionScoresJson" JSONB;
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "audienceVoterId" TEXT;
-- Make userId nullable (was NOT NULL in init migration)
ALTER TABLE "LiveVote" ALTER COLUMN "userId" DROP NOT NULL;
-- =============================================================================
-- 3. AudienceVoter: New table for audience participation
-- =============================================================================
CREATE TABLE IF NOT EXISTS "AudienceVoter" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"identifier" TEXT,
"identifierType" TEXT,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AudienceVoter_pkey" PRIMARY KEY ("id")
);
-- Unique constraint on token
DO $$ BEGIN
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_token_key" UNIQUE ("token");
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Indexes
CREATE INDEX IF NOT EXISTS "AudienceVoter_sessionId_idx" ON "AudienceVoter"("sessionId");
CREATE INDEX IF NOT EXISTS "AudienceVoter_token_idx" ON "AudienceVoter"("token");
-- Foreign key: AudienceVoter.sessionId -> LiveVotingSession.id
DO $$ BEGIN
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_sessionId_fkey"
FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- 4. LiveVote: Foreign key and indexes for audienceVoterId
-- =============================================================================
CREATE INDEX IF NOT EXISTS "LiveVote_audienceVoterId_idx" ON "LiveVote"("audienceVoterId");
-- Foreign key: LiveVote.audienceVoterId -> AudienceVoter.id
DO $$ BEGIN
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_audienceVoterId_fkey"
FOREIGN KEY ("audienceVoterId") REFERENCES "AudienceVoter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Unique constraint: sessionId + projectId + audienceVoterId
DO $$ BEGIN
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_sessionId_projectId_audienceVoterId_key"
UNIQUE ("sessionId", "projectId", "audienceVoterId");
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- SUMMARY:
--
-- LiveVotingSession new columns:
-- - votingMode (TEXT, default 'simple')
-- - criteriaJson (JSONB, nullable)
-- - audienceVotingMode (TEXT, default 'disabled')
-- - audienceMaxFavorites (INTEGER, default 3)
-- - audienceRequireId (BOOLEAN, default false)
-- - audienceVotingDuration (INTEGER, nullable)
--
-- LiveVote changes:
-- - criterionScoresJson (JSONB, nullable) - new column
-- - audienceVoterId (TEXT, nullable) - new column
-- - userId changed from NOT NULL to nullable
-- - New unique: (sessionId, projectId, audienceVoterId)
-- - New index: audienceVoterId
-- - New FK: audienceVoterId -> AudienceVoter(id)
--
-- New table: AudienceVoter
-- - id, sessionId, token (unique), identifier, identifierType,
-- ipAddress, userAgent, createdAt
-- - FK: sessionId -> LiveVotingSession(id) CASCADE
-- =============================================================================
-- Migration: Add live voting enhancements (criteria voting, audience voting, AudienceVoter)
-- Brings LiveVotingSession, LiveVote, and new AudienceVoter model in sync with schema.prisma
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
-- =============================================================================
-- 1. LiveVotingSession: Add criteria-based & audience voting columns
-- =============================================================================
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "votingMode" TEXT NOT NULL DEFAULT 'simple';
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "criteriaJson" JSONB;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingMode" TEXT NOT NULL DEFAULT 'disabled';
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceMaxFavorites" INTEGER NOT NULL DEFAULT 3;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceRequireId" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingDuration" INTEGER;
-- =============================================================================
-- 2. LiveVote: Add criteria scores, audience voter link, make userId nullable
-- =============================================================================
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "criterionScoresJson" JSONB;
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "audienceVoterId" TEXT;
-- Make userId nullable (was NOT NULL in init migration)
ALTER TABLE "LiveVote" ALTER COLUMN "userId" DROP NOT NULL;
-- =============================================================================
-- 3. AudienceVoter: New table for audience participation
-- =============================================================================
CREATE TABLE IF NOT EXISTS "AudienceVoter" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"identifier" TEXT,
"identifierType" TEXT,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AudienceVoter_pkey" PRIMARY KEY ("id")
);
-- Unique constraint on token
DO $$ BEGIN
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_token_key" UNIQUE ("token");
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Indexes
CREATE INDEX IF NOT EXISTS "AudienceVoter_sessionId_idx" ON "AudienceVoter"("sessionId");
CREATE INDEX IF NOT EXISTS "AudienceVoter_token_idx" ON "AudienceVoter"("token");
-- Foreign key: AudienceVoter.sessionId -> LiveVotingSession.id
DO $$ BEGIN
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_sessionId_fkey"
FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- 4. LiveVote: Foreign key and indexes for audienceVoterId
-- =============================================================================
CREATE INDEX IF NOT EXISTS "LiveVote_audienceVoterId_idx" ON "LiveVote"("audienceVoterId");
-- Foreign key: LiveVote.audienceVoterId -> AudienceVoter.id
DO $$ BEGIN
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_audienceVoterId_fkey"
FOREIGN KEY ("audienceVoterId") REFERENCES "AudienceVoter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Unique constraint: sessionId + projectId + audienceVoterId
DO $$ BEGIN
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_sessionId_projectId_audienceVoterId_key"
UNIQUE ("sessionId", "projectId", "audienceVoterId");
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- SUMMARY:
--
-- LiveVotingSession new columns:
-- - votingMode (TEXT, default 'simple')
-- - criteriaJson (JSONB, nullable)
-- - audienceVotingMode (TEXT, default 'disabled')
-- - audienceMaxFavorites (INTEGER, default 3)
-- - audienceRequireId (BOOLEAN, default false)
-- - audienceVotingDuration (INTEGER, nullable)
--
-- LiveVote changes:
-- - criterionScoresJson (JSONB, nullable) - new column
-- - audienceVoterId (TEXT, nullable) - new column
-- - userId changed from NOT NULL to nullable
-- - New unique: (sessionId, projectId, audienceVoterId)
-- - New index: audienceVoterId
-- - New FK: audienceVoterId -> AudienceVoter(id)
--
-- New table: AudienceVoter
-- - id, sessionId, token (unique), identifier, identifierType,
-- ipAddress, userAgent, createdAt
-- - FK: sessionId -> LiveVotingSession(id) CASCADE
-- =============================================================================

View File

@@ -0,0 +1,572 @@
-- =============================================================================
-- Phase 0+1: Add Competition/Round Architecture (additive — no breaking changes)
-- =============================================================================
-- New enums, new tables, new optional columns on existing tables.
-- Old Pipeline/Track/Stage tables are untouched.
-- ─── New Enum Types ──────────────────────────────────────────────────────────
CREATE TYPE "CompetitionStatus" AS ENUM ('DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED');
CREATE TYPE "RoundType" AS ENUM ('INTAKE', 'FILTERING', 'EVALUATION', 'SUBMISSION', 'MENTORING', 'LIVE_FINAL', 'DELIBERATION');
CREATE TYPE "RoundStatus" AS ENUM ('ROUND_DRAFT', 'ROUND_ACTIVE', 'ROUND_CLOSED', 'ROUND_ARCHIVED');
CREATE TYPE "ProjectRoundStateValue" AS ENUM ('PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN');
CREATE TYPE "AdvancementRuleType" AS ENUM ('AUTO_ADVANCE', 'SCORE_THRESHOLD', 'TOP_N', 'ADMIN_SELECTION', 'AI_RECOMMENDED');
CREATE TYPE "CapMode" AS ENUM ('HARD', 'SOFT', 'NONE');
CREATE TYPE "DeadlinePolicy" AS ENUM ('HARD_DEADLINE', 'FLAG', 'GRACE');
CREATE TYPE "JuryGroupMemberRole" AS ENUM ('CHAIR', 'MEMBER', 'OBSERVER');
CREATE TYPE "AssignmentIntentSource" AS ENUM ('INVITE', 'ADMIN', 'SYSTEM');
CREATE TYPE "AssignmentIntentStatus" AS ENUM ('INTENT_PENDING', 'HONORED', 'OVERRIDDEN', 'EXPIRED', 'CANCELLED');
CREATE TYPE "MentorMessageRole" AS ENUM ('MENTOR_ROLE', 'APPLICANT_ROLE', 'ADMIN_ROLE');
CREATE TYPE "SubmissionPromotionSource" AS ENUM ('MENTOR_FILE', 'ADMIN_REPLACEMENT');
CREATE TYPE "DeliberationMode" AS ENUM ('SINGLE_WINNER_VOTE', 'FULL_RANKING');
CREATE TYPE "DeliberationStatus" AS ENUM ('DELIB_OPEN', 'VOTING', 'TALLYING', 'RUNOFF', 'DELIB_LOCKED');
CREATE TYPE "TieBreakMethod" AS ENUM ('TIE_RUNOFF', 'TIE_ADMIN_DECIDES', 'SCORE_FALLBACK');
CREATE TYPE "DeliberationParticipantStatus" AS ENUM ('REQUIRED', 'ABSENT_EXCUSED', 'REPLACED', 'REPLACEMENT_ACTIVE');
CREATE TYPE "AwardEligibilityMode" AS ENUM ('SEPARATE_POOL', 'STAY_IN_MAIN');
-- Add FEATURE_FLAGS to SettingCategory enum
ALTER TYPE "SettingCategory" ADD VALUE 'FEATURE_FLAGS';
-- ─── New Tables ──────────────────────────────────────────────────────────────
-- Competition (replaces Pipeline)
CREATE TABLE "Competition" (
"id" TEXT NOT NULL,
"programId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"status" "CompetitionStatus" NOT NULL DEFAULT 'DRAFT',
"categoryMode" TEXT NOT NULL DEFAULT 'SHARED',
"startupFinalistCount" INTEGER NOT NULL DEFAULT 3,
"conceptFinalistCount" INTEGER NOT NULL DEFAULT 3,
"notifyOnRoundAdvance" BOOLEAN NOT NULL DEFAULT true,
"notifyOnDeadlineApproach" BOOLEAN NOT NULL DEFAULT true,
"deadlineReminderDays" INTEGER[] DEFAULT ARRAY[7, 3, 1]::INTEGER[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Competition_pkey" PRIMARY KEY ("id")
);
-- Round (replaces Stage)
CREATE TABLE "Round" (
"id" TEXT NOT NULL,
"competitionId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"roundType" "RoundType" NOT NULL,
"status" "RoundStatus" NOT NULL DEFAULT 'ROUND_DRAFT',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"windowOpenAt" TIMESTAMP(3),
"windowCloseAt" TIMESTAMP(3),
"configJson" JSONB,
"purposeKey" TEXT,
"juryGroupId" TEXT,
"submissionWindowId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Round_pkey" PRIMARY KEY ("id")
);
-- ProjectRoundState
CREATE TABLE "ProjectRoundState" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"state" "ProjectRoundStateValue" NOT NULL DEFAULT 'PENDING',
"enteredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"exitedAt" TIMESTAMP(3),
"metadataJson" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProjectRoundState_pkey" PRIMARY KEY ("id")
);
-- AdvancementRule
CREATE TABLE "AdvancementRule" (
"id" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"targetRoundId" TEXT,
"ruleType" "AdvancementRuleType" NOT NULL,
"configJson" JSONB NOT NULL,
"isDefault" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AdvancementRule_pkey" PRIMARY KEY ("id")
);
-- JuryGroup
CREATE TABLE "JuryGroup" (
"id" TEXT NOT NULL,
"competitionId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"defaultMaxAssignments" INTEGER NOT NULL DEFAULT 20,
"defaultCapMode" "CapMode" NOT NULL DEFAULT 'SOFT',
"softCapBuffer" INTEGER NOT NULL DEFAULT 2,
"categoryQuotasEnabled" BOOLEAN NOT NULL DEFAULT false,
"defaultCategoryQuotas" JSONB,
"allowJurorCapAdjustment" BOOLEAN NOT NULL DEFAULT false,
"allowJurorRatioAdjustment" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "JuryGroup_pkey" PRIMARY KEY ("id")
);
-- JuryGroupMember
CREATE TABLE "JuryGroupMember" (
"id" TEXT NOT NULL,
"juryGroupId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"role" "JuryGroupMemberRole" NOT NULL DEFAULT 'MEMBER',
"joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"maxAssignmentsOverride" INTEGER,
"capModeOverride" "CapMode",
"categoryQuotasOverride" JSONB,
"preferredStartupRatio" DOUBLE PRECISION,
"availabilityNotes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "JuryGroupMember_pkey" PRIMARY KEY ("id")
);
-- SubmissionWindow
CREATE TABLE "SubmissionWindow" (
"id" TEXT NOT NULL,
"competitionId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"roundNumber" INTEGER NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"windowOpenAt" TIMESTAMP(3),
"windowCloseAt" TIMESTAMP(3),
"deadlinePolicy" "DeadlinePolicy" NOT NULL DEFAULT 'FLAG',
"graceHours" INTEGER,
"lockOnClose" BOOLEAN NOT NULL DEFAULT true,
"isLocked" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SubmissionWindow_pkey" PRIMARY KEY ("id")
);
-- SubmissionFileRequirement
CREATE TABLE "SubmissionFileRequirement" (
"id" TEXT NOT NULL,
"submissionWindowId" TEXT NOT NULL,
"label" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"mimeTypes" TEXT[],
"maxSizeMb" INTEGER,
"required" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SubmissionFileRequirement_pkey" PRIMARY KEY ("id")
);
-- RoundSubmissionVisibility
CREATE TABLE "RoundSubmissionVisibility" (
"id" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"submissionWindowId" TEXT NOT NULL,
"canView" BOOLEAN NOT NULL DEFAULT true,
"displayLabel" TEXT,
CONSTRAINT "RoundSubmissionVisibility_pkey" PRIMARY KEY ("id")
);
-- AssignmentIntent
CREATE TABLE "AssignmentIntent" (
"id" TEXT NOT NULL,
"juryGroupMemberId" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"source" "AssignmentIntentSource" NOT NULL,
"status" "AssignmentIntentStatus" NOT NULL DEFAULT 'INTENT_PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AssignmentIntent_pkey" PRIMARY KEY ("id")
);
-- AssignmentException
CREATE TABLE "AssignmentException" (
"id" TEXT NOT NULL,
"assignmentId" TEXT NOT NULL,
"reason" TEXT NOT NULL,
"overCapBy" INTEGER NOT NULL,
"approvedById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AssignmentException_pkey" PRIMARY KEY ("id")
);
-- MentorFile
CREATE TABLE "MentorFile" (
"id" TEXT NOT NULL,
"mentorAssignmentId" TEXT NOT NULL,
"uploadedByUserId" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"bucket" TEXT NOT NULL,
"objectKey" TEXT NOT NULL,
"description" TEXT,
"isPromoted" BOOLEAN NOT NULL DEFAULT false,
"promotedToFileId" TEXT,
"promotedAt" TIMESTAMP(3),
"promotedByUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "MentorFile_pkey" PRIMARY KEY ("id")
);
-- MentorFileComment
CREATE TABLE "MentorFileComment" (
"id" TEXT NOT NULL,
"mentorFileId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"content" TEXT NOT NULL,
"parentCommentId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "MentorFileComment_pkey" PRIMARY KEY ("id")
);
-- SubmissionPromotionEvent
CREATE TABLE "SubmissionPromotionEvent" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"slotKey" TEXT NOT NULL,
"sourceType" "SubmissionPromotionSource" NOT NULL,
"sourceFileId" TEXT,
"promotedById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SubmissionPromotionEvent_pkey" PRIMARY KEY ("id")
);
-- DeliberationSession
CREATE TABLE "DeliberationSession" (
"id" TEXT NOT NULL,
"competitionId" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"category" "CompetitionCategory" NOT NULL,
"mode" "DeliberationMode" NOT NULL,
"showCollectiveRankings" BOOLEAN NOT NULL DEFAULT false,
"showPriorJuryData" BOOLEAN NOT NULL DEFAULT false,
"status" "DeliberationStatus" NOT NULL,
"tieBreakMethod" "TieBreakMethod" NOT NULL,
"adminOverrideResult" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DeliberationSession_pkey" PRIMARY KEY ("id")
);
-- DeliberationVote
CREATE TABLE "DeliberationVote" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"juryMemberId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"rank" INTEGER,
"isWinnerPick" BOOLEAN NOT NULL DEFAULT false,
"runoffRound" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DeliberationVote_pkey" PRIMARY KEY ("id")
);
-- DeliberationResult
CREATE TABLE "DeliberationResult" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"finalRank" INTEGER NOT NULL,
"voteCount" INTEGER NOT NULL DEFAULT 0,
"isAdminOverridden" BOOLEAN NOT NULL DEFAULT false,
"overrideReason" TEXT,
CONSTRAINT "DeliberationResult_pkey" PRIMARY KEY ("id")
);
-- DeliberationParticipant
CREATE TABLE "DeliberationParticipant" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"status" "DeliberationParticipantStatus" NOT NULL,
"replacedById" TEXT,
CONSTRAINT "DeliberationParticipant_pkey" PRIMARY KEY ("id")
);
-- ResultLock
CREATE TABLE "ResultLock" (
"id" TEXT NOT NULL,
"competitionId" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"category" "CompetitionCategory" NOT NULL,
"lockedById" TEXT NOT NULL,
"resultSnapshot" JSONB NOT NULL,
"lockedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ResultLock_pkey" PRIMARY KEY ("id")
);
-- ResultUnlockEvent
CREATE TABLE "ResultUnlockEvent" (
"id" TEXT NOT NULL,
"resultLockId" TEXT NOT NULL,
"unlockedById" TEXT NOT NULL,
"reason" TEXT NOT NULL,
"unlockedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ResultUnlockEvent_pkey" PRIMARY KEY ("id")
);
-- ─── Add Columns to Existing Tables ──────────────────────────────────────────
-- Assignment: add juryGroupId
ALTER TABLE "Assignment" ADD COLUMN "juryGroupId" TEXT;
-- SpecialAward: add competition/round architecture fields
ALTER TABLE "SpecialAward" ADD COLUMN "competitionId" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN "evaluationRoundId" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN "juryGroupId" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN "eligibilityMode" "AwardEligibilityMode" NOT NULL DEFAULT 'STAY_IN_MAIN';
ALTER TABLE "SpecialAward" ADD COLUMN "decisionMode" TEXT;
-- MentorAssignment: add workspace fields
ALTER TABLE "MentorAssignment" ADD COLUMN "workspaceEnabled" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "MentorAssignment" ADD COLUMN "workspaceOpenAt" TIMESTAMP(3);
ALTER TABLE "MentorAssignment" ADD COLUMN "workspaceCloseAt" TIMESTAMP(3);
-- MentorMessage: add workspace fields
ALTER TABLE "MentorMessage" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "MentorMessage" ADD COLUMN "senderRole" "MentorMessageRole";
-- ProjectFile: add submission window link
ALTER TABLE "ProjectFile" ADD COLUMN "submissionWindowId" TEXT;
ALTER TABLE "ProjectFile" ADD COLUMN "submissionFileRequirementId" TEXT;
-- ─── Unique Constraints ──────────────────────────────────────────────────────
CREATE UNIQUE INDEX "Competition_slug_key" ON "Competition"("slug");
CREATE UNIQUE INDEX "Round_competitionId_slug_key" ON "Round"("competitionId", "slug");
CREATE UNIQUE INDEX "Round_competitionId_sortOrder_key" ON "Round"("competitionId", "sortOrder");
CREATE UNIQUE INDEX "ProjectRoundState_projectId_roundId_key" ON "ProjectRoundState"("projectId", "roundId");
CREATE UNIQUE INDEX "JuryGroup_competitionId_slug_key" ON "JuryGroup"("competitionId", "slug");
CREATE UNIQUE INDEX "JuryGroupMember_juryGroupId_userId_key" ON "JuryGroupMember"("juryGroupId", "userId");
CREATE UNIQUE INDEX "SubmissionWindow_competitionId_slug_key" ON "SubmissionWindow"("competitionId", "slug");
CREATE UNIQUE INDEX "SubmissionWindow_competitionId_roundNumber_key" ON "SubmissionWindow"("competitionId", "roundNumber");
CREATE UNIQUE INDEX "RoundSubmissionVisibility_roundId_submissionWindowId_key" ON "RoundSubmissionVisibility"("roundId", "submissionWindowId");
CREATE UNIQUE INDEX "AssignmentIntent_juryGroupMemberId_roundId_projectId_key" ON "AssignmentIntent"("juryGroupMemberId", "roundId", "projectId");
CREATE UNIQUE INDEX "MentorFile_promotedToFileId_key" ON "MentorFile"("promotedToFileId");
CREATE UNIQUE INDEX "DeliberationVote_sessionId_juryMemberId_projectId_runoffRo_key" ON "DeliberationVote"("sessionId", "juryMemberId", "projectId", "runoffRound");
CREATE UNIQUE INDEX "DeliberationResult_sessionId_projectId_key" ON "DeliberationResult"("sessionId", "projectId");
CREATE UNIQUE INDEX "DeliberationParticipant_sessionId_userId_key" ON "DeliberationParticipant"("sessionId", "userId");
CREATE UNIQUE INDEX "SubmissionFileRequirement_submissionWindowId_slug_key" ON "SubmissionFileRequirement"("submissionWindowId", "slug");
CREATE UNIQUE INDEX "AdvancementRule_roundId_sortOrder_key" ON "AdvancementRule"("roundId", "sortOrder");
-- ─── Indexes ─────────────────────────────────────────────────────────────────
-- Competition
CREATE INDEX "Competition_programId_idx" ON "Competition"("programId");
CREATE INDEX "Competition_status_idx" ON "Competition"("status");
-- Round
CREATE INDEX "Round_competitionId_idx" ON "Round"("competitionId");
CREATE INDEX "Round_roundType_idx" ON "Round"("roundType");
CREATE INDEX "Round_status_idx" ON "Round"("status");
-- ProjectRoundState
CREATE INDEX "ProjectRoundState_projectId_idx" ON "ProjectRoundState"("projectId");
CREATE INDEX "ProjectRoundState_roundId_idx" ON "ProjectRoundState"("roundId");
CREATE INDEX "ProjectRoundState_state_idx" ON "ProjectRoundState"("state");
-- AdvancementRule
CREATE INDEX "AdvancementRule_roundId_idx" ON "AdvancementRule"("roundId");
-- JuryGroup
CREATE INDEX "JuryGroup_competitionId_idx" ON "JuryGroup"("competitionId");
-- JuryGroupMember
CREATE INDEX "JuryGroupMember_juryGroupId_idx" ON "JuryGroupMember"("juryGroupId");
CREATE INDEX "JuryGroupMember_userId_idx" ON "JuryGroupMember"("userId");
-- SubmissionWindow
CREATE INDEX "SubmissionWindow_competitionId_idx" ON "SubmissionWindow"("competitionId");
-- SubmissionFileRequirement
CREATE INDEX "SubmissionFileRequirement_submissionWindowId_idx" ON "SubmissionFileRequirement"("submissionWindowId");
-- RoundSubmissionVisibility
CREATE INDEX "RoundSubmissionVisibility_roundId_idx" ON "RoundSubmissionVisibility"("roundId");
-- AssignmentIntent
CREATE INDEX "AssignmentIntent_roundId_idx" ON "AssignmentIntent"("roundId");
CREATE INDEX "AssignmentIntent_projectId_idx" ON "AssignmentIntent"("projectId");
CREATE INDEX "AssignmentIntent_status_idx" ON "AssignmentIntent"("status");
-- AssignmentException
CREATE INDEX "AssignmentException_assignmentId_idx" ON "AssignmentException"("assignmentId");
CREATE INDEX "AssignmentException_approvedById_idx" ON "AssignmentException"("approvedById");
-- MentorFile
CREATE INDEX "MentorFile_mentorAssignmentId_idx" ON "MentorFile"("mentorAssignmentId");
CREATE INDEX "MentorFile_uploadedByUserId_idx" ON "MentorFile"("uploadedByUserId");
-- MentorFileComment
CREATE INDEX "MentorFileComment_mentorFileId_idx" ON "MentorFileComment"("mentorFileId");
CREATE INDEX "MentorFileComment_authorId_idx" ON "MentorFileComment"("authorId");
CREATE INDEX "MentorFileComment_parentCommentId_idx" ON "MentorFileComment"("parentCommentId");
-- SubmissionPromotionEvent
CREATE INDEX "SubmissionPromotionEvent_projectId_idx" ON "SubmissionPromotionEvent"("projectId");
CREATE INDEX "SubmissionPromotionEvent_roundId_idx" ON "SubmissionPromotionEvent"("roundId");
CREATE INDEX "SubmissionPromotionEvent_sourceFileId_idx" ON "SubmissionPromotionEvent"("sourceFileId");
-- DeliberationSession
CREATE INDEX "DeliberationSession_competitionId_idx" ON "DeliberationSession"("competitionId");
CREATE INDEX "DeliberationSession_roundId_idx" ON "DeliberationSession"("roundId");
CREATE INDEX "DeliberationSession_status_idx" ON "DeliberationSession"("status");
-- DeliberationVote
CREATE INDEX "DeliberationVote_sessionId_idx" ON "DeliberationVote"("sessionId");
CREATE INDEX "DeliberationVote_juryMemberId_idx" ON "DeliberationVote"("juryMemberId");
CREATE INDEX "DeliberationVote_projectId_idx" ON "DeliberationVote"("projectId");
-- DeliberationResult
CREATE INDEX "DeliberationResult_sessionId_idx" ON "DeliberationResult"("sessionId");
CREATE INDEX "DeliberationResult_projectId_idx" ON "DeliberationResult"("projectId");
-- DeliberationParticipant
CREATE INDEX "DeliberationParticipant_sessionId_idx" ON "DeliberationParticipant"("sessionId");
CREATE INDEX "DeliberationParticipant_userId_idx" ON "DeliberationParticipant"("userId");
-- ResultLock
CREATE INDEX "ResultLock_competitionId_idx" ON "ResultLock"("competitionId");
CREATE INDEX "ResultLock_roundId_idx" ON "ResultLock"("roundId");
CREATE INDEX "ResultLock_category_idx" ON "ResultLock"("category");
-- ResultUnlockEvent
CREATE INDEX "ResultUnlockEvent_resultLockId_idx" ON "ResultUnlockEvent"("resultLockId");
CREATE INDEX "ResultUnlockEvent_unlockedById_idx" ON "ResultUnlockEvent"("unlockedById");
-- Indexes on modified existing tables
CREATE INDEX "Assignment_juryGroupId_idx" ON "Assignment"("juryGroupId");
CREATE INDEX "SpecialAward_competitionId_idx" ON "SpecialAward"("competitionId");
CREATE INDEX "SpecialAward_evaluationRoundId_idx" ON "SpecialAward"("evaluationRoundId");
CREATE INDEX "MentorMessage_workspaceId_idx" ON "MentorMessage"("workspaceId");
CREATE INDEX "ProjectFile_submissionWindowId_idx" ON "ProjectFile"("submissionWindowId");
CREATE INDEX "ProjectFile_submissionFileRequirementId_idx" ON "ProjectFile"("submissionFileRequirementId");
-- ─── Foreign Keys ────────────────────────────────────────────────────────────
-- Competition
ALTER TABLE "Competition" ADD CONSTRAINT "Competition_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Round
ALTER TABLE "Round" ADD CONSTRAINT "Round_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Round" ADD CONSTRAINT "Round_juryGroupId_fkey" FOREIGN KEY ("juryGroupId") REFERENCES "JuryGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "Round" ADD CONSTRAINT "Round_submissionWindowId_fkey" FOREIGN KEY ("submissionWindowId") REFERENCES "SubmissionWindow"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- ProjectRoundState
ALTER TABLE "ProjectRoundState" ADD CONSTRAINT "ProjectRoundState_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProjectRoundState" ADD CONSTRAINT "ProjectRoundState_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AdvancementRule
ALTER TABLE "AdvancementRule" ADD CONSTRAINT "AdvancementRule_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- JuryGroup
ALTER TABLE "JuryGroup" ADD CONSTRAINT "JuryGroup_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- JuryGroupMember
ALTER TABLE "JuryGroupMember" ADD CONSTRAINT "JuryGroupMember_juryGroupId_fkey" FOREIGN KEY ("juryGroupId") REFERENCES "JuryGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "JuryGroupMember" ADD CONSTRAINT "JuryGroupMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- SubmissionWindow
ALTER TABLE "SubmissionWindow" ADD CONSTRAINT "SubmissionWindow_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- SubmissionFileRequirement
ALTER TABLE "SubmissionFileRequirement" ADD CONSTRAINT "SubmissionFileRequirement_submissionWindowId_fkey" FOREIGN KEY ("submissionWindowId") REFERENCES "SubmissionWindow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RoundSubmissionVisibility
ALTER TABLE "RoundSubmissionVisibility" ADD CONSTRAINT "RoundSubmissionVisibility_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "RoundSubmissionVisibility" ADD CONSTRAINT "RoundSubmissionVisibility_submissionWindowId_fkey" FOREIGN KEY ("submissionWindowId") REFERENCES "SubmissionWindow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AssignmentIntent
ALTER TABLE "AssignmentIntent" ADD CONSTRAINT "AssignmentIntent_juryGroupMemberId_fkey" FOREIGN KEY ("juryGroupMemberId") REFERENCES "JuryGroupMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "AssignmentIntent" ADD CONSTRAINT "AssignmentIntent_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "AssignmentIntent" ADD CONSTRAINT "AssignmentIntent_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AssignmentException
ALTER TABLE "AssignmentException" ADD CONSTRAINT "AssignmentException_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "AssignmentException" ADD CONSTRAINT "AssignmentException_approvedById_fkey" FOREIGN KEY ("approvedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- MentorFile
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_mentorAssignmentId_fkey" FOREIGN KEY ("mentorAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_uploadedByUserId_fkey" FOREIGN KEY ("uploadedByUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_promotedByUserId_fkey" FOREIGN KEY ("promotedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_promotedToFileId_fkey" FOREIGN KEY ("promotedToFileId") REFERENCES "ProjectFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- MentorFileComment
ALTER TABLE "MentorFileComment" ADD CONSTRAINT "MentorFileComment_mentorFileId_fkey" FOREIGN KEY ("mentorFileId") REFERENCES "MentorFile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "MentorFileComment" ADD CONSTRAINT "MentorFileComment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "MentorFileComment" ADD CONSTRAINT "MentorFileComment_parentCommentId_fkey" FOREIGN KEY ("parentCommentId") REFERENCES "MentorFileComment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- SubmissionPromotionEvent
ALTER TABLE "SubmissionPromotionEvent" ADD CONSTRAINT "SubmissionPromotionEvent_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SubmissionPromotionEvent" ADD CONSTRAINT "SubmissionPromotionEvent_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SubmissionPromotionEvent" ADD CONSTRAINT "SubmissionPromotionEvent_sourceFileId_fkey" FOREIGN KEY ("sourceFileId") REFERENCES "MentorFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SubmissionPromotionEvent" ADD CONSTRAINT "SubmissionPromotionEvent_promotedById_fkey" FOREIGN KEY ("promotedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- DeliberationSession
ALTER TABLE "DeliberationSession" ADD CONSTRAINT "DeliberationSession_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DeliberationSession" ADD CONSTRAINT "DeliberationSession_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- DeliberationVote
ALTER TABLE "DeliberationVote" ADD CONSTRAINT "DeliberationVote_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "DeliberationSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DeliberationVote" ADD CONSTRAINT "DeliberationVote_juryMemberId_fkey" FOREIGN KEY ("juryMemberId") REFERENCES "JuryGroupMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DeliberationVote" ADD CONSTRAINT "DeliberationVote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- DeliberationResult
ALTER TABLE "DeliberationResult" ADD CONSTRAINT "DeliberationResult_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "DeliberationSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DeliberationResult" ADD CONSTRAINT "DeliberationResult_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- DeliberationParticipant
ALTER TABLE "DeliberationParticipant" ADD CONSTRAINT "DeliberationParticipant_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "DeliberationSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DeliberationParticipant" ADD CONSTRAINT "DeliberationParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "JuryGroupMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DeliberationParticipant" ADD CONSTRAINT "DeliberationParticipant_replacedById_fkey" FOREIGN KEY ("replacedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- ResultLock
ALTER TABLE "ResultLock" ADD CONSTRAINT "ResultLock_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ResultLock" ADD CONSTRAINT "ResultLock_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ResultLock" ADD CONSTRAINT "ResultLock_lockedById_fkey" FOREIGN KEY ("lockedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- ResultUnlockEvent
ALTER TABLE "ResultUnlockEvent" ADD CONSTRAINT "ResultUnlockEvent_resultLockId_fkey" FOREIGN KEY ("resultLockId") REFERENCES "ResultLock"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ResultUnlockEvent" ADD CONSTRAINT "ResultUnlockEvent_unlockedById_fkey" FOREIGN KEY ("unlockedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- FKs on modified existing tables
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_juryGroupId_fkey" FOREIGN KEY ("juryGroupId") REFERENCES "JuryGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_evaluationRoundId_fkey" FOREIGN KEY ("evaluationRoundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_juryGroupId_fkey" FOREIGN KEY ("juryGroupId") REFERENCES "JuryGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "MentorMessage" ADD CONSTRAINT "MentorMessage_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "MentorAssignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_submissionWindowId_fkey" FOREIGN KEY ("submissionWindowId") REFERENCES "SubmissionWindow"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_submissionFileRequirementId_fkey" FOREIGN KEY ("submissionFileRequirementId") REFERENCES "SubmissionFileRequirement"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "JuryGroupMember" ADD COLUMN "selfServiceCap" INTEGER,
ADD COLUMN "selfServiceRatio" DOUBLE PRECISION;

View File

@@ -0,0 +1,300 @@
-- =============================================================================
-- Phase 7/8 Migration Part 1: Rename stageId → roundId on 15 tables
-- =============================================================================
-- This migration renames stageId columns to roundId and updates FK constraints
-- to point to the Round table instead of Stage table.
-- ─── 1. EvaluationForm ───────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "EvaluationForm" DROP CONSTRAINT IF EXISTS "EvaluationForm_stageId_fkey";
-- Drop indexes
DROP INDEX IF EXISTS "EvaluationForm_stageId_version_key";
DROP INDEX IF EXISTS "EvaluationForm_stageId_isActive_idx";
-- Rename column
ALTER TABLE "EvaluationForm" RENAME COLUMN "stageId" TO "roundId";
-- Recreate indexes with new name
CREATE UNIQUE INDEX "EvaluationForm_roundId_version_key" ON "EvaluationForm"("roundId", "version");
CREATE INDEX "EvaluationForm_roundId_isActive_idx" ON "EvaluationForm"("roundId", "isActive");
-- Recreate FK pointing to Round
ALTER TABLE "EvaluationForm" ADD CONSTRAINT "EvaluationForm_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 2. FileRequirement ──────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "FileRequirement" DROP CONSTRAINT IF EXISTS "FileRequirement_stageId_fkey";
-- Drop index
DROP INDEX IF EXISTS "FileRequirement_stageId_idx";
-- Rename column
ALTER TABLE "FileRequirement" RENAME COLUMN "stageId" TO "roundId";
-- Recreate index
CREATE INDEX "FileRequirement_roundId_idx" ON "FileRequirement"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "FileRequirement" ADD CONSTRAINT "FileRequirement_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 3. Assignment ───────────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "Assignment" DROP CONSTRAINT IF EXISTS "Assignment_stageId_fkey";
-- Drop indexes and unique constraint
DROP INDEX IF EXISTS "Assignment_userId_projectId_stageId_key";
DROP INDEX IF EXISTS "Assignment_stageId_idx";
-- Rename column
ALTER TABLE "Assignment" RENAME COLUMN "stageId" TO "roundId";
-- Recreate unique constraint and index with new name
CREATE UNIQUE INDEX "Assignment_userId_projectId_roundId_key" ON "Assignment"("userId", "projectId", "roundId");
CREATE INDEX "Assignment_roundId_idx" ON "Assignment"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 4. GracePeriod ──────────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "GracePeriod" DROP CONSTRAINT IF EXISTS "GracePeriod_stageId_fkey";
-- Drop indexes
DROP INDEX IF EXISTS "GracePeriod_stageId_idx";
DROP INDEX IF EXISTS "GracePeriod_stageId_userId_extendedUntil_idx";
-- Rename column
ALTER TABLE "GracePeriod" RENAME COLUMN "stageId" TO "roundId";
-- Recreate indexes
CREATE INDEX "GracePeriod_roundId_idx" ON "GracePeriod"("roundId");
CREATE INDEX "GracePeriod_roundId_userId_extendedUntil_idx" ON "GracePeriod"("roundId", "userId", "extendedUntil");
-- Recreate FK pointing to Round
ALTER TABLE "GracePeriod" ADD CONSTRAINT "GracePeriod_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 5. LiveVotingSession ────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "LiveVotingSession" DROP CONSTRAINT IF EXISTS "LiveVotingSession_stageId_fkey";
-- Drop unique index
DROP INDEX IF EXISTS "LiveVotingSession_stageId_key";
-- Rename column
ALTER TABLE "LiveVotingSession" RENAME COLUMN "stageId" TO "roundId";
-- Recreate unique index
CREATE UNIQUE INDEX "LiveVotingSession_roundId_key" ON "LiveVotingSession"("roundId");
-- Recreate FK pointing to Round (nullable)
ALTER TABLE "LiveVotingSession" ADD CONSTRAINT "LiveVotingSession_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 6. FilteringRule ────────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "FilteringRule" DROP CONSTRAINT IF EXISTS "FilteringRule_stageId_fkey";
-- Drop index
DROP INDEX IF EXISTS "FilteringRule_stageId_idx";
-- Rename column
ALTER TABLE "FilteringRule" RENAME COLUMN "stageId" TO "roundId";
-- Recreate index
CREATE INDEX "FilteringRule_roundId_idx" ON "FilteringRule"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "FilteringRule" ADD CONSTRAINT "FilteringRule_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 7. FilteringResult ──────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "FilteringResult" DROP CONSTRAINT IF EXISTS "FilteringResult_stageId_fkey";
-- Drop indexes and unique constraint
DROP INDEX IF EXISTS "FilteringResult_stageId_projectId_key";
DROP INDEX IF EXISTS "FilteringResult_stageId_idx";
-- Rename column
ALTER TABLE "FilteringResult" RENAME COLUMN "stageId" TO "roundId";
-- Recreate unique constraint and index
CREATE UNIQUE INDEX "FilteringResult_roundId_projectId_key" ON "FilteringResult"("roundId", "projectId");
CREATE INDEX "FilteringResult_roundId_idx" ON "FilteringResult"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "FilteringResult" ADD CONSTRAINT "FilteringResult_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 8. FilteringJob ─────────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "FilteringJob" DROP CONSTRAINT IF EXISTS "FilteringJob_stageId_fkey";
-- Drop index
DROP INDEX IF EXISTS "FilteringJob_stageId_idx";
-- Rename column
ALTER TABLE "FilteringJob" RENAME COLUMN "stageId" TO "roundId";
-- Recreate index
CREATE INDEX "FilteringJob_roundId_idx" ON "FilteringJob"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "FilteringJob" ADD CONSTRAINT "FilteringJob_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 9. AssignmentJob ────────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "AssignmentJob" DROP CONSTRAINT IF EXISTS "AssignmentJob_stageId_fkey";
-- Drop index
DROP INDEX IF EXISTS "AssignmentJob_stageId_idx";
-- Rename column
ALTER TABLE "AssignmentJob" RENAME COLUMN "stageId" TO "roundId";
-- Recreate index
CREATE INDEX "AssignmentJob_roundId_idx" ON "AssignmentJob"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "AssignmentJob" ADD CONSTRAINT "AssignmentJob_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 10. ReminderLog ─────────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "ReminderLog" DROP CONSTRAINT IF EXISTS "ReminderLog_stageId_fkey";
-- Drop indexes and unique constraint
DROP INDEX IF EXISTS "ReminderLog_stageId_userId_type_key";
DROP INDEX IF EXISTS "ReminderLog_stageId_idx";
-- Rename column
ALTER TABLE "ReminderLog" RENAME COLUMN "stageId" TO "roundId";
-- Recreate unique constraint and index
CREATE UNIQUE INDEX "ReminderLog_roundId_userId_type_key" ON "ReminderLog"("roundId", "userId", "type");
CREATE INDEX "ReminderLog_roundId_idx" ON "ReminderLog"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "ReminderLog" ADD CONSTRAINT "ReminderLog_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 11. EvaluationSummary ───────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "EvaluationSummary" DROP CONSTRAINT IF EXISTS "EvaluationSummary_stageId_fkey";
-- Drop indexes and unique constraint
DROP INDEX IF EXISTS "EvaluationSummary_projectId_stageId_key";
DROP INDEX IF EXISTS "EvaluationSummary_stageId_idx";
-- Rename column
ALTER TABLE "EvaluationSummary" RENAME COLUMN "stageId" TO "roundId";
-- Recreate unique constraint and index
CREATE UNIQUE INDEX "EvaluationSummary_projectId_roundId_key" ON "EvaluationSummary"("projectId", "roundId");
CREATE INDEX "EvaluationSummary_roundId_idx" ON "EvaluationSummary"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "EvaluationSummary" ADD CONSTRAINT "EvaluationSummary_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 12. EvaluationDiscussion ────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "EvaluationDiscussion" DROP CONSTRAINT IF EXISTS "EvaluationDiscussion_stageId_fkey";
-- Drop indexes and unique constraint
DROP INDEX IF EXISTS "EvaluationDiscussion_projectId_stageId_key";
DROP INDEX IF EXISTS "EvaluationDiscussion_stageId_idx";
-- Rename column
ALTER TABLE "EvaluationDiscussion" RENAME COLUMN "stageId" TO "roundId";
-- Recreate unique constraint and index
CREATE UNIQUE INDEX "EvaluationDiscussion_projectId_roundId_key" ON "EvaluationDiscussion"("projectId", "roundId");
CREATE INDEX "EvaluationDiscussion_roundId_idx" ON "EvaluationDiscussion"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 13. Message ─────────────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "Message" DROP CONSTRAINT IF EXISTS "Message_stageId_fkey";
-- Drop index
DROP INDEX IF EXISTS "Message_stageId_idx";
-- Rename column (nullable, so SET NULL on delete)
ALTER TABLE "Message" RENAME COLUMN "stageId" TO "roundId";
-- Recreate index
CREATE INDEX "Message_roundId_idx" ON "Message"("roundId");
-- Recreate FK pointing to Round with SET NULL
ALTER TABLE "Message" ADD CONSTRAINT "Message_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- ─── 14. Cohort ──────────────────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "Cohort" DROP CONSTRAINT IF EXISTS "Cohort_stageId_fkey";
-- Drop indexes
DROP INDEX IF EXISTS "Cohort_stageId_idx";
-- Rename column
ALTER TABLE "Cohort" RENAME COLUMN "stageId" TO "roundId";
-- Recreate index
CREATE INDEX "Cohort_roundId_idx" ON "Cohort"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "Cohort" ADD CONSTRAINT "Cohort_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 15. LiveProgressCursor ──────────────────────────────────────────────────
-- Drop FK constraint
ALTER TABLE "LiveProgressCursor" DROP CONSTRAINT IF EXISTS "LiveProgressCursor_stageId_fkey";
-- Drop unique index
DROP INDEX IF EXISTS "LiveProgressCursor_stageId_key";
-- Rename column
ALTER TABLE "LiveProgressCursor" RENAME COLUMN "stageId" TO "roundId";
-- Recreate unique index
CREATE UNIQUE INDEX "LiveProgressCursor_roundId_key" ON "LiveProgressCursor"("roundId");
-- Recreate FK pointing to Round
ALTER TABLE "LiveProgressCursor" ADD CONSTRAINT "LiveProgressCursor_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── 16. SpecialAward: Drop trackId column ───────────────────────────────────
-- Drop FK constraint
ALTER TABLE "SpecialAward" DROP CONSTRAINT IF EXISTS "SpecialAward_trackId_fkey";
-- Drop unique index
DROP INDEX IF EXISTS "SpecialAward_trackId_key";
-- Drop column
ALTER TABLE "SpecialAward" DROP COLUMN IF EXISTS "trackId";

View File

@@ -0,0 +1,31 @@
-- =============================================================================
-- Phase 7/8 Migration Part 2: Drop legacy Pipeline/Track/Stage tables and enums
-- =============================================================================
-- This migration removes the old stage-based architecture tables and enums.
-- All data has been migrated to Competition/Round architecture.
-- ─── Drop Tables in FK-safe order ────────────────────────────────────────────
-- Drop ProjectStageState (references Track and Stage)
DROP TABLE IF EXISTS "ProjectStageState" CASCADE;
-- Drop StageTransition (references Stage)
DROP TABLE IF EXISTS "StageTransition" CASCADE;
-- Drop Stage (references Track)
DROP TABLE IF EXISTS "Stage" CASCADE;
-- Drop Track (references Pipeline)
DROP TABLE IF EXISTS "Track" CASCADE;
-- Drop Pipeline
DROP TABLE IF EXISTS "Pipeline" CASCADE;
-- ─── Drop Enums ──────────────────────────────────────────────────────────────
DROP TYPE IF EXISTS "StageType";
DROP TYPE IF EXISTS "TrackKind";
DROP TYPE IF EXISTS "RoutingMode";
DROP TYPE IF EXISTS "StageStatus";
DROP TYPE IF EXISTS "ProjectStageStateValue";
DROP TYPE IF EXISTS "DecisionMode";

File diff suppressed because it is too large Load Diff

View File

@@ -7,16 +7,18 @@ import {
SettingCategory,
CompetitionCategory,
OceanIssue,
StageType,
TrackKind,
RoutingMode,
DecisionMode,
StageStatus,
ProjectStageStateValue,
ProjectStatus,
SubmissionSource,
// Competition architecture enums
CompetitionStatus,
RoundType,
RoundStatus,
CapMode,
JuryGroupMemberRole,
AdvancementRuleType,
} from '@prisma/client'
import bcrypt from 'bcryptjs'
import { defaultRoundConfig } from '../src/types/competition-configs'
import { readFileSync } from 'fs'
import { parse } from 'csv-parse/sync'
import { join, dirname } from 'path'
@@ -424,368 +426,10 @@ async function main() {
})
console.log(` ✓ Program: ${program.name} ${program.year}`)
// ==========================================================================
// 7. Pipeline
// ==========================================================================
console.log('\n🔗 Creating pipeline...')
const pipeline = await prisma.pipeline.upsert({
where: { slug: 'mopc-2026' },
update: {
name: 'MOPC 2026 Main Pipeline',
status: 'ACTIVE',
},
create: {
programId: program.id,
name: 'MOPC 2026 Main Pipeline',
slug: 'mopc-2026',
status: 'ACTIVE',
settingsJson: {
description: 'Main pipeline for MOPC 2026 competition',
allowParallelTracks: true,
autoAdvanceOnClose: false,
},
},
})
console.log(` ✓ Pipeline: ${pipeline.name}`)
// Legacy Pipeline/Track/Stage system removed - Competition/Round architecture now in use
// ==========================================================================
// 8. Tracks (4)
// ==========================================================================
console.log('\n🛤 Creating tracks...')
const mainTrack = await prisma.track.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'main' } },
update: { name: 'Main Competition' },
create: {
pipelineId: pipeline.id,
name: 'Main Competition',
slug: 'main',
kind: TrackKind.MAIN,
sortOrder: 0,
settingsJson: { description: 'Primary competition track for all applicants' },
},
})
const innovationTrack = await prisma.track.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'innovation-award' } },
update: { name: 'Ocean Innovation Award' },
create: {
pipelineId: pipeline.id,
name: 'Ocean Innovation Award',
slug: 'innovation-award',
kind: TrackKind.AWARD,
routingMode: RoutingMode.SHARED,
decisionMode: DecisionMode.JURY_VOTE,
sortOrder: 1,
settingsJson: { description: 'Award for most innovative ocean technology' },
},
})
const impactTrack = await prisma.track.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'impact-award' } },
update: { name: 'Ocean Impact Award' },
create: {
pipelineId: pipeline.id,
name: 'Ocean Impact Award',
slug: 'impact-award',
kind: TrackKind.AWARD,
routingMode: RoutingMode.EXCLUSIVE,
decisionMode: DecisionMode.AWARD_MASTER_DECISION,
sortOrder: 2,
settingsJson: { description: 'Award for highest community impact on ocean health' },
},
})
const peoplesTrack = await prisma.track.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'peoples-choice' } },
update: { name: "People's Choice" },
create: {
pipelineId: pipeline.id,
name: "People's Choice",
slug: 'peoples-choice',
kind: TrackKind.SHOWCASE,
routingMode: RoutingMode.SHARED,
sortOrder: 3,
settingsJson: { description: 'Public audience voting for fan favorite' },
},
})
console.log(` ✓ Main Competition (MAIN)`)
console.log(` ✓ Ocean Innovation Award (AWARD, SHARED)`)
console.log(` ✓ Ocean Impact Award (AWARD, EXCLUSIVE)`)
console.log(` ✓ People's Choice (SHOWCASE, SHARED)`)
// ==========================================================================
// 9. Stages
// ==========================================================================
console.log('\n📊 Creating stages...')
// --- Main track stages ---
const mainStages = await Promise.all([
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'intake' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.INTAKE,
name: 'Application Intake',
slug: 'intake',
status: StageStatus.STAGE_CLOSED,
sortOrder: 0,
configJson: {
fileRequirements: [
{ name: 'Executive Summary', type: 'PDF', maxSizeMB: 50, required: true },
{ name: 'Video Pitch', type: 'VIDEO', maxSizeMB: 500, required: false },
],
deadline: '2026-01-31T23:59:00Z',
maxSubmissions: 1,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'screening' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.FILTER,
name: 'AI Screening',
slug: 'screening',
status: StageStatus.STAGE_ACTIVE,
sortOrder: 1,
configJson: {
deterministic: {
rules: [
{ field: 'competitionCategory', operator: 'is_not_null', label: 'Has category' },
{ field: 'description', operator: 'min_length', value: 50, label: 'Description >= 50 chars' },
],
},
ai: { rubricVersion: '2026-v1', model: 'gpt-4o' },
confidenceBands: {
high: { threshold: 0.8, action: 'auto_pass' },
medium: { threshold: 0.5, action: 'manual_review' },
low: { threshold: 0, action: 'auto_reject' },
},
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'evaluation' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.EVALUATION,
name: 'Expert Evaluation',
slug: 'evaluation',
status: StageStatus.STAGE_DRAFT,
sortOrder: 2,
configJson: {
criteriaVersion: '2026-v1',
assignmentStrategy: 'smart',
requiredReviews: 3,
minAssignmentsPerJuror: 5,
maxAssignmentsPerJuror: 20,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'selection' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.SELECTION,
name: 'Semi-Final Selection',
slug: 'selection',
status: StageStatus.STAGE_DRAFT,
sortOrder: 3,
configJson: {
rankingSource: 'evaluation_scores',
finalistTarget: 6,
selectionMethod: 'top_n_with_admin_override',
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'grand-final' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.LIVE_FINAL,
name: 'Grand Final',
slug: 'grand-final',
status: StageStatus.STAGE_DRAFT,
sortOrder: 4,
configJson: {
sessionMode: 'cohort',
votingEnabled: true,
audienceVoting: true,
audienceVoteWeight: 0.2,
presentationDurationMinutes: 10,
qaDurationMinutes: 5,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'results' } },
update: {},
create: {
trackId: mainTrack.id,
stageType: StageType.RESULTS,
name: 'Results & Awards',
slug: 'results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 5,
configJson: {
rankingWeights: { juryScore: 0.8, audienceScore: 0.2 },
publicationPolicy: 'after_ceremony',
announcementDate: '2026-06-15',
},
},
}),
])
// --- Innovation Award track stages ---
const innovationStages = await Promise.all([
prisma.stage.upsert({
where: { trackId_slug: { trackId: innovationTrack.id, slug: 'innovation-review' } },
update: {},
create: {
trackId: innovationTrack.id,
stageType: StageType.EVALUATION,
name: 'Innovation Jury Review',
slug: 'innovation-review',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
criteriaVersion: 'innovation-2026-v1',
assignmentStrategy: 'manual',
requiredReviews: 2,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: innovationTrack.id, slug: 'innovation-results' } },
update: {},
create: {
trackId: innovationTrack.id,
stageType: StageType.RESULTS,
name: 'Innovation Results',
slug: 'innovation-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
// --- Impact Award track stages ---
const impactStages = await Promise.all([
prisma.stage.upsert({
where: { trackId_slug: { trackId: impactTrack.id, slug: 'impact-review' } },
update: {},
create: {
trackId: impactTrack.id,
stageType: StageType.EVALUATION,
name: 'Impact Assessment',
slug: 'impact-review',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
criteriaVersion: 'impact-2026-v1',
assignmentStrategy: 'award_master',
requiredReviews: 1,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: impactTrack.id, slug: 'impact-results' } },
update: {},
create: {
trackId: impactTrack.id,
stageType: StageType.RESULTS,
name: 'Impact Results',
slug: 'impact-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
// --- People's Choice track stages ---
const peoplesStages = await Promise.all([
prisma.stage.upsert({
where: { trackId_slug: { trackId: peoplesTrack.id, slug: 'public-vote' } },
update: {},
create: {
trackId: peoplesTrack.id,
stageType: StageType.LIVE_FINAL,
name: 'Public Voting',
slug: 'public-vote',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
votingMode: 'favorites',
maxFavorites: 3,
requireIdentification: false,
votingDurationMinutes: 30,
},
},
}),
prisma.stage.upsert({
where: { trackId_slug: { trackId: peoplesTrack.id, slug: 'peoples-results' } },
update: {},
create: {
trackId: peoplesTrack.id,
stageType: StageType.RESULTS,
name: "People's Choice Results",
slug: 'peoples-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
const allStages = [...mainStages, ...innovationStages, ...impactStages, ...peoplesStages]
console.log(` ✓ Created ${allStages.length} stages across 4 tracks`)
// ==========================================================================
// 10. Stage Transitions (linear within each track)
// ==========================================================================
console.log('\n🔀 Creating stage transitions...')
const trackStageGroups = [
{ name: 'Main', stages: mainStages },
{ name: 'Innovation', stages: innovationStages },
{ name: 'Impact', stages: impactStages },
{ name: "People's", stages: peoplesStages },
]
let transitionCount = 0
for (const group of trackStageGroups) {
for (let i = 0; i < group.stages.length - 1; i++) {
await prisma.stageTransition.upsert({
where: {
fromStageId_toStageId: {
fromStageId: group.stages[i].id,
toStageId: group.stages[i + 1].id,
},
},
update: {},
create: {
fromStageId: group.stages[i].id,
toStageId: group.stages[i + 1].id,
isDefault: true,
},
})
transitionCount++
}
}
console.log(` ✓ Created ${transitionCount} transitions`)
// ==========================================================================
// 11. Parse CSV & Create Applicants + Projects
// 7. Parse CSV & Create Applicants + Projects
// ==========================================================================
console.log('\n📄 Checking for existing projects...')
@@ -820,9 +464,6 @@ async function main() {
// Create applicant users and projects
console.log('\n🚀 Creating applicant users and projects...')
const intakeStage = mainStages[0] // INTAKE - CLOSED
const filterStage = mainStages[1] // FILTER - ACTIVE
let skippedNoEmail = 0
for (let rowIdx = 0; rowIdx < validRecords.length; rowIdx++) {
const row = validRecords[rowIdx]
@@ -871,7 +512,7 @@ async function main() {
})
// Create project
const project = await prisma.project.create({
await prisma.project.create({
data: {
programId: program.id,
title: projectName || `Project by ${name}`,
@@ -896,108 +537,325 @@ async function main() {
},
})
// Create ProjectStageState: INTAKE stage = PASSED (intake closed)
await prisma.projectStageState.create({
data: {
projectId: project.id,
trackId: mainTrack.id,
stageId: intakeStage.id,
state: ProjectStageStateValue.PASSED,
enteredAt: new Date('2026-01-15'),
exitedAt: new Date('2026-01-31'),
},
})
// Create ProjectStageState: FILTER stage = PENDING (current active stage)
await prisma.projectStageState.create({
data: {
projectId: project.id,
trackId: mainTrack.id,
stageId: filterStage.id,
state: ProjectStageStateValue.PENDING,
enteredAt: new Date('2026-02-01'),
},
})
projectCount++
if (projectCount % 50 === 0) {
console.log(` ... ${projectCount} projects created`)
}
}
console.log(` ✓ Created ${projectCount} projects with stage states`)
console.log(` ✓ Created ${projectCount} projects`)
if (skippedNoEmail > 0) {
console.log(` ⚠ Skipped ${skippedNoEmail} rows with no valid email`)
}
}
// ==========================================================================
// 12. Evaluation Form (for Expert Evaluation stage)
// ==========================================================================
console.log('\n📝 Creating evaluation form...')
// Legacy evaluation forms and special awards removed - Competition/Round architecture now in use
const evaluationStage = mainStages[2] // EVALUATION stage
await prisma.evaluationForm.upsert({
where: { stageId_version: { stageId: evaluationStage.id, version: 1 } },
// ==========================================================================
// 8. Competition Architecture
// ==========================================================================
console.log('\n🏗 Creating competition architecture...')
const competition = await prisma.competition.upsert({
where: { slug: 'mopc-2026' },
update: {},
create: {
stageId: evaluationStage.id,
version: 1,
isActive: true,
criteriaJson: [
{ id: 'need_clarity', label: 'Need Clarity', description: 'How clearly is the problem/need articulated?', scale: '1-5', weight: 20, type: 'numeric', required: true },
{ id: 'solution_relevance', label: 'Solution Relevance', description: 'How relevant and innovative is the proposed solution?', scale: '1-5', weight: 25, type: 'numeric', required: true },
{ id: 'ocean_impact', label: 'Ocean Impact', description: 'What is the potential positive impact on ocean conservation?', scale: '1-5', weight: 25, type: 'numeric', required: true },
{ id: 'feasibility', label: 'Feasibility & Scalability', description: 'How feasible and scalable is the project?', scale: '1-5', weight: 20, type: 'numeric', required: true },
{ id: 'team_strength', label: 'Team Strength', description: 'How strong and capable is the team?', scale: '1-5', weight: 10, type: 'numeric', required: true },
],
scalesJson: {
'1-5': { min: 1, max: 5, labels: { 1: 'Poor', 2: 'Below Average', 3: 'Average', 4: 'Good', 5: 'Excellent' } },
programId: program.id,
name: 'MOPC 2026',
slug: 'mopc-2026',
status: CompetitionStatus.ACTIVE,
categoryMode: 'SHARED',
startupFinalistCount: 3,
conceptFinalistCount: 3,
notifyOnRoundAdvance: true,
notifyOnDeadlineApproach: true,
deadlineReminderDays: [7, 3, 1],
},
})
console.log(` ✓ Competition: ${competition.name}`)
// --- Jury Groups ---
const juryGroup1 = await prisma.juryGroup.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'screening-jury' } },
update: {},
create: {
competitionId: competition.id,
name: 'Screening Jury',
slug: 'screening-jury',
sortOrder: 0,
defaultMaxAssignments: 30,
defaultCapMode: CapMode.SOFT,
softCapBuffer: 5,
categoryQuotasEnabled: false,
},
})
const juryGroup2 = await prisma.juryGroup.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'expert-jury' } },
update: {},
create: {
competitionId: competition.id,
name: 'Expert Jury',
slug: 'expert-jury',
sortOrder: 1,
defaultMaxAssignments: 20,
defaultCapMode: CapMode.SOFT,
softCapBuffer: 2,
categoryQuotasEnabled: true,
defaultCategoryQuotas: {
STARTUP: { min: 5, max: 15 },
BUSINESS_CONCEPT: { min: 3, max: 10 },
},
},
})
console.log(' ✓ Evaluation form created (5 criteria)')
// ==========================================================================
// 13. Special Awards
// ==========================================================================
console.log('\n🏆 Creating special awards...')
await prisma.specialAward.upsert({
where: { trackId: innovationTrack.id },
const juryGroup3 = await prisma.juryGroup.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'finals-jury' } },
update: {},
create: {
programId: program.id,
name: 'Ocean Innovation Award',
description: 'Recognizes the most innovative technology solution for ocean protection',
status: 'DRAFT',
trackId: innovationTrack.id,
scoringMode: 'PICK_WINNER',
useAiEligibility: true,
criteriaText: 'Projects demonstrating breakthrough technological innovation for ocean conservation',
competitionId: competition.id,
name: 'Finals Jury',
slug: 'finals-jury',
sortOrder: 2,
defaultMaxAssignments: 10,
defaultCapMode: CapMode.HARD,
softCapBuffer: 0,
categoryQuotasEnabled: false,
},
})
await prisma.specialAward.upsert({
where: { trackId: impactTrack.id },
console.log(' ✓ Jury Groups: Screening, Expert, Finals')
// --- Add jury members to groups ---
// Split 8 jurors: 4 in screening, 6 in expert (some overlap), all 8 in finals
const juryGroupAssignments = [
{ groupId: juryGroup1.id, userIds: juryUserIds.slice(0, 4), role: JuryGroupMemberRole.MEMBER },
{ groupId: juryGroup2.id, userIds: juryUserIds.slice(0, 6), role: JuryGroupMemberRole.MEMBER },
{ groupId: juryGroup3.id, userIds: juryUserIds, role: JuryGroupMemberRole.MEMBER },
]
let memberCount = 0
for (const assignment of juryGroupAssignments) {
for (let i = 0; i < assignment.userIds.length; i++) {
const userId = assignment.userIds[i]
await prisma.juryGroupMember.upsert({
where: {
juryGroupId_userId: { juryGroupId: assignment.groupId, userId },
},
update: {},
create: {
juryGroupId: assignment.groupId,
userId,
role: i === 0 ? JuryGroupMemberRole.CHAIR : assignment.role,
},
})
memberCount++
}
}
console.log(`${memberCount} jury group memberships created`)
// --- Demo self-service preferences ---
// Enable self-service on the Expert Panel and set preferences for first 2 members
await prisma.juryGroup.update({
where: { id: juryGroup2.id },
data: { allowJurorCapAdjustment: true, allowJurorRatioAdjustment: true },
})
// Juror 0 sets a lower cap and prefers startups
const selfServiceMember1 = await prisma.juryGroupMember.findUnique({
where: { juryGroupId_userId: { juryGroupId: juryGroup2.id, userId: juryUserIds[0] } },
})
if (selfServiceMember1) {
await prisma.juryGroupMember.update({
where: { id: selfServiceMember1.id },
data: { selfServiceCap: 12, selfServiceRatio: 0.7 },
})
}
// Juror 1 sets a moderate ratio preference
const selfServiceMember2 = await prisma.juryGroupMember.findUnique({
where: { juryGroupId_userId: { juryGroupId: juryGroup2.id, userId: juryUserIds[1] } },
})
if (selfServiceMember2) {
await prisma.juryGroupMember.update({
where: { id: selfServiceMember2.id },
data: { selfServiceRatio: 0.4 },
})
}
console.log(' ✓ Self-service preferences: 2 jurors in Expert Panel')
// --- Submission Windows ---
const submissionWindow1 = await prisma.submissionWindow.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'r1-application-docs' } },
update: {},
create: {
programId: program.id,
name: 'Ocean Impact Award',
description: 'Recognizes the project with highest community and environmental impact',
status: 'DRAFT',
trackId: impactTrack.id,
scoringMode: 'PICK_WINNER',
useAiEligibility: false,
criteriaText: 'Projects with measurable, significant impact on ocean health and coastal communities',
competitionId: competition.id,
name: 'R1 Application Documents',
slug: 'r1-application-docs',
roundNumber: 1,
sortOrder: 0,
windowOpenAt: new Date('2026-01-01'),
windowCloseAt: new Date('2026-01-31'),
isLocked: true,
},
})
console.log(' ✓ Ocean Innovation Award → innovation-award track')
console.log(' ✓ Ocean Impact Award → impact-award track')
const submissionWindow2 = await prisma.submissionWindow.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: 'r4-semifinal-docs' } },
update: {},
create: {
competitionId: competition.id,
name: 'R4 Semi-Finalist Documents',
slug: 'r4-semifinal-docs',
roundNumber: 4,
sortOrder: 1,
windowOpenAt: new Date('2026-04-01'),
windowCloseAt: new Date('2026-04-30'),
isLocked: false,
},
})
console.log(' ✓ Submission Windows: R1 Application, R4 Semi-finalist')
// --- File Requirements ---
await prisma.submissionFileRequirement.upsert({
where: { submissionWindowId_slug: { submissionWindowId: submissionWindow1.id, slug: 'executive-summary' } },
update: {},
create: {
submissionWindowId: submissionWindow1.id,
label: 'Executive Summary',
slug: 'executive-summary',
description: 'PDF document summarizing the project',
mimeTypes: ['application/pdf'],
maxSizeMb: 50,
required: true,
sortOrder: 0,
},
})
await prisma.submissionFileRequirement.upsert({
where: { submissionWindowId_slug: { submissionWindowId: submissionWindow1.id, slug: 'video-pitch' } },
update: {},
create: {
submissionWindowId: submissionWindow1.id,
label: 'Video Pitch',
slug: 'video-pitch',
description: 'Short video pitching the project (max 5 minutes)',
mimeTypes: ['video/mp4', 'video/quicktime'],
maxSizeMb: 500,
required: false,
sortOrder: 1,
},
})
await prisma.submissionFileRequirement.upsert({
where: { submissionWindowId_slug: { submissionWindowId: submissionWindow2.id, slug: 'updated-business-plan' } },
update: {},
create: {
submissionWindowId: submissionWindow2.id,
label: 'Updated Business Plan',
slug: 'updated-business-plan',
description: 'Updated business plan with financials',
mimeTypes: ['application/pdf'],
maxSizeMb: 50,
required: true,
sortOrder: 0,
},
})
console.log(' ✓ File Requirements: Exec Summary, Video Pitch, Business Plan')
// --- Rounds (8-round Monaco flow) ---
const roundDefs = [
{ name: 'R1 - Application Intake', slug: 'r1-intake', roundType: RoundType.INTAKE, sortOrder: 0, status: RoundStatus.ROUND_CLOSED, juryGroupId: null, submissionWindowId: submissionWindow1.id },
{ name: 'R2 - AI Screening', slug: 'r2-screening', roundType: RoundType.FILTERING, sortOrder: 1, status: RoundStatus.ROUND_ACTIVE, juryGroupId: juryGroup1.id, submissionWindowId: null },
{ name: 'R3 - Expert Evaluation', slug: 'r3-evaluation', roundType: RoundType.EVALUATION, sortOrder: 2, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup2.id, submissionWindowId: null },
{ name: 'R4 - Document Submission', slug: 'r4-submission', roundType: RoundType.SUBMISSION, sortOrder: 3, status: RoundStatus.ROUND_DRAFT, juryGroupId: null, submissionWindowId: submissionWindow2.id },
{ name: 'R5 - Semi-Final Evaluation', slug: 'r5-semi-eval', roundType: RoundType.EVALUATION, sortOrder: 4, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup2.id, submissionWindowId: null },
{ name: 'R6 - Mentoring', slug: 'r6-mentoring', roundType: RoundType.MENTORING, sortOrder: 5, status: RoundStatus.ROUND_DRAFT, juryGroupId: null, submissionWindowId: null },
{ name: 'R7 - Grand Final', slug: 'r7-grand-final', roundType: RoundType.LIVE_FINAL, sortOrder: 6, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup3.id, submissionWindowId: null },
{ name: 'R8 - Deliberation', slug: 'r8-deliberation', roundType: RoundType.DELIBERATION, sortOrder: 7, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup3.id, submissionWindowId: null },
]
const rounds = []
for (const def of roundDefs) {
const config = defaultRoundConfig(def.roundType)
const round = await prisma.round.upsert({
where: { competitionId_slug: { competitionId: competition.id, slug: def.slug } },
update: {},
create: {
competitionId: competition.id,
name: def.name,
slug: def.slug,
roundType: def.roundType,
status: def.status,
sortOrder: def.sortOrder,
configJson: config as object,
juryGroupId: def.juryGroupId,
submissionWindowId: def.submissionWindowId,
},
})
rounds.push(round)
}
console.log(`${rounds.length} rounds created (R1-R8)`)
// --- Advancement Rules (auto-advance between rounds) ---
for (let i = 0; i < rounds.length - 1; i++) {
await prisma.advancementRule.upsert({
where: {
roundId_sortOrder: { roundId: rounds[i].id, sortOrder: 0 },
},
update: {},
create: {
roundId: rounds[i].id,
ruleType: AdvancementRuleType.AUTO_ADVANCE,
sortOrder: 0,
targetRoundId: rounds[i + 1].id,
configJson: {},
},
})
}
console.log(`${rounds.length - 1} advancement rules created`)
// --- Round-Submission Visibility (which rounds can see which submission windows) ---
// R2 and R3 can see R1 docs, R5 can see R4 docs
const visibilityLinks = [
{ roundId: rounds[1].id, submissionWindowId: submissionWindow1.id }, // R2 sees R1 docs
{ roundId: rounds[2].id, submissionWindowId: submissionWindow1.id }, // R3 sees R1 docs
{ roundId: rounds[4].id, submissionWindowId: submissionWindow1.id }, // R5 sees R1 docs
{ roundId: rounds[4].id, submissionWindowId: submissionWindow2.id }, // R5 sees R4 docs
]
for (const link of visibilityLinks) {
await prisma.roundSubmissionVisibility.upsert({
where: {
roundId_submissionWindowId: {
roundId: link.roundId,
submissionWindowId: link.submissionWindowId,
},
},
update: {},
create: link,
})
}
console.log(`${visibilityLinks.length} submission visibility links created`)
// --- Feature flag: enable competition model ---
await prisma.systemSettings.upsert({
where: { key: 'feature.useCompetitionModel' },
update: { value: 'true' },
create: {
key: 'feature.useCompetitionModel',
value: 'true',
type: SettingType.BOOLEAN,
category: SettingCategory.FEATURE_FLAGS,
description: 'Use Competition/Round model (legacy Pipeline system removed)',
},
})
console.log(' ✓ Feature flag: feature.useCompetitionModel = true')
// ==========================================================================
// 14. Notification Email Settings
// 9. Notification Email Settings
// ==========================================================================
console.log('\n🔔 Creating notification email settings...')
@@ -1052,27 +910,12 @@ async function main() {
console.log(` ✓ Created ${notificationSettings.length} notification email settings`)
// ==========================================================================
// 16. Summary
// 10. Summary
// ==========================================================================
console.log('\n' + '='.repeat(60))
console.log('✅ SEEDING COMPLETE')
console.log('='.repeat(60))
console.log(`
Program: ${program.name} ${program.year}
Pipeline: ${pipeline.name} (${pipeline.slug})
Tracks: 4 (Main, Innovation Award, Impact Award, People's Choice)
Stages: ${allStages.length} total
Transitions: ${transitionCount}
Projects: ${projectCount} (from CSV)
Users: ${3 + juryMembers.length + mentors.length + observers.length + projectCount} total
- Admin/Staff: 3
- Jury: ${juryMembers.length}
- Mentors: ${mentors.length}
- Observers: ${observers.length}
- Applicants: ${projectCount}
Login: matt@monaco-opc.com / 195260Mp!
`)
console.log(`\n Program: ${program.name} ${program.year}\n\n Competition: ${competition.name} (${competition.slug})\n Rounds: ${rounds.length} (R1-R8)\n Jury Groups: 3 (Screening, Expert, Finals)\n Sub. Windows: 2 (R1 Application, R4 Semi-finalist)\n\n Projects: ${projectCount} (from CSV)\n Users: ${3 + juryMembers.length + mentors.length + observers.length + projectCount} total\n - Admin/Staff: 3\n - Jury: ${juryMembers.length}\n - Mentors: ${mentors.length}\n - Observers: ${observers.length}\n - Applicants: ${projectCount}\n\n Login: matt@monaco-opc.com / 195260Mp!\n `)
}
main()