MOPC-App/prisma/schema.prisma

2101 lines
63 KiB
Plaintext
Raw Normal View History

// =============================================================================
// MOPC Platform - Prisma Schema
// =============================================================================
// This schema defines the database structure for the Monaco Ocean Protection
// Challenge jury voting platform.
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "windows", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// =============================================================================
// ENUMS
// =============================================================================
enum UserRole {
SUPER_ADMIN
PROGRAM_ADMIN
JURY_MEMBER
MENTOR
OBSERVER
APPLICANT
AWARD_MASTER
AUDIENCE
}
enum UserStatus {
NONE
INVITED
ACTIVE
SUSPENDED
}
enum ProgramStatus {
DRAFT
ACTIVE
ARCHIVED
}
enum ProjectStatus {
SUBMITTED
ELIGIBLE
ASSIGNED
SEMIFINALIST
FINALIST
REJECTED
}
enum EvaluationStatus {
NOT_STARTED
DRAFT
SUBMITTED
LOCKED
}
enum AssignmentMethod {
MANUAL
BULK
AI_SUGGESTED
AI_AUTO
ALGORITHM
}
enum FileType {
EXEC_SUMMARY
PRESENTATION
VIDEO
OTHER
BUSINESS_PLAN
VIDEO_PITCH
SUPPORTING_DOC
}
enum SubmissionSource {
MANUAL
CSV
NOTION
TYPEFORM
PUBLIC_FORM
}
enum SettingType {
STRING
NUMBER
BOOLEAN
JSON
SECRET
}
enum SettingCategory {
AI
BRANDING
EMAIL
STORAGE
SECURITY
DEFAULTS
WHATSAPP
AUDIT_CONFIG
LOCALIZATION
DIGEST
ANALYTICS
INTEGRATIONS
COMMUNICATION
}
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
}
// =============================================================================
// PIPELINE / STAGE ENGINE ENUMS
// =============================================================================
enum StageType {
INTAKE
FILTER
EVALUATION
SELECTION
LIVE_FINAL
RESULTS
}
enum TrackKind {
MAIN
AWARD
SHOWCASE
}
enum RoutingMode {
PARALLEL
EXCLUSIVE
POST_MAIN
}
enum StageStatus {
STAGE_DRAFT
STAGE_ACTIVE
STAGE_CLOSED
STAGE_ARCHIVED
}
enum ProjectStageStateValue {
PENDING
IN_PROGRESS
PASSED
REJECTED
ROUTED
COMPLETED
WITHDRAWN
}
enum DecisionMode {
JURY_VOTE
AWARD_MASTER_DECISION
ADMIN_DECISION
}
enum OverrideReasonCode {
DATA_CORRECTION
POLICY_EXCEPTION
JURY_CONFLICT
SPONSOR_DECISION
ADMIN_DISCRETION
}
// =============================================================================
// 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[]
@@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[]
pipelines Pipeline[]
@@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])
}
// Round model RETIRED in Phase 6 — all orchestration now uses Pipeline/Stage.
// roundId columns are preserved as optional raw fields for historical data.
model EvaluationForm {
id String @id @default(cuid())
roundId String? // Legacy — kept for historical data
version Int @default(1)
stageId String
// 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
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
evaluations Evaluation[]
@@unique([stageId, version])
@@index([stageId, 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[]
projectStageStates ProjectStageState[]
cohortProjects CohortProject[]
@@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? // Legacy — kept for historical data
stageId 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
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
files ProjectFile[]
@@index([stageId])
}
model ProjectFile {
id String @id @default(cuid())
projectId String
roundId String? // Which round this file was submitted for
requirementId String? // FK to FileRequirement (if uploaded against a requirement)
// File info
fileType FileType
fileName String
mimeType String
size Int // bytes
// MinIO location
bucket String
objectKey String
isLate Boolean @default(false) // Uploaded after round deadline
// Versioning
version Int @default(1)
replacedById String? // FK to the newer file that replaced this one
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")
@@unique([bucket, objectKey])
@@index([projectId])
@@index([fileType])
@@index([requirementId])
}
// =============================================================================
// ASSIGNMENTS & EVALUATIONS
// =============================================================================
model Assignment {
id String @id @default(cuid())
userId String
projectId String
roundId String? // Legacy — kept for historical data
stageId String
// Assignment info
method AssignmentMethod @default(MANUAL)
isRequired Boolean @default(true)
isCompleted Boolean @default(false)
// AI assignment metadata
aiConfidenceScore Float? // 0-1 confidence from AI
expertiseMatchScore Float? // 0-1 match score
aiReasoning String? @db.Text
createdAt DateTime @default(now())
createdBy String? // Admin who created the assignment
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
evaluation Evaluation?
conflictOfInterest ConflictOfInterest?
@@unique([userId, projectId, stageId])
@@index([stageId])
@@index([userId])
@@index([projectId])
@@index([isCompleted])
@@index([projectId, userId])
}
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? // Legacy — kept for historical data
stageId String
userId String
projectId String? // Optional: specific project or all projects in stage
extendedUntil DateTime
reason String? @db.Text
grantedById String
createdAt DateTime @default(now())
// Relations
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
grantedBy User @relation("GrantedBy", fields: [grantedById], references: [id])
@@index([stageId])
@@index([userId])
@@index([extendedUntil])
@@index([grantedById])
@@index([projectId])
@@index([stageId, 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? // Legacy — kept for historical data
stageId 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
stage Stage? @relation(fields: [stageId], 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?
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
mentor User @relation("MentorAssignments", fields: [mentorId], references: [id])
notes MentorNote[]
milestoneCompletions MentorMilestoneCompletion[]
@@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? // Legacy — kept for historical data
stageId 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
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
@@index([stageId])
@@index([priority])
}
model FilteringResult {
id String @id @default(cuid())
roundId String? // Legacy — kept for historical data
stageId 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
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
overriddenByUser User? @relation("FilteringOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull)
@@unique([stageId, projectId])
@@index([stageId])
@@index([projectId])
@@index([outcome])
}
// Tracks progress of long-running filtering jobs
model FilteringJob {
id String @id @default(cuid())
roundId String? // Legacy — kept for historical data
stageId 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
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
@@index([stageId])
@@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? // Legacy — kept for historical data
stageId 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
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
@@index([stageId])
@@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)
// Pipeline track link
trackId String? @unique
track Track? @relation(fields: [trackId], references: [id], onDelete: SetNull)
// 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[]
@@index([programId])
@@index([status])
@@index([sortOrder])
}
model AwardEligibility {
id String @id @default(cuid())
awardId String
projectId String
method EligibilityMethod @default(AUTO)
eligible Boolean @default(false)
aiReasoningJson Json? @db.JsonB
// Admin override
overriddenBy String?
overriddenAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
overriddenByUser User? @relation("AwardEligibilityOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull)
@@unique([awardId, projectId])
@@index([awardId])
@@index([projectId])
@@index([eligible])
@@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? // Legacy — kept for historical data
stageId String
userId String
type String // "3_DAYS", "24H", "1H"
sentAt DateTime @default(now())
// Relations
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([stageId, userId, type])
@@index([stageId])
}
// =============================================================================
// 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? // Legacy — kept for historical data
stageId 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)
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
generatedBy User @relation("EvaluationSummaryGeneratedBy", fields: [generatedById], references: [id])
@@unique([projectId, stageId])
@@index([stageId])
}
// =============================================================================
// 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())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
sender User @relation("MentorMessageSender", fields: [senderId], references: [id])
@@index([projectId, createdAt])
}
// 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? // Legacy — kept for historical data
stageId 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)
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
closedBy User? @relation("DiscussionClosedBy", fields: [closedById], references: [id], onDelete: SetNull)
comments DiscussionComment[]
@@unique([projectId, stageId])
@@index([stageId])
@@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?
stageId 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])
stage Stage? @relation(fields: [stageId], references: [id], onDelete: SetNull)
template MessageTemplate? @relation(fields: [templateId], references: [id], onDelete: SetNull)
recipients MessageRecipient[]
@@index([senderId])
@@index([stageId])
@@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])
}
// =============================================================================
// PIPELINE / TRACK / STAGE ENGINE
// =============================================================================
model Pipeline {
id String @id @default(cuid())
programId String
name String
slug String @unique
status String @default("DRAFT") // DRAFT, ACTIVE, ARCHIVED
settingsJson Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
tracks Track[]
routingRules RoutingRule[]
@@index([programId])
@@index([status])
}
model Track {
id String @id @default(cuid())
pipelineId String
name String
slug String
kind TrackKind @default(MAIN)
routingMode RoutingMode?
decisionMode DecisionMode?
sortOrder Int @default(0)
settingsJson Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
stages Stage[]
projectStageStates ProjectStageState[]
specialAward SpecialAward?
routingRulesAsSource RoutingRule[] @relation("RoutingSourceTrack")
routingRulesAsDestination RoutingRule[] @relation("RoutingDestinationTrack")
@@unique([pipelineId, slug])
@@unique([pipelineId, sortOrder])
@@index([pipelineId])
@@index([kind])
}
model Stage {
id String @id @default(cuid())
trackId String
stageType StageType
name String
slug String
status StageStatus @default(STAGE_DRAFT)
sortOrder Int @default(0)
configJson Json? @db.JsonB
windowOpenAt DateTime?
windowCloseAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
track Track @relation(fields: [trackId], references: [id], onDelete: Cascade)
projectStageStates ProjectStageState[]
transitionsFrom StageTransition[] @relation("TransitionFrom")
transitionsTo StageTransition[] @relation("TransitionTo")
cohorts Cohort[]
liveCursor LiveProgressCursor?
liveVotingSession LiveVotingSession?
// Reverse relations from existing models
evaluationForms EvaluationForm[]
assignments Assignment[]
fileRequirements FileRequirement[]
filteringRules FilteringRule[]
filteringResults FilteringResult[]
filteringJobs FilteringJob[]
assignmentJobs AssignmentJob[]
gracePeriods GracePeriod[]
reminderLogs ReminderLog[]
evaluationSummaries EvaluationSummary[]
evaluationDiscussions EvaluationDiscussion[]
messages Message[]
@@unique([trackId, slug])
@@unique([trackId, sortOrder])
@@index([trackId])
@@index([stageType])
@@index([status])
}
model StageTransition {
id String @id @default(cuid())
fromStageId String
toStageId String
isDefault Boolean @default(false)
guardJson Json? @db.JsonB // Conditions that must be met
createdAt DateTime @default(now())
// Relations
fromStage Stage @relation("TransitionFrom", fields: [fromStageId], references: [id], onDelete: Cascade)
toStage Stage @relation("TransitionTo", fields: [toStageId], references: [id], onDelete: Cascade)
@@unique([fromStageId, toStageId])
@@index([fromStageId])
@@index([toStageId])
}
model ProjectStageState {
id String @id @default(cuid())
projectId String
trackId String
stageId String
state ProjectStageStateValue @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)
track Track @relation(fields: [trackId], references: [id], onDelete: Cascade)
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
@@unique([projectId, trackId, stageId])
@@index([projectId])
@@index([trackId])
@@index([stageId])
@@index([state])
@@index([projectId, trackId])
}
model RoutingRule {
id String @id @default(cuid())
pipelineId String
name String
scope String @default("global") // global, track, stage
sourceTrackId String?
destinationTrackId String
destinationStageId String?
predicateJson Json @db.JsonB // { field, operator, value } or compound
priority Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
sourceTrack Track? @relation("RoutingSourceTrack", fields: [sourceTrackId], references: [id], onDelete: SetNull)
destinationTrack Track @relation("RoutingDestinationTrack", fields: [destinationTrackId], references: [id], onDelete: Cascade)
@@index([pipelineId])
@@index([priority])
@@index([isActive])
}
model Cohort {
id String @id @default(cuid())
stageId 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
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
projects CohortProject[]
@@index([stageId])
@@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())
stageId 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
stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade)
@@index([sessionId])
}
model OverrideAction {
id String @id @default(cuid())
entityType String // ProjectStageState, 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])
}