All checks were successful
Build and Push Docker Image / build (push) Successful in 11m7s
Introduces a document analyzer service that extracts page count (via pdf-parse), text preview, and detected language (via franc) from uploaded files. Analysis runs automatically on upload (configurable via SystemSettings) and can be triggered retroactively for existing files. Results are displayed as badges in the FileViewer and fed to AI screening for language-based filtering criteria. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2599 lines
80 KiB
Plaintext
2599 lines
80 KiB
Plaintext
// =============================================================================
|
||
// 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.0–1.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])
|
||
}
|