MOPC-App/prisma/schema.prisma

1385 lines
39 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
}
enum UserStatus {
INVITED
ACTIVE
SUSPENDED
}
enum ProgramStatus {
DRAFT
ACTIVE
ARCHIVED
}
enum RoundStatus {
DRAFT
ACTIVE
CLOSED
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 RoundType {
FILTERING
EVALUATION
LIVE_EVENT
}
enum SettingType {
STRING
NUMBER
BOOLEAN
JSON
SECRET
}
enum SettingCategory {
AI
BRANDING
EMAIL
STORAGE
SECURITY
DEFAULTS
WHATSAPP
}
enum NotificationChannel {
EMAIL
WHATSAPP
BOTH
NONE
}
enum ResourceType {
PDF
VIDEO
DOCUMENT
LINK
OTHER
}
enum CohortLevel {
ALL
SEMIFINALIST
FINALIST
}
enum PartnerVisibility {
ADMIN_ONLY
JURY_VISIBLE
PUBLIC
}
enum PartnerType {
SPONSOR
PARTNER
SUPPORTER
MEDIA
OTHER
}
enum FormFieldType {
TEXT
TEXTAREA
NUMBER
EMAIL
PHONE
URL
DATE
DATETIME
SELECT
MULTI_SELECT
RADIO
CHECKBOX
CHECKBOX_GROUP
FILE
FILE_MULTIPLE
SECTION
INSTRUCTIONS
}
enum SpecialFieldType {
TEAM_MEMBERS // Team member repeater
COMPETITION_CATEGORY // Business Concept vs Startup
OCEAN_ISSUE // Ocean issue dropdown
FILE_UPLOAD // File upload
GDPR_CONSENT // GDPR consent checkbox
COUNTRY_SELECT // Country dropdown
}
// =============================================================================
// 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
metadataJson Json? @db.JsonB
// Profile image
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?
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")
// In-app notifications
notifications InAppNotification[] @relation("UserNotifications")
notificationSettingsUpdated NotificationEmailSetting[] @relation("NotificationSettingUpdater")
// NextAuth relations
accounts Account[]
sessions Session[]
@@index([email])
@@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"
year Int // e.g., 2026
status ProgramStatus @default(DRAFT)
description String?
settingsJson Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
rounds Round[]
projects Project[]
learningResources LearningResource[]
partners Partner[]
applicationForms ApplicationForm[]
specialAwards SpecialAward[]
@@unique([name, year])
@@index([status])
}
model Round {
id String @id @default(cuid())
programId String
name String // e.g., "Round 1 - Semi-Finalists"
slug String? @unique // URL-friendly identifier for public submissions
status RoundStatus @default(DRAFT)
roundType RoundType @default(EVALUATION)
sortOrder Int @default(0) // Progression order within program
// Submission window (for applicant portal)
submissionDeadline DateTime? // Deadline for project submissions
submissionStartDate DateTime? // When submissions open
submissionEndDate DateTime? // When submissions close (replaces submissionDeadline if set)
lateSubmissionGrace Int? // Hours of grace period after deadline
// Phase-specific deadlines
phase1Deadline DateTime?
phase2Deadline DateTime?
// Voting window
votingStartAt DateTime?
votingEndAt DateTime?
// Configuration
requiredReviews Int @default(3) // Min evaluations per project
settingsJson Json? @db.JsonB // Grace periods, visibility rules, etc.
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
roundProjects RoundProject[]
assignments Assignment[]
evaluationForms EvaluationForm[]
gracePeriods GracePeriod[]
liveVotingSession LiveVotingSession?
filteringRules FilteringRule[]
filteringResults FilteringResult[]
filteringJobs FilteringJob[]
applicationForm ApplicationForm?
@@index([programId])
@@index([status])
@@index([roundType])
@@index([votingStartAt, votingEndAt])
@@index([submissionStartDate, submissionEndDate])
}
model EvaluationForm {
id String @id @default(cuid())
roundId String
version Int @default(1)
// Form configuration
// criteriaJson: Array of { id, label, description, scale, weight, required }
criteriaJson Json @db.JsonB
// scalesJson: { "1-5": { min, max, labels }, "1-10": { min, max, labels } }
scalesJson Json? @db.JsonB
isActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
evaluations Evaluation[]
@@unique([roundId, version])
@@index([roundId, isActive])
}
// =============================================================================
// PROJECTS
// =============================================================================
model Project {
id String @id @default(cuid())
programId String
// 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'
// 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)
roundProjects RoundProject[]
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")
@@index([programId])
@@index([tags])
@@index([submissionSource])
@@index([submittedByUserId])
@@index([competitionCategory])
@@index([oceanIssue])
@@index([country])
}
model RoundProject {
id String @id @default(cuid())
roundId String
projectId String
status ProjectStatus @default(SUBMITTED)
addedAt DateTime @default(now())
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([roundId, projectId])
@@index([roundId])
@@index([projectId])
@@index([status])
}
model ProjectFile {
id String @id @default(cuid())
projectId String
// File info
fileType FileType
fileName String
mimeType String
size Int // bytes
// MinIO location
bucket String
objectKey String
createdAt DateTime @default(now())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([bucket, objectKey])
@@index([projectId])
@@index([fileType])
}
// =============================================================================
// ASSIGNMENTS & EVALUATIONS
// =============================================================================
model Assignment {
id String @id @default(cuid())
userId String
projectId String
roundId String
// Assignment info
method AssignmentMethod @default(MANUAL)
isRequired Boolean @default(true)
isCompleted Boolean @default(false)
// AI assignment metadata
aiConfidenceScore Float? // 0-1 confidence from AI
expertiseMatchScore Float? // 0-1 match score
aiReasoning String? @db.Text
createdAt DateTime @default(now())
createdBy String? // Admin who created the assignment
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
evaluation Evaluation?
@@unique([userId, projectId, roundId])
@@index([userId])
@@index([projectId])
@@index([roundId])
@@index([isCompleted])
}
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
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])
@@index([status])
@@index([submittedAt])
@@index([formId])
}
// =============================================================================
// GRACE PERIODS
// =============================================================================
model GracePeriod {
id String @id @default(cuid())
roundId String
userId String
projectId String? // Optional: specific project or all projects in round
extendedUntil DateTime
reason String? @db.Text
grantedById String
createdAt DateTime @default(now())
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
grantedBy User @relation("GrantedBy", fields: [grantedById], references: [id])
@@index([roundId])
@@index([userId])
@@index([extendedUntil])
@@index([grantedById])
@@index([projectId])
}
// =============================================================================
// 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
// Request info
ipAddress String?
userAgent String?
timestamp DateTime @default(now())
// Relations
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([action])
@@index([entityType, entityId])
@@index([timestamp])
}
// =============================================================================
// 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])
}
// =============================================================================
// APPLICATION FORMS (Phase 2)
// =============================================================================
model ApplicationForm {
id String @id @default(cuid())
programId String? // null = global form
name String
description String? @db.Text
status String @default("DRAFT") // DRAFT, PUBLISHED, CLOSED
isPublic Boolean @default(false)
publicSlug String? @unique // /apply/ocean-challenge-2026
submissionLimit Int?
opensAt DateTime?
closesAt DateTime?
confirmationMessage String? @db.Text
// Round linking (for onboarding forms that create projects)
roundId String? @unique
// Email settings
sendConfirmationEmail Boolean @default(true)
sendTeamInviteEmails Boolean @default(true)
confirmationEmailSubject String?
confirmationEmailBody String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
program Program? @relation(fields: [programId], references: [id], onDelete: SetNull)
round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
fields ApplicationFormField[]
steps OnboardingStep[]
submissions ApplicationFormSubmission[]
@@index([programId])
@@index([status])
@@index([isPublic])
@@index([roundId])
}
model ApplicationFormField {
id String @id @default(cuid())
formId String
stepId String? // Which step this field belongs to (for onboarding)
fieldType FormFieldType
name String // Internal name (e.g., "project_title")
label String // Display label (e.g., "Project Title")
description String? @db.Text
placeholder String?
required Boolean @default(false)
minLength Int?
maxLength Int?
minValue Float? // For NUMBER type
maxValue Float? // For NUMBER type
optionsJson Json? @db.JsonB // For select/radio: [{ value, label }]
conditionJson Json? @db.JsonB // Conditional logic: { fieldId, operator, value }
// Onboarding-specific fields
projectMapping String? // Maps to Project column: "title", "description", etc.
specialType SpecialFieldType? // Special handling for complex fields
sortOrder Int @default(0)
width String @default("full") // full, half
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
step OnboardingStep? @relation(fields: [stepId], references: [id], onDelete: SetNull)
@@index([formId])
@@index([stepId])
@@index([sortOrder])
}
model OnboardingStep {
id String @id @default(cuid())
formId String
name String // Internal identifier (e.g., "category", "contact")
title String // Display title (e.g., "Category", "Contact Information")
description String? @db.Text
sortOrder Int @default(0)
isOptional Boolean @default(false)
conditionJson Json? @db.JsonB // Conditional visibility: { fieldId, operator, value }
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
fields ApplicationFormField[]
@@index([formId])
@@index([sortOrder])
}
model ApplicationFormSubmission {
id String @id @default(cuid())
formId String
email String?
name String?
dataJson Json @db.JsonB // Field values: { fieldName: value, ... }
status String @default("SUBMITTED") // SUBMITTED, REVIEWED, APPROVED, REJECTED
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
files SubmissionFile[]
@@index([formId])
@@index([status])
@@index([email])
@@index([createdAt])
}
model SubmissionFile {
id String @id @default(cuid())
submissionId String
fieldName String
fileName String
mimeType String?
size Int?
bucket String
objectKey String
createdAt DateTime @default(now())
// Relations
submission ApplicationFormSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
@@index([submissionId])
@@unique([bucket, objectKey])
}
// =============================================================================
// 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
@@index([category])
@@index([isActive])
@@index([sortOrder])
}
// =============================================================================
// LIVE VOTING (Phase 2B)
// =============================================================================
model LiveVotingSession {
id String @id @default(cuid())
roundId String @unique
status String @default("NOT_STARTED") // NOT_STARTED, IN_PROGRESS, PAUSED, COMPLETED
currentProjectIndex Int @default(0)
currentProjectId String?
votingStartedAt DateTime?
votingEndsAt DateTime?
projectOrderJson Json? @db.JsonB // Array of project IDs in presentation order
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
votes LiveVote[]
@@index([status])
}
model LiveVote {
id String @id @default(cuid())
sessionId String
projectId String
userId String
score Int // 1-10
votedAt DateTime @default(now())
// Relations
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([sessionId, projectId, userId])
@@index([sessionId])
@@index([projectId])
@@index([userId])
}
// =============================================================================
// 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
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
mentor User @relation("MentorAssignments", fields: [mentorId], references: [id])
@@index([mentorId])
@@index([method])
}
// =============================================================================
// FILTERING ROUND SYSTEM
// =============================================================================
enum FilteringOutcome {
PASSED
FILTERED_OUT
FLAGGED
}
enum FilteringRuleType {
FIELD_BASED
DOCUMENT_CHECK
AI_SCREENING
}
model FilteringRule {
id String @id @default(cuid())
roundId String
name String
ruleType FilteringRuleType
configJson Json @db.JsonB // Conditions, logic, action per rule type
priority Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
@@index([roundId])
@@index([priority])
}
model FilteringResult {
id String @id @default(cuid())
roundId String
projectId String
outcome FilteringOutcome
ruleResultsJson Json? @db.JsonB // Per-rule results
aiScreeningJson Json? @db.JsonB // AI screening details
// Admin override
overriddenBy String?
overriddenAt DateTime?
overrideReason String? @db.Text
finalOutcome FilteringOutcome?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
overriddenByUser User? @relation("FilteringOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull)
@@unique([roundId, projectId])
@@index([roundId])
@@index([projectId])
@@index([outcome])
}
// Tracks progress of long-running filtering jobs
model FilteringJob {
id String @id @default(cuid())
roundId String
status FilteringJobStatus @default(PENDING)
totalProjects Int @default(0)
totalBatches Int @default(0)
currentBatch Int @default(0)
processedCount Int @default(0)
passedCount Int @default(0)
filteredCount Int @default(0)
flaggedCount Int @default(0)
errorMessage String? @db.Text
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
@@index([roundId])
@@index([status])
}
enum FilteringJobStatus {
PENDING
RUNNING
COMPLETED
FAILED
}
// =============================================================================
// 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?
sortOrder Int @default(0)
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)
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])
}
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])
}