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])
|
||
}
|