MOPC-App/prisma/schema.prisma

2592 lines
80 KiB
Plaintext
Raw Normal View History

// =============================================================================
// 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
AWARD_MASTER
AUDIENCE
}
enum UserStatus {
NONE
INVITED
ACTIVE
SUSPENDED
}
enum ProgramStatus {
DRAFT
ACTIVE
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 SettingType {
STRING
NUMBER
BOOLEAN
JSON
SECRET
}
enum SettingCategory {
AI
BRANDING
EMAIL
STORAGE
SECURITY
DEFAULTS
WHATSAPP
AUDIT_CONFIG
LOCALIZATION
DIGEST
ANALYTICS
INTEGRATIONS
COMMUNICATION
FEATURE_FLAGS
}
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 OverrideReasonCode {
DATA_CORRECTION
POLICY_EXCEPTION
JURY_CONFLICT
SPONSOR_DECISION
ADMIN_DISCRETION
}
// =============================================================================
// COMPETITION / ROUND ENGINE ENUMS
// =============================================================================
enum CompetitionStatus {
DRAFT
ACTIVE
CLOSED
ARCHIVED
}
enum RoundType {
INTAKE
FILTERING
EVALUATION
SUBMISSION
MENTORING
LIVE_FINAL
DELIBERATION
}
enum RoundStatus {
ROUND_DRAFT
ROUND_ACTIVE
ROUND_CLOSED
ROUND_ARCHIVED
}
enum ProjectRoundStateValue {
PENDING
IN_PROGRESS
PASSED
REJECTED
COMPLETED
WITHDRAWN
}
enum AdvancementRuleType {
AUTO_ADVANCE
SCORE_THRESHOLD
TOP_N
ADMIN_SELECTION
AI_RECOMMENDED
}
enum CapMode {
HARD
SOFT
NONE
}
enum DeadlinePolicy {
HARD_DEADLINE
FLAG
GRACE
}
enum JuryGroupMemberRole {
CHAIR
MEMBER
OBSERVER
}
enum AssignmentIntentSource {
INVITE
ADMIN
SYSTEM
}
enum AssignmentIntentStatus {
INTENT_PENDING
HONORED
OVERRIDDEN
EXPIRED
CANCELLED
}
enum MentorMessageRole {
MENTOR_ROLE
APPLICANT_ROLE
ADMIN_ROLE
}
enum SubmissionPromotionSource {
MENTOR_FILE
ADMIN_REPLACEMENT
}
enum DeliberationMode {
SINGLE_WINNER_VOTE
FULL_RANKING
}
enum DeliberationStatus {
DELIB_OPEN
VOTING
TALLYING
RUNOFF
DELIB_LOCKED
}
enum TieBreakMethod {
TIE_RUNOFF
TIE_ADMIN_DECIDES
SCORE_FALLBACK
}
enum DeliberationParticipantStatus {
REQUIRED
ABSENT_EXCUSED
REPLACED
REPLACEMENT_ACTIVE
}
enum AwardEligibilityMode {
SEPARATE_POOL
STAY_IN_MAIN
}
// =============================================================================
// 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
country String? // User's home country (for mentor matching)
metadataJson Json? @db.JsonB
// Profile
bio String? // User bio for matching with project descriptions
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?
// Digest & availability preferences
digestFrequency String @default("none") // 'none' | 'daily' | 'weekly'
preferredWorkload Int?
availabilityJson Json? @db.JsonB // { startDate?: string, endDate?: string }
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")
awardWinnerOverrides SpecialAward[] @relation("AwardOverriddenBy")
// In-app notifications
notifications InAppNotification[] @relation("UserNotifications")
notificationSettingsUpdated NotificationEmailSetting[] @relation("NotificationSettingUpdater")
// Reminder logs
reminderLogs ReminderLog[]
// Conflict of interest
conflictsOfInterest ConflictOfInterest[]
coiReviews ConflictOfInterest[] @relation("COIReviewedBy")
// Evaluation summaries
generatedSummaries EvaluationSummary[] @relation("EvaluationSummaryGeneratedBy")
// Mentor messages
mentorMessages MentorMessage[] @relation("MentorMessageSender")
// Wizard templates
wizardTemplates WizardTemplate[] @relation("WizardTemplateCreatedBy")
// Mentor notes
mentorNotes MentorNote[] @relation("MentorNoteAuthor")
// Milestone completions
milestoneCompletions MentorMilestoneCompletion[] @relation("MilestoneCompletedBy")
// Evaluation discussions
closedDiscussions EvaluationDiscussion[] @relation("DiscussionClosedBy")
discussionComments DiscussionComment[] @relation("DiscussionCommentAuthor")
// Messaging
sentMessages Message[] @relation("MessageSender")
receivedMessages MessageRecipient[] @relation("MessageRecipient")
messageTemplates MessageTemplate[] @relation("MessageTemplateCreator")
// Webhooks
webhooks Webhook[] @relation("WebhookCreator")
// Digest logs
digestLogs DigestLog[] @relation("DigestLog")
// NextAuth relations
accounts Account[]
sessions Session[]
// ── Competition/Round architecture relations ──
juryGroupMemberships JuryGroupMember[]
mentorFilesUploaded MentorFile[] @relation("MentorFileUploader")
mentorFilesPromoted MentorFile[] @relation("MentorFilePromoter")
mentorFileComments MentorFileComment[] @relation("MentorFileCommentAuthor")
resultLocksCreated ResultLock[] @relation("ResultLockCreator")
resultUnlockEvents ResultUnlockEvent[] @relation("ResultUnlocker")
assignmentExceptionsApproved AssignmentException[] @relation("AssignmentExceptionApprover")
submissionPromotions SubmissionPromotionEvent[] @relation("SubmissionPromoter")
deliberationReplacements DeliberationParticipant[] @relation("DeliberationReplacement")
@@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"
slug String? @unique // URL-friendly identifier for edition-wide applications
year Int // e.g., 2026
status ProgramStatus @default(DRAFT)
description String?
settingsJson Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
projects Project[]
learningResources LearningResource[]
partners Partner[]
specialAwards SpecialAward[]
taggingJobs TaggingJob[]
wizardTemplates WizardTemplate[]
mentorMilestones MentorMilestone[]
competitions Competition[]
@@unique([name, year])
@@index([status])
}
model WizardTemplate {
id String @id @default(cuid())
name String
description String?
config Json @db.JsonB
isGlobal Boolean @default(false)
programId String?
program Program? @relation(fields: [programId], references: [id], onDelete: Cascade)
createdBy String
creator User @relation("WizardTemplateCreatedBy", fields: [createdBy], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([programId])
@@index([isGlobal])
}
// =============================================================================
// EVALUATION FORMS
// =============================================================================
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
roundId String?
status ProjectStatus @default(SUBMITTED)
// 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'
// Draft support
isDraft Boolean @default(false)
draftDataJson Json? @db.JsonB // Form data for drafts
draftExpiresAt DateTime?
// 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)
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")
projectTags ProjectTag[]
statusHistory ProjectStatusHistory[]
mentorMessages MentorMessage[]
evaluationSummaries EvaluationSummary[]
evaluationDiscussions EvaluationDiscussion[]
cohortProjects CohortProject[]
// ── Competition/Round architecture relations ──
projectRoundStates ProjectRoundState[]
assignmentIntents AssignmentIntent[]
deliberationVotes DeliberationVote[]
deliberationResults DeliberationResult[]
submissionPromotions SubmissionPromotionEvent[]
@@index([programId])
@@index([status])
@@index([tags])
@@index([submissionSource])
@@index([submittedByUserId])
@@index([competitionCategory])
@@index([oceanIssue])
@@index([country])
}
model FileRequirement {
id String @id @default(cuid())
roundId String
name String
description String?
acceptedMimeTypes String[] // e.g. ["application/pdf", "video/*"]
maxSizeMB Int? // Max file size in MB
isRequired Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
files ProjectFile[]
@@index([roundId])
}
model ProjectFile {
id String @id @default(cuid())
projectId String
roundId String? // Which round this file was submitted for
requirementId String? // FK to FileRequirement (if uploaded against a requirement)
// File info
fileType FileType
fileName String
mimeType String
size Int // bytes
// MinIO location
bucket String
objectKey String
isLate Boolean @default(false) // Uploaded after round deadline
// Versioning
version Int @default(1)
replacedById String? // FK to the newer file that replaced this one
// ── Competition/Round architecture fields ──
submissionWindowId String? // FK to SubmissionWindow
submissionFileRequirementId String? // FK to SubmissionFileRequirement
createdAt DateTime @default(now())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
requirement FileRequirement? @relation(fields: [requirementId], references: [id], onDelete: SetNull)
replacedBy ProjectFile? @relation("FileVersions", fields: [replacedById], references: [id], onDelete: SetNull)
replacements ProjectFile[] @relation("FileVersions")
submissionWindow SubmissionWindow? @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull)
submissionFileRequirement SubmissionFileRequirement? @relation(fields: [submissionFileRequirementId], references: [id], onDelete: SetNull)
promotedFrom MentorFile? @relation("PromotedFromMentorFile")
@@unique([bucket, objectKey])
@@index([projectId])
@@index([fileType])
@@index([requirementId])
@@index([submissionWindowId])
@@index([submissionFileRequirementId])
}
// =============================================================================
// 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
// Competition/Round architecture — jury group link
juryGroupId String?
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
evaluation Evaluation?
conflictOfInterest ConflictOfInterest?
exceptions AssignmentException[]
@@unique([userId, projectId, roundId])
@@index([roundId])
@@index([userId])
@@index([projectId])
@@index([isCompleted])
@@index([projectId, userId])
@@index([juryGroupId])
}
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 (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
submittedAt DateTime?
// Relations
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
form EvaluationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
@@index([status])
@@index([submittedAt])
@@index([formId])
@@index([status, 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])
@@index([roundId, userId, extendedUntil])
}
// =============================================================================
// 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
previousDataJson Json? @db.JsonB // Previous state for tracking changes
// Request info
ipAddress String?
userAgent String?
sessionId String?
timestamp DateTime @default(now())
// Relations
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([action])
@@index([entityType, entityId])
@@index([timestamp])
@@index([entityType, entityId, timestamp])
@@index([sessionId])
}
// =============================================================================
// 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])
}
// =============================================================================
// IN-APP NOTIFICATIONS
// =============================================================================
model InAppNotification {
id String @id @default(cuid())
userId String
type String // FILTERING_COMPLETE, NEW_APPLICATION, ASSIGNED_TO_PROJECT, etc.
priority String @default("normal") // low, normal, high, urgent
icon String? // lucide icon name
title String
message String @db.Text
linkUrl String? // Where to navigate when clicked
linkLabel String? // CTA text
metadata Json? @db.JsonB // Extra context (projectId, roundId, etc.)
groupKey String? // For batching similar notifications
isRead Boolean @default(false)
readAt DateTime?
expiresAt DateTime? // Auto-dismiss after date
createdAt DateTime @default(now())
// Relations
user User @relation("UserNotifications", fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, isRead])
@@index([userId, createdAt])
@@index([groupKey])
}
model NotificationEmailSetting {
id String @id @default(cuid())
notificationType String @unique // e.g., "ADVANCED_TO_ROUND", "ASSIGNED_TO_PROJECT"
category String // "team", "jury", "mentor", "admin"
label String // Human-readable label for admin UI
description String? // Help text
sendEmail Boolean @default(true)
emailSubject String? // Custom subject template (optional)
emailTemplate String? @db.Text // Custom body template (optional)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
updatedById String?
updatedBy User? @relation("NotificationSettingUpdater", fields: [updatedById], references: [id])
@@index([category])
}
// =============================================================================
// 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])
}
// =============================================================================
// 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
// Relations
projectTags ProjectTag[]
@@index([category])
@@index([isActive])
@@index([sortOrder])
}
// Project-Tag relationship for AI tagging
model ProjectTag {
id String @id @default(cuid())
projectId String
tagId String
confidence Float @default(1.0) // AI confidence score 0-1
source String @default("AI") // "AI" or "MANUAL"
createdAt DateTime @default(now())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
tag ExpertiseTag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([projectId, tagId])
@@index([projectId])
@@index([tagId])
}
// =============================================================================
// 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
// Criteria-based voting
votingMode String @default("simple") // "simple" (1-10) | "criteria" (per-criterion scores)
criteriaJson Json? @db.JsonB // Array of { id, label, description, scale, weight }
// Audience & presentation settings
allowAudienceVotes Boolean @default(false)
audienceVoteWeight Float @default(0) // 0.0 to 1.0
tieBreakerMethod String @default("admin_decides") // 'admin_decides' | 'highest_individual' | 'revote'
presentationSettingsJson Json? @db.JsonB
// Audience voting configuration
audienceVotingMode String @default("disabled") // "disabled" | "per_project" | "per_category" | "favorites"
audienceMaxFavorites Int @default(3) // For "favorites" mode
audienceRequireId Boolean @default(false) // Require email/phone for audience
audienceVotingDuration Int? // Minutes (null = same as jury)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round? @relation(fields: [roundId], references: [id], onDelete: Cascade)
votes LiveVote[]
audienceVoters AudienceVoter[]
@@index([status])
}
model LiveVote {
id String @id @default(cuid())
sessionId String
projectId String
userId String? // Nullable for audience voters without accounts
score Int // 1-10 (or weighted score for criteria mode)
isAudienceVote Boolean @default(false)
votedAt DateTime @default(now())
// Criteria scores (used when votingMode="criteria")
criterionScoresJson Json? @db.JsonB // { [criterionId]: score } - null for simple mode
// Audience voter link
audienceVoterId String?
// Relations
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
audienceVoter AudienceVoter? @relation(fields: [audienceVoterId], references: [id], onDelete: Cascade)
@@unique([sessionId, projectId, userId])
@@unique([sessionId, projectId, audienceVoterId])
@@index([sessionId])
@@index([projectId])
@@index([userId])
@@index([audienceVoterId])
}
model AudienceVoter {
id String @id @default(cuid())
sessionId String
token String @unique // Unique voting token (UUID)
identifier String? // Optional: email, phone, or name
identifierType String? // "email" | "phone" | "name" | "anonymous"
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
// Relations
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
votes LiveVote[]
@@index([sessionId])
@@index([token])
}
// =============================================================================
// 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
// Tracking
completionStatus String @default("in_progress") // 'in_progress' | 'completed' | 'paused'
lastViewedAt DateTime?
// ── Competition/Round architecture — workspace activation ──
workspaceEnabled Boolean @default(false)
workspaceOpenAt DateTime?
workspaceCloseAt DateTime?
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
mentor User @relation("MentorAssignments", fields: [mentorId], references: [id])
notes MentorNote[]
milestoneCompletions MentorMilestoneCompletion[]
messages MentorMessage[]
files MentorFile[]
@@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
}
// Tracks progress of long-running AI assignment jobs
model AssignmentJob {
id String @id @default(cuid())
roundId String
status AssignmentJobStatus @default(PENDING)
totalProjects Int @default(0)
totalBatches Int @default(0)
currentBatch Int @default(0)
processedCount Int @default(0)
suggestionsCount Int @default(0)
suggestionsJson Json? @db.JsonB // Stores the AI-generated suggestions
errorMessage String? @db.Text
startedAt DateTime?
completedAt DateTime?
fallbackUsed Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
@@index([roundId])
@@index([status])
}
enum AssignmentJobStatus {
PENDING
RUNNING
COMPLETED
FAILED
}
// Tracks progress of long-running AI tagging jobs
model TaggingJob {
id String @id @default(cuid())
programId String? // If tagging entire program
roundId String? // If tagging single round
status TaggingJobStatus @default(PENDING)
totalProjects Int @default(0)
processedCount Int @default(0)
taggedCount Int @default(0)
skippedCount Int @default(0)
failedCount Int @default(0)
errorMessage String? @db.Text
errorsJson Json? @db.JsonB // Array of error messages
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations (optional - can tag by program)
program Program? @relation(fields: [programId], references: [id], onDelete: Cascade)
@@index([programId])
@@index([status])
}
enum TaggingJobStatus {
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? // FK to User who overrode the winner
sortOrder Int @default(0)
// ── Competition/Round architecture fields ──
competitionId String?
evaluationRoundId String?
juryGroupId String?
eligibilityMode AwardEligibilityMode @default(STAY_IN_MAIN)
decisionMode String? // "JURY_VOTE" | "AWARD_MASTER_DECISION" | "ADMIN_DECISION"
// Eligibility job tracking
eligibilityJobStatus String? // PENDING, PROCESSING, COMPLETED, FAILED
eligibilityJobTotal Int? // total projects to process
eligibilityJobDone Int? // completed so far
eligibilityJobError String? @db.Text // error message if failed
eligibilityJobStarted DateTime? // when job started
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)
overriddenByUser User? @relation("AwardOverriddenBy", fields: [winnerOverriddenBy], references: [id], onDelete: SetNull)
eligibilities AwardEligibility[]
jurors AwardJuror[]
votes AwardVote[]
// Competition/Round architecture relations
competition Competition? @relation(fields: [competitionId], references: [id], onDelete: SetNull)
evaluationRound Round? @relation(fields: [evaluationRoundId], references: [id], onDelete: SetNull)
awardJuryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
@@index([programId])
@@index([status])
@@index([sortOrder])
@@index([competitionId])
@@index([evaluationRoundId])
}
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])
@@index([awardId, 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])
@@index([awardId, userId])
}
// =============================================================================
// REMINDER LOG (Evaluation Deadline Reminders)
// =============================================================================
model ReminderLog {
id String @id @default(cuid())
roundId String
userId String
type String // "3_DAYS", "24H", "1H"
sentAt DateTime @default(now())
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([roundId, userId, type])
@@index([roundId])
}
// =============================================================================
// CONFLICT OF INTEREST
// =============================================================================
model ConflictOfInterest {
id String @id @default(cuid())
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
declaredAt DateTime @default(now())
// Admin review
reviewedById String?
reviewedAt DateTime?
reviewAction String? // "cleared", "reassigned", "noted"
// Relations
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
reviewedBy User? @relation("COIReviewedBy", fields: [reviewedById], references: [id])
@@index([userId])
@@index([hasConflict])
}
// =============================================================================
// AI EVALUATION SUMMARY
// =============================================================================
model EvaluationSummary {
id String @id @default(cuid())
projectId String
roundId String
summaryJson Json @db.JsonB
generatedAt DateTime @default(now())
generatedById String
model String
tokensUsed Int
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
generatedBy User @relation("EvaluationSummaryGeneratedBy", fields: [generatedById], references: [id])
@@unique([projectId, roundId])
@@index([roundId])
}
// =============================================================================
// PROJECT STATUS HISTORY
// =============================================================================
model ProjectStatusHistory {
id String @id @default(cuid())
projectId String
status ProjectStatus
changedAt DateTime @default(now())
changedBy String?
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId, changedAt])
}
// =============================================================================
// MENTOR MESSAGES
// =============================================================================
model MentorMessage {
id String @id @default(cuid())
projectId String
senderId String
message String @db.Text
isRead Boolean @default(false)
createdAt DateTime @default(now())
// ── Competition/Round architecture fields ──
workspaceId String? // FK to MentorAssignment (used as workspace)
senderRole MentorMessageRole?
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
sender User @relation("MentorMessageSender", fields: [senderId], references: [id])
workspace MentorAssignment? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@index([projectId, createdAt])
@@index([workspaceId])
}
// RoundTemplate model RETIRED in Phase 6 — replaced by WizardTemplate for pipelines.
// =============================================================================
// MENTOR NOTES & MILESTONES
// =============================================================================
model MentorNote {
id String @id @default(cuid())
mentorAssignmentId String
authorId String
content String @db.Text
isVisibleToAdmin Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
author User @relation("MentorNoteAuthor", fields: [authorId], references: [id])
@@index([mentorAssignmentId])
@@index([authorId])
}
model MentorMilestone {
id String @id @default(cuid())
programId String
name String
description String? @db.Text
isRequired Boolean @default(false)
deadlineOffsetDays Int?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
completions MentorMilestoneCompletion[]
@@index([programId])
@@index([sortOrder])
}
model MentorMilestoneCompletion {
id String @id @default(cuid())
milestoneId String
mentorAssignmentId String
completedById String
completedAt DateTime @default(now())
// Relations
milestone MentorMilestone @relation(fields: [milestoneId], references: [id], onDelete: Cascade)
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
completedBy User @relation("MilestoneCompletedBy", fields: [completedById], references: [id])
@@unique([milestoneId, mentorAssignmentId])
@@index([mentorAssignmentId])
@@index([completedById])
}
// =============================================================================
// EVALUATION DISCUSSIONS
// =============================================================================
model EvaluationDiscussion {
id String @id @default(cuid())
projectId String
roundId String
status String @default("open") // 'open' | 'closed'
createdAt DateTime @default(now())
closedAt DateTime?
closedById String?
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
closedBy User? @relation("DiscussionClosedBy", fields: [closedById], references: [id], onDelete: SetNull)
comments DiscussionComment[]
@@unique([projectId, roundId])
@@index([roundId])
@@index([closedById])
}
model DiscussionComment {
id String @id @default(cuid())
discussionId String
userId String
content String @db.Text
createdAt DateTime @default(now())
// Relations
discussion EvaluationDiscussion @relation(fields: [discussionId], references: [id], onDelete: Cascade)
user User @relation("DiscussionCommentAuthor", fields: [userId], references: [id])
@@index([discussionId])
@@index([userId])
}
// =============================================================================
// MESSAGING SYSTEM
// =============================================================================
model Message {
id String @id @default(cuid())
senderId String
recipientType String // 'USER', 'ROLE', 'ROUND_JURY', 'PROGRAM_TEAM', 'ALL'
recipientFilter Json? @db.JsonB
roundId String?
templateId String?
subject String
body String @db.Text
deliveryChannels String[]
scheduledAt DateTime?
sentAt DateTime?
metadata Json? @db.JsonB
createdAt DateTime @default(now())
// Relations
sender User @relation("MessageSender", fields: [senderId], references: [id])
round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
template MessageTemplate? @relation(fields: [templateId], references: [id], onDelete: SetNull)
recipients MessageRecipient[]
@@index([senderId])
@@index([roundId])
@@index([sentAt])
}
model MessageRecipient {
id String @id @default(cuid())
messageId String
userId String
channel String // 'EMAIL', 'IN_APP', etc.
isRead Boolean @default(false)
readAt DateTime?
deliveredAt DateTime?
// Relations
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
user User @relation("MessageRecipient", fields: [userId], references: [id], onDelete: Cascade)
@@unique([messageId, userId, channel])
@@index([userId])
}
model MessageTemplate {
id String @id @default(cuid())
name String
category String // 'SYSTEM', 'EVALUATION', 'ASSIGNMENT'
subject String
body String @db.Text
variables Json? @db.JsonB
isActive Boolean @default(true)
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
creator User @relation("MessageTemplateCreator", fields: [createdBy], references: [id])
messages Message[]
@@index([category])
@@index([isActive])
}
// =============================================================================
// WEBHOOKS
// =============================================================================
model Webhook {
id String @id @default(cuid())
name String
url String
secret String
events String[]
headers Json? @db.JsonB
maxRetries Int @default(3)
isActive Boolean @default(true)
createdById String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
createdBy User @relation("WebhookCreator", fields: [createdById], references: [id])
deliveries WebhookDelivery[]
@@index([isActive])
@@index([createdById])
}
model WebhookDelivery {
id String @id @default(cuid())
webhookId String
event String
payload Json @db.JsonB
status String @default("PENDING") // 'PENDING', 'DELIVERED', 'FAILED'
responseStatus Int?
responseBody String? @db.Text
attempts Int @default(0)
lastAttemptAt DateTime?
createdAt DateTime @default(now())
// Relations
webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
@@index([webhookId])
@@index([status])
@@index([event])
}
// =============================================================================
// DIGEST LOGS
// =============================================================================
model DigestLog {
id String @id @default(cuid())
userId String
digestType String // 'daily' | 'weekly'
contentJson Json @db.JsonB
sentAt DateTime @default(now())
// Relations
user User @relation("DigestLog", fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([sentAt])
}
// =============================================================================
// COHORT & LIVE PROGRESS (formerly part of Stage engine, now refit to Round)
// =============================================================================
model Cohort {
id String @id @default(cuid())
roundId String
name String
votingMode String @default("simple") // simple, criteria, ranked
isOpen Boolean @default(false)
windowOpenAt DateTime?
windowCloseAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
projects CohortProject[]
@@index([roundId])
@@index([isOpen])
}
model CohortProject {
id String @id @default(cuid())
cohortId String
projectId String
sortOrder Int @default(0)
createdAt DateTime @default(now())
// Relations
cohort Cohort @relation(fields: [cohortId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([cohortId, projectId])
@@index([cohortId])
@@index([projectId])
@@index([sortOrder])
}
model LiveProgressCursor {
id String @id @default(cuid())
roundId String @unique
sessionId String @unique @default(cuid())
activeProjectId String?
activeOrderIndex Int @default(0)
isPaused Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
@@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.
entityType String
entityId String
actorId String?
detailsJson Json? @db.JsonB
snapshotJson Json? @db.JsonB // State at time of decision
createdAt DateTime @default(now())
@@index([eventType])
@@index([entityType, entityId])
@@index([actorId])
@@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)
// =============================================================================
model Competition {
id String @id @default(cuid())
programId String
name String
slug String @unique
status CompetitionStatus @default(DRAFT)
// Competition-wide settings
categoryMode String @default("SHARED")
startupFinalistCount Int @default(3)
conceptFinalistCount Int @default(3)
// Notification preferences
notifyOnRoundAdvance Boolean @default(true)
notifyOnDeadlineApproach Boolean @default(true)
deadlineReminderDays Int[] @default([7, 3, 1])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
rounds Round[]
juryGroups JuryGroup[]
submissionWindows SubmissionWindow[]
specialAwards SpecialAward[]
deliberationSessions DeliberationSession[]
resultLocks ResultLock[]
@@index([programId])
@@index([status])
}
model Round {
id String @id @default(cuid())
competitionId String
name String
slug String
roundType RoundType
status RoundStatus @default(ROUND_DRAFT)
sortOrder Int @default(0)
// Time windows
windowOpenAt DateTime?
windowCloseAt DateTime?
// Round-type-specific configuration (validated by Zod per RoundType)
configJson Json? @db.JsonB
// Semantic analytics tag
purposeKey String?
// Links to other entities
juryGroupId String?
submissionWindowId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
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[]
resultLocks ResultLock[]
submissionPromotions SubmissionPromotionEvent[]
specialAwards SpecialAward[]
// Reverse relations from refitted models
evaluationForms EvaluationForm[]
fileRequirements FileRequirement[]
assignments Assignment[]
gracePeriods GracePeriod[]
liveVotingSession LiveVotingSession?
filteringRules FilteringRule[]
filteringResults FilteringResult[]
filteringJobs FilteringJob[]
assignmentJobs AssignmentJob[]
reminderLogs ReminderLog[]
evaluationSummaries EvaluationSummary[]
evaluationDiscussions EvaluationDiscussion[]
messages Message[]
cohorts Cohort[]
liveCursor LiveProgressCursor?
@@unique([competitionId, slug])
@@unique([competitionId, sortOrder])
@@index([competitionId])
@@index([roundType])
@@index([status])
}
model ProjectRoundState {
id String @id @default(cuid())
projectId String
roundId String
state ProjectRoundStateValue @default(PENDING)
enteredAt DateTime @default(now())
exitedAt DateTime?
metadataJson Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
@@unique([projectId, roundId])
@@index([projectId])
@@index([roundId])
@@index([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)
// =============================================================================
model JuryGroup {
id String @id @default(cuid())
competitionId String
name String
slug String
description String? @db.Text
sortOrder Int @default(0)
// Default assignment configuration
defaultMaxAssignments Int @default(20)
defaultCapMode CapMode @default(SOFT)
softCapBuffer Int @default(2)
// Default category quotas
categoryQuotasEnabled Boolean @default(false)
defaultCategoryQuotas Json? @db.JsonB
// Onboarding self-service
allowJurorCapAdjustment Boolean @default(false)
allowJurorRatioAdjustment Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
members JuryGroupMember[]
rounds Round[]
assignments Assignment[]
awards SpecialAward[]
@@unique([competitionId, slug])
@@index([competitionId])
}
model JuryGroupMember {
id String @id @default(cuid())
juryGroupId String
userId String
role JuryGroupMemberRole @default(MEMBER)
joinedAt DateTime @default(now())
// Per-juror overrides (null = use group defaults)
maxAssignmentsOverride Int?
capModeOverride CapMode?
categoryQuotasOverride Json? @db.JsonB
// Juror preferences (admin-set)
preferredStartupRatio Float?
availabilityNotes String? @db.Text
// Self-service overrides (set by juror during onboarding, Layer 4b)
selfServiceCap Int?
selfServiceRatio Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
assignmentIntents AssignmentIntent[]
deliberationVotes DeliberationVote[]
deliberationParticipations DeliberationParticipant[]
@@unique([juryGroupId, userId])
@@index([juryGroupId])
@@index([userId])
}
// =============================================================================
// SUBMISSION WINDOW MODELS (NEW)
// =============================================================================
model SubmissionWindow {
id String @id @default(cuid())
competitionId String
name String
slug String
roundNumber Int
sortOrder Int @default(0)
// Window timing
windowOpenAt DateTime?
windowCloseAt DateTime?
// Deadline behavior
deadlinePolicy DeadlinePolicy @default(FLAG)
graceHours Int?
// Locking behavior
lockOnClose Boolean @default(true)
isLocked Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
fileRequirements SubmissionFileRequirement[]
projectFiles ProjectFile[]
rounds Round[]
visibility RoundSubmissionVisibility[]
@@unique([competitionId, slug])
@@unique([competitionId, roundNumber])
@@index([competitionId])
}
model SubmissionFileRequirement {
id String @id @default(cuid())
submissionWindowId String
label String
slug String
description String? @db.Text
mimeTypes String[]
maxSizeMb Int?
required Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
files ProjectFile[]
@@unique([submissionWindowId, slug])
@@index([submissionWindowId])
}
model RoundSubmissionVisibility {
id String @id @default(cuid())
roundId String
submissionWindowId String
canView Boolean @default(true)
displayLabel String?
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
@@unique([roundId, submissionWindowId])
@@index([roundId])
}
// =============================================================================
// ASSIGNMENT GOVERNANCE MODELS (NEW)
// =============================================================================
model AssignmentIntent {
id String @id @default(cuid())
juryGroupMemberId String
roundId String
projectId String
source AssignmentIntentSource
status AssignmentIntentStatus @default(INTENT_PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
juryGroupMember JuryGroupMember @relation(fields: [juryGroupMemberId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([juryGroupMemberId, roundId, projectId])
@@index([roundId])
@@index([projectId])
@@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)
// =============================================================================
model MentorFile {
id String @id @default(cuid())
mentorAssignmentId String
uploadedByUserId String
fileName String
mimeType String
size Int
bucket String
objectKey String
description String? @db.Text
// Promotion to official submission
isPromoted Boolean @default(false)
promotedToFileId String? @unique
promotedAt DateTime?
promotedByUserId String?
createdAt DateTime @default(now())
// Relations
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
uploadedBy User @relation("MentorFileUploader", fields: [uploadedByUserId], references: [id])
promotedBy User? @relation("MentorFilePromoter", fields: [promotedByUserId], references: [id])
promotedFile ProjectFile? @relation("PromotedFromMentorFile", fields: [promotedToFileId], references: [id], onDelete: SetNull)
comments MentorFileComment[]
promotionEvents SubmissionPromotionEvent[]
@@index([mentorAssignmentId])
@@index([uploadedByUserId])
}
model MentorFileComment {
id String @id @default(cuid())
mentorFileId String
authorId String
content String @db.Text
// Threading support
parentCommentId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
mentorFile MentorFile @relation(fields: [mentorFileId], references: [id], onDelete: Cascade)
author User @relation("MentorFileCommentAuthor", fields: [authorId], references: [id])
parentComment MentorFileComment? @relation("CommentThread", fields: [parentCommentId], references: [id], onDelete: Cascade)
replies MentorFileComment[] @relation("CommentThread")
@@index([mentorFileId])
@@index([authorId])
@@index([parentCommentId])
}
model SubmissionPromotionEvent {
id String @id @default(cuid())
projectId String
roundId String
slotKey String
sourceType SubmissionPromotionSource
sourceFileId String?
promotedById String
createdAt DateTime @default(now())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
sourceFile MentorFile? @relation(fields: [sourceFileId], references: [id], onDelete: SetNull)
promotedBy User @relation("SubmissionPromoter", fields: [promotedById], references: [id])
@@index([projectId])
@@index([roundId])
@@index([sourceFileId])
}
// =============================================================================
// DELIBERATION MODELS (NEW)
// =============================================================================
model DeliberationSession {
id String @id @default(cuid())
competitionId String
roundId String
category CompetitionCategory
mode DeliberationMode
showCollectiveRankings Boolean @default(false)
showPriorJuryData Boolean @default(false)
status DeliberationStatus
tieBreakMethod TieBreakMethod
adminOverrideResult Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
votes DeliberationVote[]
results DeliberationResult[]
participants DeliberationParticipant[]
@@index([competitionId])
@@index([roundId])
@@index([status])
}
model DeliberationVote {
id String @id @default(cuid())
sessionId String
juryMemberId String
projectId String
rank Int?
isWinnerPick Boolean @default(false)
runoffRound Int @default(0)
createdAt DateTime @default(now())
// Relations
session DeliberationSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
juryMember JuryGroupMember @relation(fields: [juryMemberId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([sessionId, juryMemberId, projectId, runoffRound])
@@index([sessionId])
@@index([juryMemberId])
@@index([projectId])
}
model DeliberationResult {
id String @id @default(cuid())
sessionId String
projectId String
finalRank Int
voteCount Int @default(0)
isAdminOverridden Boolean @default(false)
overrideReason String? @db.Text
// Relations
session DeliberationSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([sessionId, projectId])
@@index([sessionId])
@@index([projectId])
}
model DeliberationParticipant {
id String @id @default(cuid())
sessionId String
userId String
status DeliberationParticipantStatus
replacedById String?
// Relations
session DeliberationSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
user JuryGroupMember @relation(fields: [userId], references: [id], onDelete: Cascade)
replacedBy User? @relation("DeliberationReplacement", fields: [replacedById], references: [id])
@@unique([sessionId, userId])
@@index([sessionId])
@@index([userId])
}
// =============================================================================
// RESULT LOCKING MODELS (NEW)
// =============================================================================
model ResultLock {
id String @id @default(cuid())
competitionId String
roundId String
category CompetitionCategory
lockedById String
resultSnapshot Json @db.JsonB
lockedAt DateTime @default(now())
// Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
lockedBy User @relation("ResultLockCreator", fields: [lockedById], references: [id])
unlockEvents ResultUnlockEvent[]
@@index([competitionId])
@@index([roundId])
@@index([category])
}
model ResultUnlockEvent {
id String @id @default(cuid())
resultLockId String
unlockedById String
reason String @db.Text
unlockedAt DateTime @default(now())
// Relations
resultLock ResultLock @relation(fields: [resultLockId], references: [id], onDelete: Cascade)
unlockedBy User @relation("ResultUnlocker", fields: [unlockedById], references: [id])
@@index([resultLockId])
@@index([unlockedById])
}