diff --git a/prisma/migrations/20260308000000_drop_dead_models_and_stale_columns/migration.sql b/prisma/migrations/20260308000000_drop_dead_models_and_stale_columns/migration.sql new file mode 100644 index 0000000..9a5e1ba --- /dev/null +++ b/prisma/migrations/20260308000000_drop_dead_models_and_stale_columns/migration.sql @@ -0,0 +1,35 @@ +-- DropForeignKey +ALTER TABLE "AdvancementRule" DROP CONSTRAINT "AdvancementRule_roundId_fkey"; + +-- DropForeignKey +ALTER TABLE "AssignmentException" DROP CONSTRAINT "AssignmentException_approvedById_fkey"; + +-- DropForeignKey +ALTER TABLE "AssignmentException" DROP CONSTRAINT "AssignmentException_assignmentId_fkey"; + +-- AlterTable +ALTER TABLE "ConflictOfInterest" DROP COLUMN "roundId"; + +-- AlterTable +ALTER TABLE "Evaluation" DROP COLUMN "version"; + +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "roundId"; + +-- DropTable +DROP TABLE "AdvancementRule"; + +-- DropTable +DROP TABLE "AssignmentException"; + +-- DropTable +DROP TABLE "NotificationPolicy"; + +-- DropTable +DROP TABLE "OverrideAction"; + +-- DropEnum +DROP TYPE "AdvancementRuleType"; + +-- DropEnum +DROP TYPE "OverrideReasonCode"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a4f36a3..bc5ae76 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -134,13 +134,6 @@ enum PartnerType { OTHER } -enum OverrideReasonCode { - DATA_CORRECTION - POLICY_EXCEPTION - JURY_CONFLICT - SPONSOR_DECISION - ADMIN_DISCRETION -} // ============================================================================= // COMPETITION / ROUND ENGINE ENUMS @@ -179,13 +172,6 @@ enum ProjectRoundStateValue { WITHDRAWN } -enum AdvancementRuleType { - AUTO_ADVANCE - SCORE_THRESHOLD - TOP_N - ADMIN_SELECTION - AI_RECOMMENDED -} enum CapMode { HARD @@ -431,7 +417,6 @@ model User { mentorFileComments MentorFileComment[] @relation("MentorFileCommentAuthor") resultLocksCreated ResultLock[] @relation("ResultLockCreator") resultUnlockEvents ResultUnlockEvent[] @relation("ResultUnlocker") - assignmentExceptionsApproved AssignmentException[] @relation("AssignmentExceptionApprover") submissionPromotions SubmissionPromotionEvent[] @relation("SubmissionPromoter") deliberationReplacements DeliberationParticipant[] @relation("DeliberationReplacement") @@ -563,7 +548,6 @@ model EvaluationForm { model Project { id String @id @default(cuid()) programId String - roundId String? status ProjectStatus @default(SUBMITTED) // Core fields @@ -763,7 +747,6 @@ model Assignment { juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) evaluation Evaluation? conflictOfInterest ConflictOfInterest? - exceptions AssignmentException[] @@unique([userId, projectId, roundId]) @@index([roundId]) @@ -790,11 +773,6 @@ model Evaluation { binaryDecision Boolean? // Yes/No for semi-finalist feedbackText String? @db.Text - // Versioning (currently unused - evaluations are updated in-place. - // TODO: Implement proper versioning by creating new rows on re-submission - // if version history is needed for audit purposes) - version Int @default(1) - // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -1729,7 +1707,6 @@ model ConflictOfInterest { assignmentId String @unique userId String projectId String - roundId String? // Legacy — kept for historical data hasConflict Boolean @default(false) conflictType String? // "financial", "personal", "organizational", "other" description String? @db.Text @@ -2111,24 +2088,6 @@ model LiveProgressCursor { @@index([sessionId]) } -model OverrideAction { - id String @id @default(cuid()) - entityType String // ProjectRoundState, FilteringResult, AwardEligibility, etc. - entityId String - previousValue Json? @db.JsonB - newValueJson Json @db.JsonB - reasonCode OverrideReasonCode - reasonText String? @db.Text - actorId String - - createdAt DateTime @default(now()) - - @@index([entityType, entityId]) - @@index([actorId]) - @@index([reasonCode]) - @@index([createdAt]) -} - model DecisionAuditLog { id String @id @default(cuid()) eventType String // stage.transitioned, routing.executed, filtering.completed, etc. @@ -2146,21 +2105,6 @@ model DecisionAuditLog { @@index([createdAt]) } -model NotificationPolicy { - id String @id @default(cuid()) - eventType String @unique // stage.transitioned, filtering.completed, etc. - channel String @default("EMAIL") // EMAIL, IN_APP, BOTH, NONE - templateId String? // Optional reference to MessageTemplate - isActive Boolean @default(true) - configJson Json? @db.JsonB // Additional config (delay, batch, etc.) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([eventType]) - @@index([isActive]) -} - // ============================================================================= // COMPETITION / ROUND ENGINE MODELS (NEW — coexists with Pipeline/Track/Stage) // ============================================================================= @@ -2236,7 +2180,6 @@ model Round { juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) submissionWindow SubmissionWindow? @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull) projectRoundStates ProjectRoundState[] - advancementRules AdvancementRule[] visibleSubmissionWindows RoundSubmissionVisibility[] assignmentIntents AssignmentIntent[] deliberationSessions DeliberationSession[] @@ -2295,24 +2238,6 @@ model ProjectRoundState { @@index([roundId, state]) } -model AdvancementRule { - id String @id @default(cuid()) - roundId String - targetRoundId String? - ruleType AdvancementRuleType - configJson Json @db.JsonB - isDefault Boolean @default(true) - sortOrder Int @default(0) - - createdAt DateTime @default(now()) - - // Relations - round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) - - @@unique([roundId, sortOrder]) - @@index([roundId]) -} - // ============================================================================= // JURY GROUP MODELS (NEW) // ============================================================================= @@ -2489,22 +2414,6 @@ model AssignmentIntent { @@index([status]) } -model AssignmentException { - id String @id @default(cuid()) - assignmentId String - reason String @db.Text - overCapBy Int - approvedById String - createdAt DateTime @default(now()) - - // Relations - assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade) - approvedBy User @relation("AssignmentExceptionApprover", fields: [approvedById], references: [id]) - - @@index([assignmentId]) - @@index([approvedById]) -} - // ============================================================================= // MENTORING WORKSPACE MODELS (NEW) // ============================================================================= diff --git a/prisma/seed.ts b/prisma/seed.ts index 3bb9a79..072ddd9 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -15,7 +15,6 @@ import { RoundStatus, CapMode, JuryGroupMemberRole, - AdvancementRuleType, } from '@prisma/client' import bcrypt from 'bcryptjs' // Inline default configs so seed has ZERO dependency on src/ (not available in Docker prod image) @@ -858,24 +857,6 @@ async function main() { } 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`) - // --- Assign all projects to intake round (COMPLETED, since intake is closed) --- const intakeRound = rounds[0] const allProjects = await prisma.project.findMany({ diff --git a/src/server/routers/assignment.ts b/src/server/routers/assignment.ts index e26776d..8e20e40 100644 --- a/src/server/routers/assignment.ts +++ b/src/server/routers/assignment.ts @@ -119,7 +119,7 @@ async function runAIAssignmentJob(jobId: string, roundId: string, userId: string // Query COI records for this round to exclude conflicted juror-project pairs const coiRecords = await prisma.conflictOfInterest.findMany({ where: { - roundId, + assignment: { roundId }, hasConflict: true, }, select: { userId: true, projectId: true }, @@ -1665,7 +1665,7 @@ export const assignmentRouter = router({ for (const a of existingAssignments) currentLoads.set(a.userId, (currentLoads.get(a.userId) ?? 0) + 1) const coiRecords = await ctx.prisma.conflictOfInterest.findMany({ - where: { roundId: input.roundId, hasConflict: true, userId: { in: candidateIds } }, + where: { assignment: { roundId: input.roundId }, hasConflict: true, userId: { in: candidateIds } }, select: { userId: true, projectId: true }, }) const coiPairs = new Set(coiRecords.map((c) => `${c.userId}:${c.projectId}`)) @@ -1911,7 +1911,7 @@ export const assignmentRouter = router({ const coiRecords = await ctx.prisma.conflictOfInterest.findMany({ where: { - roundId: input.roundId, + assignment: { roundId: input.roundId }, hasConflict: true, userId: { in: candidateIds }, }, @@ -2026,7 +2026,7 @@ export const assignmentRouter = router({ const coiRecords = await ctx.prisma.conflictOfInterest.findMany({ where: { - roundId: input.roundId, + assignment: { roundId: input.roundId }, hasConflict: true, userId: { in: destinationIds }, }, @@ -2367,7 +2367,7 @@ export const assignmentRouter = router({ const coiRecords = await ctx.prisma.conflictOfInterest.findMany({ where: { - roundId: input.roundId, + assignment: { roundId: input.roundId }, hasConflict: true, userId: { in: candidateIds }, }, diff --git a/src/server/routers/project.ts b/src/server/routers/project.ts index b156379..58e7deb 100644 --- a/src/server/routers/project.ts +++ b/src/server/routers/project.ts @@ -638,10 +638,6 @@ export const projectRouter = router({ }) if (input.roundId) { - await tx.project.update({ - where: { id: created.id }, - data: { roundId: input.roundId }, - }) await tx.projectRoundState.create({ data: { projectId: created.id, diff --git a/src/server/routers/round.ts b/src/server/routers/round.ts index de7f7a1..8af77ae 100644 --- a/src/server/routers/round.ts +++ b/src/server/routers/round.ts @@ -120,7 +120,6 @@ export const roundRouter = router({ submissionWindow: { include: { fileRequirements: true }, }, - advancementRules: { orderBy: { sortOrder: 'asc' } }, visibleSubmissionWindows: { include: { submissionWindow: true }, }, diff --git a/src/server/routers/user.ts b/src/server/routers/user.ts index 71c3dfd..1abdd3a 100644 --- a/src/server/routers/user.ts +++ b/src/server/routers/user.ts @@ -206,7 +206,10 @@ export const userRouter = router({ }) } - // Delete user + // TODO: This delete will fail with a FK violation for any user with activity + // (COI declarations, mentor assignments, messages, evaluations, etc.). + // A proper purge flow with pre-deletion cleanup or soft-delete is needed + // before hard-delete can work reliably for active users. await ctx.prisma.user.delete({ where: { id: ctx.user.id }, }) @@ -657,6 +660,10 @@ export const userRouter = router({ select: { email: true }, }) + // TODO: This delete will fail with a FK violation for any user with activity + // (COI declarations, mentor assignments, messages, evaluations, etc.). + // A proper purge flow with pre-deletion cleanup or soft-delete is needed + // before hard-delete can work reliably for active users. const user = await ctx.prisma.user.delete({ where: { id: input.id }, }) diff --git a/src/server/services/juror-reassignment.ts b/src/server/services/juror-reassignment.ts index 97c20b1..1082c83 100644 --- a/src/server/services/juror-reassignment.ts +++ b/src/server/services/juror-reassignment.ts @@ -357,7 +357,7 @@ export async function reassignDroppedJurorAssignments(params: { const coiRecords = await prisma.conflictOfInterest.findMany({ where: { - roundId: params.roundId, + assignment: { roundId: params.roundId }, hasConflict: true, userId: { in: candidateIds }, }, diff --git a/src/types/competition.ts b/src/types/competition.ts index 36c0714..4778168 100644 --- a/src/types/competition.ts +++ b/src/types/competition.ts @@ -5,7 +5,6 @@ import type { JuryGroupMember, SubmissionWindow, SubmissionFileRequirement, - AdvancementRule, RoundSubmissionVisibility, ProjectRoundState, DeliberationSession, @@ -36,7 +35,6 @@ export type RoundSummary = Pick< export type RoundWithRelations = Round & { juryGroup: (JuryGroup & { members: JuryGroupMember[] }) | null submissionWindow: (SubmissionWindow & { fileRequirements: SubmissionFileRequirement[] }) | null - advancementRules: AdvancementRule[] visibleSubmissionWindows: (RoundSubmissionVisibility & { submissionWindow: SubmissionWindow })[] _count?: { projectRoundStates: number; assignments: number } } diff --git a/tests/helpers.ts b/tests/helpers.ts index 160036f..2317590 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -329,7 +329,6 @@ export async function cleanupTestData(programId: string, userIds: string[] = []) // Delete in reverse dependency order — scoped by programId or userIds if (userIds.length > 0) { await prisma.decisionAuditLog.deleteMany({ where: { actorId: { in: userIds } } }) - await prisma.overrideAction.deleteMany({ where: { actorId: { in: userIds } } }) await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } }) } // Competition/Round cascade cleanup @@ -350,7 +349,6 @@ export async function cleanupTestData(programId: string, userIds: string[] = []) await prisma.evaluationSummary.deleteMany({ where: { round: { competition: { programId } } } }) await prisma.evaluationDiscussion.deleteMany({ where: { round: { competition: { programId } } } }) await prisma.projectRoundState.deleteMany({ where: { round: { competition: { programId } } } }) - await prisma.advancementRule.deleteMany({ where: { round: { competition: { programId } } } }) await prisma.awardEligibility.deleteMany({ where: { award: { program: { id: programId } } } }) await prisma.awardVote.deleteMany({ where: { award: { program: { id: programId } } } }) await prisma.awardJuror.deleteMany({ where: { award: { program: { id: programId } } } })