2073 lines
62 KiB
Plaintext
2073 lines
62 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
|
|
}
|
|
|
|
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 {
|
|
SHARED
|
|
EXCLUSIVE
|
|
}
|
|
|
|
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[]
|
|
|
|
@@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?
|
|
|
|
@@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 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])
|
|
}
|