MOPC-App/prisma/schema.prisma

2599 lines
80 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =============================================================================
// 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
pageCount Int? // Number of pages (PDFs, presentations, etc.)
// Document analysis (optional, populated by document-analyzer service)
textPreview String? @db.Text // First ~2000 chars of extracted text
detectedLang String? // ISO 639-3 code (e.g. 'eng', 'fra', 'und')
langConfidence Float? // 0.01.0 confidence
analyzedAt DateTime? // When analysis last ran
// 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])
}