// ============================================================================= // MOPC Platform - Prisma Schema // ============================================================================= // This schema defines the database structure for the Monaco Ocean Protection // Challenge jury voting platform. generator client { provider = "prisma-client-js" binaryTargets = ["native", "windows", "linux-musl-openssl-3.0.x"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================================================= // ENUMS // ============================================================================= enum UserRole { SUPER_ADMIN PROGRAM_ADMIN JURY_MEMBER MENTOR OBSERVER APPLICANT } enum UserStatus { INVITED ACTIVE SUSPENDED } enum ProgramStatus { DRAFT ACTIVE ARCHIVED } enum RoundStatus { DRAFT ACTIVE CLOSED ARCHIVED } enum ProjectStatus { SUBMITTED ELIGIBLE ASSIGNED SEMIFINALIST FINALIST REJECTED } enum EvaluationStatus { NOT_STARTED DRAFT SUBMITTED LOCKED } enum AssignmentMethod { MANUAL BULK AI_SUGGESTED AI_AUTO ALGORITHM } enum FileType { EXEC_SUMMARY PRESENTATION VIDEO OTHER BUSINESS_PLAN VIDEO_PITCH SUPPORTING_DOC } enum SubmissionSource { MANUAL CSV NOTION TYPEFORM PUBLIC_FORM } enum RoundType { FILTERING EVALUATION LIVE_EVENT } enum SettingType { STRING NUMBER BOOLEAN JSON SECRET } enum SettingCategory { AI BRANDING EMAIL STORAGE SECURITY DEFAULTS WHATSAPP } enum NotificationChannel { EMAIL WHATSAPP BOTH NONE } enum ResourceType { PDF VIDEO DOCUMENT LINK OTHER } enum CohortLevel { ALL SEMIFINALIST FINALIST } enum PartnerVisibility { ADMIN_ONLY JURY_VISIBLE PUBLIC } enum PartnerType { SPONSOR PARTNER SUPPORTER MEDIA OTHER } enum FormFieldType { TEXT TEXTAREA NUMBER EMAIL PHONE URL DATE DATETIME SELECT MULTI_SELECT RADIO CHECKBOX CHECKBOX_GROUP FILE FILE_MULTIPLE SECTION INSTRUCTIONS } enum SpecialFieldType { TEAM_MEMBERS // Team member repeater COMPETITION_CATEGORY // Business Concept vs Startup OCEAN_ISSUE // Ocean issue dropdown FILE_UPLOAD // File upload GDPR_CONSENT // GDPR consent checkbox COUNTRY_SELECT // Country dropdown } // ============================================================================= // APPLICANT SYSTEM ENUMS // ============================================================================= enum CompetitionCategory { STARTUP // Existing companies BUSINESS_CONCEPT // Students/graduates } enum OceanIssue { POLLUTION_REDUCTION CLIMATE_MITIGATION TECHNOLOGY_INNOVATION SUSTAINABLE_SHIPPING BLUE_CARBON HABITAT_RESTORATION COMMUNITY_CAPACITY SUSTAINABLE_FISHING CONSUMER_AWARENESS OCEAN_ACIDIFICATION OTHER } enum TeamMemberRole { LEAD // Primary contact / team lead MEMBER // Regular team member ADVISOR // Advisor/mentor from team side } enum MentorAssignmentMethod { MANUAL AI_SUGGESTED AI_AUTO ALGORITHM } // ============================================================================= // USERS & AUTHENTICATION // ============================================================================= model User { id String @id @default(cuid()) email String @unique name String? emailVerified DateTime? // Required by NextAuth Prisma adapter role UserRole @default(JURY_MEMBER) status UserStatus @default(INVITED) expertiseTags String[] @default([]) maxAssignments Int? // Per-round limit metadataJson Json? @db.JsonB // Profile image profileImageKey String? // Storage key (e.g., "avatars/user123/1234567890.jpg") profileImageProvider String? // Storage provider used: 's3' or 'local' // Phone and notification preferences (Phase 2) phoneNumber String? phoneNumberVerified Boolean @default(false) notificationPreference NotificationChannel @default(EMAIL) whatsappOptIn Boolean @default(false) // Onboarding (Phase 2B) onboardingCompletedAt DateTime? // Password authentication (hybrid auth) passwordHash String? // bcrypt hashed password passwordSetAt DateTime? // When password was set mustSetPassword Boolean @default(true) // Force setup on first login // Invitation token for one-click invite acceptance inviteToken String? @unique inviteTokenExpiresAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lastLoginAt DateTime? // Relations assignments Assignment[] auditLogs AuditLog[] gracePeriods GracePeriod[] grantedGracePeriods GracePeriod[] @relation("GrantedBy") notificationLogs NotificationLog[] createdResources LearningResource[] @relation("ResourceCreatedBy") resourceAccess ResourceAccess[] submittedProjects Project[] @relation("ProjectSubmittedBy") liveVotes LiveVote[] // Team membership & mentorship teamMemberships TeamMember[] mentorAssignments MentorAssignment[] @relation("MentorAssignments") // Awards awardJurorships AwardJuror[] awardVotes AwardVote[] // Filtering overrides filteringOverrides FilteringResult[] @relation("FilteringOverriddenBy") // Award overrides awardEligibilityOverrides AwardEligibility[] @relation("AwardEligibilityOverriddenBy") // NextAuth relations accounts Account[] sessions Session[] @@index([email]) @@index([role]) @@index([status]) } // NextAuth.js required models model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@index([userId]) } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) } // ============================================================================= // PROGRAMS & ROUNDS // ============================================================================= model Program { id String @id @default(cuid()) name String // e.g., "Monaco Ocean Protection Challenge" year Int // e.g., 2026 status ProgramStatus @default(DRAFT) description String? settingsJson Json? @db.JsonB createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations rounds Round[] projects Project[] learningResources LearningResource[] partners Partner[] applicationForms ApplicationForm[] specialAwards SpecialAward[] @@unique([name, year]) @@index([status]) } model Round { id String @id @default(cuid()) programId String name String // e.g., "Round 1 - Semi-Finalists" slug String? @unique // URL-friendly identifier for public submissions status RoundStatus @default(DRAFT) roundType RoundType @default(EVALUATION) sortOrder Int @default(0) // Progression order within program // Submission window (for applicant portal) submissionDeadline DateTime? // Deadline for project submissions submissionStartDate DateTime? // When submissions open submissionEndDate DateTime? // When submissions close (replaces submissionDeadline if set) lateSubmissionGrace Int? // Hours of grace period after deadline // Phase-specific deadlines phase1Deadline DateTime? phase2Deadline DateTime? // Voting window votingStartAt DateTime? votingEndAt DateTime? // Configuration requiredReviews Int @default(3) // Min evaluations per project settingsJson Json? @db.JsonB // Grace periods, visibility rules, etc. createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations program Program @relation(fields: [programId], references: [id], onDelete: Cascade) roundProjects RoundProject[] assignments Assignment[] evaluationForms EvaluationForm[] gracePeriods GracePeriod[] liveVotingSession LiveVotingSession? filteringRules FilteringRule[] filteringResults FilteringResult[] filteringJobs FilteringJob[] applicationForm ApplicationForm? @@index([programId]) @@index([status]) @@index([roundType]) @@index([votingStartAt, votingEndAt]) @@index([submissionStartDate, submissionEndDate]) } model EvaluationForm { id String @id @default(cuid()) roundId String version Int @default(1) // Form configuration // criteriaJson: Array of { id, label, description, scale, weight, required } criteriaJson Json @db.JsonB // scalesJson: { "1-5": { min, max, labels }, "1-10": { min, max, labels } } scalesJson Json? @db.JsonB isActive Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) evaluations Evaluation[] @@unique([roundId, version]) @@index([roundId, isActive]) } // ============================================================================= // PROJECTS // ============================================================================= model Project { id String @id @default(cuid()) programId String // Core fields title String teamName String? description String? @db.Text // Competition category competitionCategory CompetitionCategory? oceanIssue OceanIssue? // Location country String? geographicZone String? // "Europe, France" // Institution (for students/Business Concepts) institution String? // Mentorship wantsMentorship Boolean @default(false) // Founding date foundedAt DateTime? // When the project/company was founded // Submission links (external, from CSV) phase1SubmissionUrl String? phase2SubmissionUrl String? // Referral tracking referralSource String? // Internal admin fields internalComments String? @db.Text applicationStatus String? // "Received", etc. // Submission tracking submissionSource SubmissionSource @default(MANUAL) submittedByEmail String? submittedAt DateTime? submittedByUserId String? // Project branding logoKey String? // Storage key (e.g., "logos/project456/1234567890.png") logoProvider String? // Storage provider used: 's3' or 'local' // Flexible fields tags String[] @default([]) // "Ocean Conservation", "Tech", etc. metadataJson Json? @db.JsonB // Custom fields from Typeform, etc. externalIdsJson Json? @db.JsonB // Typeform ID, Notion ID, etc. createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations program Program @relation(fields: [programId], references: [id], onDelete: Cascade) roundProjects RoundProject[] files ProjectFile[] assignments Assignment[] submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull) teamMembers TeamMember[] mentorAssignment MentorAssignment? filteringResults FilteringResult[] awardEligibilities AwardEligibility[] awardVotes AwardVote[] wonAwards SpecialAward[] @relation("AwardWinner") @@index([programId]) @@index([tags]) @@index([submissionSource]) @@index([submittedByUserId]) @@index([competitionCategory]) @@index([oceanIssue]) @@index([country]) } model RoundProject { id String @id @default(cuid()) roundId String projectId String status ProjectStatus @default(SUBMITTED) addedAt DateTime @default(now()) // Relations round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@unique([roundId, projectId]) @@index([roundId]) @@index([projectId]) @@index([status]) } model ProjectFile { id String @id @default(cuid()) projectId String // File info fileType FileType fileName String mimeType String size Int // bytes // MinIO location bucket String objectKey String createdAt DateTime @default(now()) // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@unique([bucket, objectKey]) @@index([projectId]) @@index([fileType]) } // ============================================================================= // ASSIGNMENTS & EVALUATIONS // ============================================================================= model Assignment { id String @id @default(cuid()) userId String projectId String roundId String // Assignment info method AssignmentMethod @default(MANUAL) isRequired Boolean @default(true) isCompleted Boolean @default(false) // AI assignment metadata aiConfidenceScore Float? // 0-1 confidence from AI expertiseMatchScore Float? // 0-1 match score aiReasoning String? @db.Text createdAt DateTime @default(now()) createdBy String? // Admin who created the assignment // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) evaluation Evaluation? @@unique([userId, projectId, roundId]) @@index([userId]) @@index([projectId]) @@index([roundId]) @@index([isCompleted]) } model Evaluation { id String @id @default(cuid()) assignmentId String @unique formId String // Status status EvaluationStatus @default(NOT_STARTED) // Scores // criterionScoresJson: { "criterion_id": score, ... } criterionScoresJson Json? @db.JsonB globalScore Int? // 1-10 binaryDecision Boolean? // Yes/No for semi-finalist feedbackText String? @db.Text // Versioning version Int @default(1) // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt submittedAt DateTime? // Relations assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade) form EvaluationForm @relation(fields: [formId], references: [id]) @@index([status]) @@index([submittedAt]) @@index([formId]) } // ============================================================================= // GRACE PERIODS // ============================================================================= model GracePeriod { id String @id @default(cuid()) roundId String userId String projectId String? // Optional: specific project or all projects in round extendedUntil DateTime reason String? @db.Text grantedById String createdAt DateTime @default(now()) // Relations round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) grantedBy User @relation("GrantedBy", fields: [grantedById], references: [id]) @@index([roundId]) @@index([userId]) @@index([extendedUntil]) @@index([grantedById]) @@index([projectId]) } // ============================================================================= // SYSTEM SETTINGS // ============================================================================= model SystemSettings { id String @id @default(cuid()) key String @unique value String @db.Text type SettingType @default(STRING) category SettingCategory description String? isSecret Boolean @default(false) // If true, value is encrypted updatedAt DateTime @updatedAt updatedBy String? @@index([category]) } // ============================================================================= // AUDIT LOGGING // ============================================================================= model AuditLog { id String @id @default(cuid()) userId String? // Event info action String // "CREATE", "UPDATE", "DELETE", "LOGIN", "EXPORT", etc. entityType String // "Round", "Project", "Evaluation", etc. entityId String? // Details detailsJson Json? @db.JsonB // Before/after values, additional context // Request info ipAddress String? userAgent String? timestamp DateTime @default(now()) // Relations user User? @relation(fields: [userId], references: [id], onDelete: SetNull) @@index([userId]) @@index([action]) @@index([entityType, entityId]) @@index([timestamp]) } // ============================================================================= // AI USAGE TRACKING // ============================================================================= model AIUsageLog { id String @id @default(cuid()) createdAt DateTime @default(now()) // Who/what triggered it userId String? action String // ASSIGNMENT, FILTERING, AWARD_ELIGIBILITY, MENTOR_MATCHING entityType String? // Round, Project, Award entityId String? // What was used model String // gpt-4o, gpt-4o-mini, o1, etc. promptTokens Int completionTokens Int totalTokens Int // Cost tracking estimatedCostUsd Decimal? @db.Decimal(10, 6) // Request context batchSize Int? itemsProcessed Int? // Status status String // SUCCESS, PARTIAL, ERROR errorMessage String? // Detailed data (optional) detailsJson Json? @db.JsonB @@index([userId]) @@index([action]) @@index([createdAt]) @@index([model]) } // ============================================================================= // NOTIFICATION LOG (Phase 2) // ============================================================================= model NotificationLog { id String @id @default(cuid()) userId String channel NotificationChannel provider String? // META, TWILIO, SMTP type String // MAGIC_LINK, REMINDER, ANNOUNCEMENT, JURY_INVITATION status String // PENDING, SENT, DELIVERED, FAILED externalId String? // Message ID from provider errorMsg String? @db.Text createdAt DateTime @default(now()) // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([status]) @@index([createdAt]) } // ============================================================================= // LEARNING HUB (Phase 2) // ============================================================================= model LearningResource { id String @id @default(cuid()) programId String? // null = global resource title String description String? @db.Text contentJson Json? @db.JsonB // BlockNote document structure resourceType ResourceType cohortLevel CohortLevel @default(ALL) // File storage (for uploaded resources) fileName String? mimeType String? size Int? bucket String? objectKey String? // External link externalUrl String? sortOrder Int @default(0) isPublished Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt createdById String // Relations program Program? @relation(fields: [programId], references: [id], onDelete: SetNull) createdBy User @relation("ResourceCreatedBy", fields: [createdById], references: [id]) accessLogs ResourceAccess[] @@index([programId]) @@index([cohortLevel]) @@index([isPublished]) @@index([sortOrder]) } model ResourceAccess { id String @id @default(cuid()) resourceId String userId String accessedAt DateTime @default(now()) ipAddress String? // Relations resource LearningResource @relation(fields: [resourceId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([resourceId]) @@index([userId]) @@index([accessedAt]) } // ============================================================================= // PARTNER MANAGEMENT (Phase 2) // ============================================================================= model Partner { id String @id @default(cuid()) programId String? // null = global partner name String description String? @db.Text website String? partnerType PartnerType @default(PARTNER) visibility PartnerVisibility @default(ADMIN_ONLY) // Logo file logoFileName String? logoBucket String? logoObjectKey String? sortOrder Int @default(0) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations program Program? @relation(fields: [programId], references: [id], onDelete: SetNull) @@index([programId]) @@index([partnerType]) @@index([visibility]) @@index([isActive]) @@index([sortOrder]) } // ============================================================================= // APPLICATION FORMS (Phase 2) // ============================================================================= model ApplicationForm { id String @id @default(cuid()) programId String? // null = global form name String description String? @db.Text status String @default("DRAFT") // DRAFT, PUBLISHED, CLOSED isPublic Boolean @default(false) publicSlug String? @unique // /apply/ocean-challenge-2026 submissionLimit Int? opensAt DateTime? closesAt DateTime? confirmationMessage String? @db.Text // Round linking (for onboarding forms that create projects) roundId String? @unique // Email settings sendConfirmationEmail Boolean @default(true) sendTeamInviteEmails Boolean @default(true) confirmationEmailSubject String? confirmationEmailBody String? @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations program Program? @relation(fields: [programId], references: [id], onDelete: SetNull) round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull) fields ApplicationFormField[] steps OnboardingStep[] submissions ApplicationFormSubmission[] @@index([programId]) @@index([status]) @@index([isPublic]) @@index([roundId]) } model ApplicationFormField { id String @id @default(cuid()) formId String stepId String? // Which step this field belongs to (for onboarding) fieldType FormFieldType name String // Internal name (e.g., "project_title") label String // Display label (e.g., "Project Title") description String? @db.Text placeholder String? required Boolean @default(false) minLength Int? maxLength Int? minValue Float? // For NUMBER type maxValue Float? // For NUMBER type optionsJson Json? @db.JsonB // For select/radio: [{ value, label }] conditionJson Json? @db.JsonB // Conditional logic: { fieldId, operator, value } // Onboarding-specific fields projectMapping String? // Maps to Project column: "title", "description", etc. specialType SpecialFieldType? // Special handling for complex fields sortOrder Int @default(0) width String @default("full") // full, half createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade) step OnboardingStep? @relation(fields: [stepId], references: [id], onDelete: SetNull) @@index([formId]) @@index([stepId]) @@index([sortOrder]) } model OnboardingStep { id String @id @default(cuid()) formId String name String // Internal identifier (e.g., "category", "contact") title String // Display title (e.g., "Category", "Contact Information") description String? @db.Text sortOrder Int @default(0) isOptional Boolean @default(false) conditionJson Json? @db.JsonB // Conditional visibility: { fieldId, operator, value } createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade) fields ApplicationFormField[] @@index([formId]) @@index([sortOrder]) } model ApplicationFormSubmission { id String @id @default(cuid()) formId String email String? name String? dataJson Json @db.JsonB // Field values: { fieldName: value, ... } status String @default("SUBMITTED") // SUBMITTED, REVIEWED, APPROVED, REJECTED createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade) files SubmissionFile[] @@index([formId]) @@index([status]) @@index([email]) @@index([createdAt]) } model SubmissionFile { id String @id @default(cuid()) submissionId String fieldName String fileName String mimeType String? size Int? bucket String objectKey String createdAt DateTime @default(now()) // Relations submission ApplicationFormSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade) @@index([submissionId]) @@unique([bucket, objectKey]) } // ============================================================================= // EXPERTISE TAGS (Phase 2B) // ============================================================================= model ExpertiseTag { id String @id @default(cuid()) name String @unique description String? category String? // "Marine Science", "Technology", "Policy" color String? // Hex for badge isActive Boolean @default(true) sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([category]) @@index([isActive]) @@index([sortOrder]) } // ============================================================================= // LIVE VOTING (Phase 2B) // ============================================================================= model LiveVotingSession { id String @id @default(cuid()) roundId String @unique status String @default("NOT_STARTED") // NOT_STARTED, IN_PROGRESS, PAUSED, COMPLETED currentProjectIndex Int @default(0) currentProjectId String? votingStartedAt DateTime? votingEndsAt DateTime? projectOrderJson Json? @db.JsonB // Array of project IDs in presentation order createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) votes LiveVote[] @@index([status]) } model LiveVote { id String @id @default(cuid()) sessionId String projectId String userId String score Int // 1-10 votedAt DateTime @default(now()) // Relations session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([sessionId, projectId, userId]) @@index([sessionId]) @@index([projectId]) @@index([userId]) } // ============================================================================= // TEAM MEMBERSHIP // ============================================================================= model TeamMember { id String @id @default(cuid()) projectId String userId String role TeamMemberRole @default(MEMBER) title String? // "CEO", "CTO", etc. joinedAt DateTime @default(now()) // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([projectId, userId]) @@index([projectId]) @@index([userId]) @@index([role]) } // ============================================================================= // MENTOR ASSIGNMENT // ============================================================================= model MentorAssignment { id String @id @default(cuid()) projectId String @unique // One mentor per project mentorId String // User with MENTOR role or expertise // Assignment tracking method MentorAssignmentMethod @default(MANUAL) assignedAt DateTime @default(now()) assignedBy String? // Admin who assigned // AI assignment metadata aiConfidenceScore Float? expertiseMatchScore Float? aiReasoning String? @db.Text // Relations project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) mentor User @relation("MentorAssignments", fields: [mentorId], references: [id]) @@index([mentorId]) @@index([method]) } // ============================================================================= // FILTERING ROUND SYSTEM // ============================================================================= enum FilteringOutcome { PASSED FILTERED_OUT FLAGGED } enum FilteringRuleType { FIELD_BASED DOCUMENT_CHECK AI_SCREENING } model FilteringRule { id String @id @default(cuid()) roundId String name String ruleType FilteringRuleType configJson Json @db.JsonB // Conditions, logic, action per rule type priority Int @default(0) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) @@index([roundId]) @@index([priority]) } model FilteringResult { id String @id @default(cuid()) roundId String projectId String outcome FilteringOutcome ruleResultsJson Json? @db.JsonB // Per-rule results aiScreeningJson Json? @db.JsonB // AI screening details // Admin override overriddenBy String? overriddenAt DateTime? overrideReason String? @db.Text finalOutcome FilteringOutcome? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) overriddenByUser User? @relation("FilteringOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull) @@unique([roundId, projectId]) @@index([roundId]) @@index([projectId]) @@index([outcome]) } // Tracks progress of long-running filtering jobs model FilteringJob { id String @id @default(cuid()) roundId String status FilteringJobStatus @default(PENDING) totalProjects Int @default(0) totalBatches Int @default(0) currentBatch Int @default(0) processedCount Int @default(0) passedCount Int @default(0) filteredCount Int @default(0) flaggedCount Int @default(0) errorMessage String? @db.Text startedAt DateTime? completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) @@index([roundId]) @@index([status]) } enum FilteringJobStatus { PENDING RUNNING COMPLETED FAILED } // ============================================================================= // SPECIAL AWARDS SYSTEM // ============================================================================= enum AwardScoringMode { PICK_WINNER RANKED SCORED } enum AwardStatus { DRAFT NOMINATIONS_OPEN VOTING_OPEN CLOSED ARCHIVED } enum EligibilityMethod { AUTO MANUAL } model SpecialAward { id String @id @default(cuid()) programId String name String description String? @db.Text status AwardStatus @default(DRAFT) // Criteria criteriaText String? @db.Text // Plain-language criteria for AI autoTagRulesJson Json? @db.JsonB // Deterministic eligibility rules useAiEligibility Boolean @default(true) // Whether AI evaluates eligibility // Scoring scoringMode AwardScoringMode @default(PICK_WINNER) maxRankedPicks Int? // For RANKED mode // Voting window votingStartAt DateTime? votingEndAt DateTime? // Evaluation form (for SCORED mode) evaluationFormId String? // Winner winnerProjectId String? winnerOverridden Boolean @default(false) winnerOverriddenBy String? sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations program Program @relation(fields: [programId], references: [id], onDelete: Cascade) winnerProject Project? @relation("AwardWinner", fields: [winnerProjectId], references: [id], onDelete: SetNull) eligibilities AwardEligibility[] jurors AwardJuror[] votes AwardVote[] @@index([programId]) @@index([status]) @@index([sortOrder]) } model AwardEligibility { id String @id @default(cuid()) awardId String projectId String method EligibilityMethod @default(AUTO) eligible Boolean @default(false) aiReasoningJson Json? @db.JsonB // Admin override overriddenBy String? overriddenAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) overriddenByUser User? @relation("AwardEligibilityOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull) @@unique([awardId, projectId]) @@index([awardId]) @@index([projectId]) @@index([eligible]) } model AwardJuror { id String @id @default(cuid()) awardId String userId String createdAt DateTime @default(now()) // Relations award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([awardId, userId]) @@index([awardId]) @@index([userId]) } model AwardVote { id String @id @default(cuid()) awardId String userId String projectId String rank Int? // For RANKED mode votedAt DateTime @default(now()) // Relations award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@unique([awardId, userId, projectId]) @@index([awardId]) @@index([userId]) @@index([projectId]) }