MOPC-App/prisma/seed.ts

869 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
PrismaClient,
UserRole,
UserStatus,
ProgramStatus,
SettingType,
SettingCategory,
CompetitionCategory,
OceanIssue,
StageType,
TrackKind,
RoutingMode,
DecisionMode,
StageStatus,
ProjectStageStateValue,
ProjectStatus,
SubmissionSource,
} from '@prisma/client'
import bcrypt from 'bcryptjs'
import { readFileSync } from 'fs'
import { parse } from 'csv-parse/sync'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const prisma = new PrismaClient()
// =============================================================================
// CSV Column Mapping Helpers
// =============================================================================
const categoryMap: Record<string, CompetitionCategory> = {
'the « Start-ups » category': CompetitionCategory.STARTUP,
'the « Business concepts » category': CompetitionCategory.BUSINESS_CONCEPT,
}
const issueMap: Record<string, OceanIssue> = {
'Reduction of pollution': OceanIssue.POLLUTION_REDUCTION,
'Mitigation of climate change': OceanIssue.CLIMATE_MITIGATION,
'Technology & innovations': OceanIssue.TECHNOLOGY_INNOVATION,
'Sustainable shipping': OceanIssue.SUSTAINABLE_SHIPPING,
'Blue Carbon': OceanIssue.BLUE_CARBON,
'Restoration of marine': OceanIssue.HABITAT_RESTORATION,
'Capacity building': OceanIssue.COMMUNITY_CAPACITY,
'Sustainable fishing': OceanIssue.SUSTAINABLE_FISHING,
'Consumer awareness': OceanIssue.CONSUMER_AWARENESS,
'Mitigation of ocean acidification': OceanIssue.OCEAN_ACIDIFICATION,
'Other': OceanIssue.OTHER,
}
function mapCategory(raw: string | undefined): CompetitionCategory | null {
if (!raw) return null
const trimmed = raw.trim()
for (const [prefix, value] of Object.entries(categoryMap)) {
if (trimmed.startsWith(prefix)) return value
}
return null
}
function mapIssue(raw: string | undefined): OceanIssue | null {
if (!raw) return null
const trimmed = raw.trim()
for (const [prefix, value] of Object.entries(issueMap)) {
if (trimmed.startsWith(prefix)) return value
}
return OceanIssue.OTHER
}
function parseFoundedDate(raw: string | undefined): Date | null {
if (!raw) return null
const trimmed = raw.trim()
if (!trimmed) return null
const d = new Date(trimmed)
return isNaN(d.getTime()) ? null : d
}
function isValidEntry(row: Record<string, string>): boolean {
const status = (row['Application status'] || '').trim().toLowerCase()
if (status === 'ignore' || status === 'doublon') return false
const name = (row['Full name'] || '').trim()
if (name.length <= 2) return false // skip test entries
const email = (row['E-mail'] || '').trim()
if (!email || !email.includes('@')) return false
return true
}
// =============================================================================
// Main Seed Function
// =============================================================================
async function main() {
console.log('🌱 Seeding database with MOPC 2026 real data...\n')
// ==========================================================================
// 1. System Settings
// ==========================================================================
console.log('📋 Creating system settings...')
const settings = [
{ key: 'ai_enabled', value: 'false', type: SettingType.BOOLEAN, category: SettingCategory.AI, description: 'Enable AI-powered jury assignment suggestions' },
{ key: 'ai_provider', value: 'openai', type: SettingType.STRING, category: SettingCategory.AI, description: 'AI provider for smart assignment (openai)' },
{ key: 'ai_model', value: 'gpt-4o', type: SettingType.STRING, category: SettingCategory.AI, description: 'OpenAI model to use for suggestions' },
{ key: 'ai_send_descriptions', value: 'false', type: SettingType.BOOLEAN, category: SettingCategory.AI, description: 'Send anonymized project descriptions to AI' },
{ key: 'platform_name', value: 'Monaco Ocean Protection Challenge', type: SettingType.STRING, category: SettingCategory.BRANDING, description: 'Platform display name' },
{ key: 'primary_color', value: '#de0f1e', type: SettingType.STRING, category: SettingCategory.BRANDING, description: 'Primary brand color (hex)' },
{ key: 'secondary_color', value: '#053d57', type: SettingType.STRING, category: SettingCategory.BRANDING, description: 'Secondary brand color (hex)' },
{ key: 'accent_color', value: '#557f8c', type: SettingType.STRING, category: SettingCategory.BRANDING, description: 'Accent color (hex)' },
{ key: 'session_duration_hours', value: '24', type: SettingType.NUMBER, category: SettingCategory.SECURITY, description: 'Session duration in hours' },
{ key: 'magic_link_expiry_minutes', value: '15', type: SettingType.NUMBER, category: SettingCategory.SECURITY, description: 'Magic link expiry time in minutes' },
{ key: 'rate_limit_requests_per_minute', value: '60', type: SettingType.NUMBER, category: SettingCategory.SECURITY, description: 'API rate limit per minute' },
{ key: 'storage_provider', value: 's3', type: SettingType.STRING, category: SettingCategory.STORAGE, description: 'Storage provider: s3 (MinIO) or local (filesystem)' },
{ key: 'local_storage_path', value: './uploads', type: SettingType.STRING, category: SettingCategory.STORAGE, description: 'Base path for local file storage' },
{ key: 'max_file_size_mb', value: '500', type: SettingType.NUMBER, category: SettingCategory.STORAGE, description: 'Maximum file upload size in MB' },
{ key: 'avatar_max_size_mb', value: '5', type: SettingType.NUMBER, category: SettingCategory.STORAGE, description: 'Maximum avatar image size in MB' },
{ key: 'allowed_file_types', value: JSON.stringify(['application/pdf', 'video/mp4', 'video/quicktime', 'image/png', 'image/jpeg']), type: SettingType.JSON, category: SettingCategory.STORAGE, description: 'Allowed MIME types for file uploads' },
{ key: 'allowed_image_types', value: JSON.stringify(['image/png', 'image/jpeg', 'image/webp']), type: SettingType.JSON, category: SettingCategory.STORAGE, description: 'Allowed MIME types for avatar/logo uploads' },
{ key: 'default_timezone', value: 'Europe/Monaco', type: SettingType.STRING, category: SettingCategory.DEFAULTS, description: 'Default timezone for date displays' },
{ key: 'default_page_size', value: '20', type: SettingType.NUMBER, category: SettingCategory.DEFAULTS, description: 'Default pagination size' },
{ key: 'autosave_interval_seconds', value: '30', type: SettingType.NUMBER, category: SettingCategory.DEFAULTS, description: 'Autosave interval for evaluation forms' },
{ key: 'whatsapp_enabled', value: 'false', type: SettingType.BOOLEAN, category: SettingCategory.WHATSAPP, description: 'Enable WhatsApp notifications' },
{ key: 'whatsapp_provider', value: 'META', type: SettingType.STRING, category: SettingCategory.WHATSAPP, description: 'WhatsApp provider (META or TWILIO)' },
{ key: 'openai_api_key', value: '', type: SettingType.SECRET, category: SettingCategory.AI, description: 'OpenAI API Key for AI-powered features', isSecret: true },
]
for (const setting of settings) {
await prisma.systemSettings.upsert({
where: { key: setting.key },
update: {},
create: setting,
})
}
console.log(` Created ${settings.length} settings`)
// ==========================================================================
// 2. Admin/Staff Users
// ==========================================================================
console.log('\n👤 Creating admin & staff users...')
const staffAccounts = [
{ email: 'matt@monaco-opc.com', name: 'Matt', role: UserRole.SUPER_ADMIN, password: '195260Mp!' },
{ email: 'admin@monaco-opc.com', name: 'Admin', role: UserRole.PROGRAM_ADMIN, password: 'Admin123!' },
{ email: 'awards@monaco-opc.com', name: 'Award Director', role: UserRole.AWARD_MASTER, password: 'Awards123!' },
]
const staffUsers: Record<string, string> = {}
for (const account of staffAccounts) {
const passwordHash = await bcrypt.hash(account.password, 12)
const user = await prisma.user.upsert({
where: { email: account.email },
update: { passwordHash },
create: {
email: account.email,
name: account.name,
role: account.role,
status: UserStatus.ACTIVE,
passwordHash,
mustSetPassword: false,
passwordSetAt: new Date(),
onboardingCompletedAt: new Date(),
},
})
staffUsers[account.email] = user.id
console.log(`${account.role}: ${account.email}`)
}
// ==========================================================================
// 3. Jury Members (8 fictional)
// ==========================================================================
console.log('\n⚖ Creating jury members...')
const juryMembers = [
{ email: 'jury1@monaco-opc.com', name: 'Dr. Sophie Laurent', country: 'France', tags: ['marine-biology', 'coral-restoration', 'biodiversity'] },
{ email: 'jury2@monaco-opc.com', name: 'Prof. Marco Bianchi', country: 'Italy', tags: ['ocean-engineering', 'renewable-energy', 'desalination'] },
{ email: 'jury3@monaco-opc.com', name: 'Dr. Aisha Patel', country: 'United Kingdom', tags: ['sustainability', 'circular-economy', 'waste-management'] },
{ email: 'jury4@monaco-opc.com', name: 'Dr. Kenji Tanaka', country: 'Japan', tags: ['aquaculture', 'sustainable-fishing', 'marine-technology'] },
{ email: 'jury5@monaco-opc.com', name: 'Prof. Elena Volkov', country: 'Germany', tags: ['climate-science', 'ocean-acidification', 'blue-carbon'] },
{ email: 'jury6@monaco-opc.com', name: 'Dr. Amara Diallo', country: 'Senegal', tags: ['community-development', 'capacity-building', 'coastal-management'] },
{ email: 'jury7@monaco-opc.com', name: 'Dr. Carlos Rivera', country: 'Spain', tags: ['blue-economy', 'maritime-policy', 'shipping'] },
{ email: 'jury8@monaco-opc.com', name: 'Prof. Lin Wei', country: 'Singapore', tags: ['marine-biotech', 'pollution-monitoring', 'AI-ocean'] },
]
const juryUserIds: string[] = []
for (const j of juryMembers) {
const passwordHash = await bcrypt.hash('Jury2026!', 12)
const user = await prisma.user.upsert({
where: { email: j.email },
update: {},
create: {
email: j.email,
name: j.name,
role: UserRole.JURY_MEMBER,
status: UserStatus.ACTIVE,
country: j.country,
expertiseTags: j.tags,
passwordHash,
mustSetPassword: true,
bio: `Expert in ${j.tags.join(', ')}`,
},
})
juryUserIds.push(user.id)
console.log(` ✓ Jury: ${j.name} (${j.country})`)
}
// ==========================================================================
// 4. Mentors (3 fictional)
// ==========================================================================
console.log('\n🧑🏫 Creating mentors...')
const mentors = [
{ email: 'mentor1@monaco-opc.com', name: 'Marie Dubois', country: 'Monaco', tags: ['startup-coaching', 'ocean-conservation'] },
{ email: 'mentor2@monaco-opc.com', name: 'James Cooper', country: 'United States', tags: ['venture-capital', 'cleantech'] },
{ email: 'mentor3@monaco-opc.com', name: 'Fatima Al-Rashid', country: 'UAE', tags: ['impact-investing', 'sustainability-strategy'] },
]
for (const m of mentors) {
const passwordHash = await bcrypt.hash('Mentor2026!', 12)
await prisma.user.upsert({
where: { email: m.email },
update: {},
create: {
email: m.email,
name: m.name,
role: UserRole.MENTOR,
status: UserStatus.ACTIVE,
country: m.country,
expertiseTags: m.tags,
passwordHash,
mustSetPassword: true,
},
})
console.log(` ✓ Mentor: ${m.name}`)
}
// ==========================================================================
// 5. Observers (2 fictional)
// ==========================================================================
console.log('\n👁 Creating observers...')
const observers = [
{ email: 'observer1@monaco-opc.com', name: 'Pierre Martin', country: 'Monaco' },
{ email: 'observer2@monaco-opc.com', name: 'Sarah Chen', country: 'Canada' },
]
for (const o of observers) {
const passwordHash = await bcrypt.hash('Observer2026!', 12)
await prisma.user.upsert({
where: { email: o.email },
update: {},
create: {
email: o.email,
name: o.name,
role: UserRole.OBSERVER,
status: UserStatus.ACTIVE,
country: o.country,
passwordHash,
mustSetPassword: true,
},
})
console.log(` ✓ Observer: ${o.name}`)
}
// ==========================================================================
// 6. Program
// ==========================================================================
console.log('\n📁 Creating program...')
const program = await prisma.program.upsert({
where: { name_year: { name: 'Monaco Ocean Protection Challenge', year: 2026 } },
update: {},
create: {
name: 'Monaco Ocean Protection Challenge',
year: 2026,
status: ProgramStatus.ACTIVE,
description: 'Annual ocean conservation startup competition supporting innovative solutions for ocean protection.',
},
})
console.log(` ✓ Program: ${program.name} ${program.year}`)
// ==========================================================================
// 7. Pipeline
// ==========================================================================
console.log('\n🔗 Creating pipeline...')
const pipeline = await prisma.pipeline.create({
data: {
programId: program.id,
name: 'MOPC 2026 Main Pipeline',
slug: 'mopc-2026',
status: 'ACTIVE',
settingsJson: {
description: 'Main pipeline for MOPC 2026 competition',
allowParallelTracks: true,
autoAdvanceOnClose: false,
},
},
})
console.log(` ✓ Pipeline: ${pipeline.name}`)
// ==========================================================================
// 8. Tracks (4)
// ==========================================================================
console.log('\n🛤 Creating tracks...')
const mainTrack = await prisma.track.create({
data: {
pipelineId: pipeline.id,
name: 'Main Competition',
slug: 'main',
kind: TrackKind.MAIN,
sortOrder: 0,
settingsJson: { description: 'Primary competition track for all applicants' },
},
})
const innovationTrack = await prisma.track.create({
data: {
pipelineId: pipeline.id,
name: 'Ocean Innovation Award',
slug: 'innovation-award',
kind: TrackKind.AWARD,
routingMode: RoutingMode.PARALLEL,
decisionMode: DecisionMode.JURY_VOTE,
sortOrder: 1,
settingsJson: { description: 'Award for most innovative ocean technology' },
},
})
const impactTrack = await prisma.track.create({
data: {
pipelineId: pipeline.id,
name: 'Ocean Impact Award',
slug: 'impact-award',
kind: TrackKind.AWARD,
routingMode: RoutingMode.EXCLUSIVE,
decisionMode: DecisionMode.AWARD_MASTER_DECISION,
sortOrder: 2,
settingsJson: { description: 'Award for highest community impact on ocean health' },
},
})
const peoplesTrack = await prisma.track.create({
data: {
pipelineId: pipeline.id,
name: "People's Choice",
slug: 'peoples-choice',
kind: TrackKind.SHOWCASE,
routingMode: RoutingMode.POST_MAIN,
sortOrder: 3,
settingsJson: { description: 'Public audience voting for fan favorite' },
},
})
console.log(` ✓ Main Competition (MAIN)`)
console.log(` ✓ Ocean Innovation Award (AWARD, PARALLEL)`)
console.log(` ✓ Ocean Impact Award (AWARD, EXCLUSIVE)`)
console.log(` ✓ People's Choice (SHOWCASE, POST_MAIN)`)
// ==========================================================================
// 9. Stages
// ==========================================================================
console.log('\n📊 Creating stages...')
// --- Main track stages ---
const mainStages = await Promise.all([
prisma.stage.create({
data: {
trackId: mainTrack.id,
stageType: StageType.INTAKE,
name: 'Application Intake',
slug: 'intake',
status: StageStatus.STAGE_CLOSED,
sortOrder: 0,
configJson: {
fileRequirements: [
{ name: 'Executive Summary', type: 'PDF', maxSizeMB: 50, required: true },
{ name: 'Video Pitch', type: 'VIDEO', maxSizeMB: 500, required: false },
],
deadline: '2026-01-31T23:59:00Z',
maxSubmissions: 1,
},
},
}),
prisma.stage.create({
data: {
trackId: mainTrack.id,
stageType: StageType.FILTER,
name: 'AI Screening',
slug: 'screening',
status: StageStatus.STAGE_ACTIVE,
sortOrder: 1,
configJson: {
deterministic: {
rules: [
{ field: 'competitionCategory', operator: 'is_not_null', label: 'Has category' },
{ field: 'description', operator: 'min_length', value: 50, label: 'Description >= 50 chars' },
],
},
ai: { rubricVersion: '2026-v1', model: 'gpt-4o' },
confidenceBands: {
high: { threshold: 0.8, action: 'auto_pass' },
medium: { threshold: 0.5, action: 'manual_review' },
low: { threshold: 0, action: 'auto_reject' },
},
},
},
}),
prisma.stage.create({
data: {
trackId: mainTrack.id,
stageType: StageType.EVALUATION,
name: 'Expert Evaluation',
slug: 'evaluation',
status: StageStatus.STAGE_DRAFT,
sortOrder: 2,
configJson: {
criteriaVersion: '2026-v1',
assignmentStrategy: 'smart',
requiredReviews: 3,
minAssignmentsPerJuror: 5,
maxAssignmentsPerJuror: 20,
},
},
}),
prisma.stage.create({
data: {
trackId: mainTrack.id,
stageType: StageType.SELECTION,
name: 'Semi-Final Selection',
slug: 'selection',
status: StageStatus.STAGE_DRAFT,
sortOrder: 3,
configJson: {
rankingSource: 'evaluation_scores',
finalistTarget: 6,
selectionMethod: 'top_n_with_admin_override',
},
},
}),
prisma.stage.create({
data: {
trackId: mainTrack.id,
stageType: StageType.LIVE_FINAL,
name: 'Grand Final',
slug: 'grand-final',
status: StageStatus.STAGE_DRAFT,
sortOrder: 4,
configJson: {
sessionMode: 'cohort',
votingEnabled: true,
audienceVoting: true,
audienceVoteWeight: 0.2,
presentationDurationMinutes: 10,
qaDurationMinutes: 5,
},
},
}),
prisma.stage.create({
data: {
trackId: mainTrack.id,
stageType: StageType.RESULTS,
name: 'Results & Awards',
slug: 'results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 5,
configJson: {
rankingWeights: { juryScore: 0.8, audienceScore: 0.2 },
publicationPolicy: 'after_ceremony',
announcementDate: '2026-06-15',
},
},
}),
])
// --- Innovation Award track stages ---
const innovationStages = await Promise.all([
prisma.stage.create({
data: {
trackId: innovationTrack.id,
stageType: StageType.EVALUATION,
name: 'Innovation Jury Review',
slug: 'innovation-review',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
criteriaVersion: 'innovation-2026-v1',
assignmentStrategy: 'manual',
requiredReviews: 2,
},
},
}),
prisma.stage.create({
data: {
trackId: innovationTrack.id,
stageType: StageType.RESULTS,
name: 'Innovation Results',
slug: 'innovation-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
// --- Impact Award track stages ---
const impactStages = await Promise.all([
prisma.stage.create({
data: {
trackId: impactTrack.id,
stageType: StageType.EVALUATION,
name: 'Impact Assessment',
slug: 'impact-review',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
criteriaVersion: 'impact-2026-v1',
assignmentStrategy: 'award_master',
requiredReviews: 1,
},
},
}),
prisma.stage.create({
data: {
trackId: impactTrack.id,
stageType: StageType.RESULTS,
name: 'Impact Results',
slug: 'impact-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
// --- People's Choice track stages ---
const peoplesStages = await Promise.all([
prisma.stage.create({
data: {
trackId: peoplesTrack.id,
stageType: StageType.LIVE_FINAL,
name: 'Public Voting',
slug: 'public-vote',
status: StageStatus.STAGE_DRAFT,
sortOrder: 0,
configJson: {
votingMode: 'favorites',
maxFavorites: 3,
requireIdentification: false,
votingDurationMinutes: 30,
},
},
}),
prisma.stage.create({
data: {
trackId: peoplesTrack.id,
stageType: StageType.RESULTS,
name: "People's Choice Results",
slug: 'peoples-results',
status: StageStatus.STAGE_DRAFT,
sortOrder: 1,
configJson: { publicationPolicy: 'after_ceremony' },
},
}),
])
const allStages = [...mainStages, ...innovationStages, ...impactStages, ...peoplesStages]
console.log(` ✓ Created ${allStages.length} stages across 4 tracks`)
// ==========================================================================
// 10. Stage Transitions (linear within each track)
// ==========================================================================
console.log('\n🔀 Creating stage transitions...')
const trackStageGroups = [
{ name: 'Main', stages: mainStages },
{ name: 'Innovation', stages: innovationStages },
{ name: 'Impact', stages: impactStages },
{ name: "People's", stages: peoplesStages },
]
let transitionCount = 0
for (const group of trackStageGroups) {
for (let i = 0; i < group.stages.length - 1; i++) {
await prisma.stageTransition.create({
data: {
fromStageId: group.stages[i].id,
toStageId: group.stages[i + 1].id,
isDefault: true,
},
})
transitionCount++
}
}
console.log(` ✓ Created ${transitionCount} transitions`)
// ==========================================================================
// 11. Parse CSV & Create Applicants + Projects
// ==========================================================================
console.log('\n📄 Parsing Candidatures2026.csv...')
const csvPath = join(__dirname, '..', 'docs', 'Candidatures2026.csv')
const csvContent = readFileSync(csvPath, 'utf-8')
// Remove BOM if present
const cleanContent = csvContent.replace(/^\uFEFF/, '')
const records: Record<string, string>[] = parse(cleanContent, {
columns: true,
skip_empty_lines: true,
relax_column_count: true,
trim: true,
})
console.log(` Raw CSV rows: ${records.length}`)
// Filter and deduplicate
const seenEmails = new Set<string>()
const validRecords: Record<string, string>[] = []
for (const row of records) {
if (!isValidEntry(row)) continue
const email = (row['E-mail'] || '').trim().toLowerCase()
if (seenEmails.has(email)) continue
seenEmails.add(email)
validRecords.push(row)
}
console.log(` Valid entries after filtering: ${validRecords.length}`)
// Create applicant users and projects
console.log('\n🚀 Creating applicant users and projects...')
const intakeStage = mainStages[0] // INTAKE - CLOSED
const filterStage = mainStages[1] // FILTER - ACTIVE
let projectCount = 0
for (const row of validRecords) {
const email = (row['E-mail'] || '').trim().toLowerCase()
const name = (row['Full name'] || '').trim()
const phone = (row['Téléphone'] || '').trim() || null
const country = (row['Country'] || '').trim() || null
const zone = (row['Tri par zone'] || '').trim() || null
const university = (row['University'] || '').trim() || null
const projectName = (row["Project's name"] || '').trim()
const teamMembers = (row['Team members'] || '').trim() || null
const category = mapCategory(row['Category'])
const issue = mapIssue(row['Issue'])
const comment = (row['Comment'] || row['Comment '] || '').trim() || null
const mentorship = (row['Mentorship'] || '').trim().toLowerCase() === 'true'
const referral = (row['How did you hear about MOPC?'] || '').trim() || null
const appStatus = (row['Application status'] || '').trim() || null
const phase1Url = (row['PHASE 1 - Submission'] || '').trim() || null
const phase2Url = (row['PHASE 2 - Submission'] || '').trim() || null
const foundedAt = parseFoundedDate(row['Date of creation'])
// Create or get applicant user
const user = await prisma.user.upsert({
where: { email },
update: {},
create: {
email,
name,
role: UserRole.APPLICANT,
status: UserStatus.ACTIVE,
phoneNumber: phone,
country,
metadataJson: university ? { institution: university } : undefined,
mustSetPassword: true,
},
})
// Create project
const project = await prisma.project.create({
data: {
programId: program.id,
title: projectName || `Project by ${name}`,
description: comment,
competitionCategory: category,
oceanIssue: issue,
country,
geographicZone: zone,
institution: university,
wantsMentorship: mentorship,
foundedAt,
phase1SubmissionUrl: phase1Url,
phase2SubmissionUrl: phase2Url,
referralSource: referral,
applicationStatus: appStatus,
submissionSource: SubmissionSource.CSV,
submittedByUserId: user.id,
submittedByEmail: email,
submittedAt: new Date(),
status: ProjectStatus.SUBMITTED,
metadataJson: teamMembers ? { teamMembers } : undefined,
},
})
// Create ProjectStageState: INTAKE stage = PASSED (intake closed)
await prisma.projectStageState.create({
data: {
projectId: project.id,
trackId: mainTrack.id,
stageId: intakeStage.id,
state: ProjectStageStateValue.PASSED,
enteredAt: new Date('2026-01-15'),
exitedAt: new Date('2026-01-31'),
},
})
// Create ProjectStageState: FILTER stage = PENDING (current active stage)
await prisma.projectStageState.create({
data: {
projectId: project.id,
trackId: mainTrack.id,
stageId: filterStage.id,
state: ProjectStageStateValue.PENDING,
enteredAt: new Date('2026-02-01'),
},
})
projectCount++
if (projectCount % 50 === 0) {
console.log(` ... ${projectCount} projects created`)
}
}
console.log(` ✓ Created ${projectCount} projects with stage states`)
// ==========================================================================
// 12. Evaluation Form (for Expert Evaluation stage)
// ==========================================================================
console.log('\n📝 Creating evaluation form...')
const evaluationStage = mainStages[2] // EVALUATION stage
await prisma.evaluationForm.upsert({
where: { stageId_version: { stageId: evaluationStage.id, version: 1 } },
update: {},
create: {
stageId: evaluationStage.id,
version: 1,
isActive: true,
criteriaJson: [
{ id: 'need_clarity', label: 'Need Clarity', description: 'How clearly is the problem/need articulated?', scale: '1-5', weight: 20, type: 'numeric', required: true },
{ id: 'solution_relevance', label: 'Solution Relevance', description: 'How relevant and innovative is the proposed solution?', scale: '1-5', weight: 25, type: 'numeric', required: true },
{ id: 'ocean_impact', label: 'Ocean Impact', description: 'What is the potential positive impact on ocean conservation?', scale: '1-5', weight: 25, type: 'numeric', required: true },
{ id: 'feasibility', label: 'Feasibility & Scalability', description: 'How feasible and scalable is the project?', scale: '1-5', weight: 20, type: 'numeric', required: true },
{ id: 'team_strength', label: 'Team Strength', description: 'How strong and capable is the team?', scale: '1-5', weight: 10, type: 'numeric', required: true },
],
scalesJson: {
'1-5': { min: 1, max: 5, labels: { 1: 'Poor', 2: 'Below Average', 3: 'Average', 4: 'Good', 5: 'Excellent' } },
},
},
})
console.log(' ✓ Evaluation form created (5 criteria)')
// ==========================================================================
// 13. Special Awards
// ==========================================================================
console.log('\n🏆 Creating special awards...')
await prisma.specialAward.create({
data: {
programId: program.id,
name: 'Ocean Innovation Award',
description: 'Recognizes the most innovative technology solution for ocean protection',
status: 'DRAFT',
trackId: innovationTrack.id,
scoringMode: 'PICK_WINNER',
useAiEligibility: true,
criteriaText: 'Projects demonstrating breakthrough technological innovation for ocean conservation',
},
})
await prisma.specialAward.create({
data: {
programId: program.id,
name: 'Ocean Impact Award',
description: 'Recognizes the project with highest community and environmental impact',
status: 'DRAFT',
trackId: impactTrack.id,
scoringMode: 'PICK_WINNER',
useAiEligibility: false,
criteriaText: 'Projects with measurable, significant impact on ocean health and coastal communities',
},
})
console.log(' ✓ Ocean Innovation Award → innovation-award track')
console.log(' ✓ Ocean Impact Award → impact-award track')
// ==========================================================================
// 14. Routing Rules
// ==========================================================================
console.log('\n🔀 Creating routing rules...')
await prisma.routingRule.create({
data: {
pipelineId: pipeline.id,
name: 'Route Tech Innovation to Innovation Award',
scope: 'global',
destinationTrackId: innovationTrack.id,
predicateJson: {
field: 'oceanIssue',
operator: 'eq',
value: 'TECHNOLOGY_INNOVATION',
},
priority: 10,
isActive: true,
},
})
await prisma.routingRule.create({
data: {
pipelineId: pipeline.id,
name: 'Route Community Impact to Impact Award',
scope: 'global',
destinationTrackId: impactTrack.id,
predicateJson: {
or: [
{ field: 'oceanIssue', operator: 'eq', value: 'COMMUNITY_CAPACITY' },
{ field: 'oceanIssue', operator: 'eq', value: 'HABITAT_RESTORATION' },
],
},
priority: 5,
isActive: true,
},
})
console.log(' ✓ Tech Innovation → Innovation Award (PARALLEL)')
console.log(' ✓ Community Impact → Impact Award (EXCLUSIVE)')
// ==========================================================================
// 15. Summary
// ==========================================================================
console.log('\n' + '='.repeat(60))
console.log('✅ SEEDING COMPLETE')
console.log('='.repeat(60))
console.log(`
Program: ${program.name} ${program.year}
Pipeline: ${pipeline.name} (${pipeline.slug})
Tracks: 4 (Main, Innovation Award, Impact Award, People's Choice)
Stages: ${allStages.length} total
Transitions: ${transitionCount}
Projects: ${projectCount} (from CSV)
Users: ${3 + juryMembers.length + mentors.length + observers.length + projectCount} total
- Admin/Staff: 3
- Jury: ${juryMembers.length}
- Mentors: ${mentors.length}
- Observers: ${observers.length}
- Applicants: ${projectCount}
Login: matt@monaco-opc.com / 195260Mp!
`)
}
main()
.catch((e) => {
console.error('❌ Seeding failed:', e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})