MOPC-App/prisma/seed.ts

939 lines
42 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`)
// ==========================================================================
// 1b. Expertise Tags
// ==========================================================================
console.log('\n🏷 Creating expertise tags...')
const expertiseTags = [
// Pollution & Waste — aligned with MOPC OceanIssue: POLLUTION_REDUCTION
{ name: 'Plastic Pollution Solutions', description: 'Technologies and methods to reduce marine plastic debris, microplastics, and single-use packaging', category: 'Pollution & Waste', color: '#dc2626', sortOrder: 0 },
{ name: 'Oil Spill Prevention & Response', description: 'Tools and systems for preventing and cleaning up oil and chemical spills at sea', category: 'Pollution & Waste', color: '#dc2626', sortOrder: 1 },
{ name: 'Wastewater & Runoff Treatment', description: 'Filtering agricultural runoff, industrial discharge, and urban wastewater before it reaches the ocean', category: 'Pollution & Waste', color: '#dc2626', sortOrder: 2 },
{ name: 'Marine Debris Cleanup', description: 'Ocean and coastal cleanup technologies, collection vessels, and waste recovery systems', category: 'Pollution & Waste', color: '#dc2626', sortOrder: 3 },
{ name: 'Circular Economy & Recycling', description: 'Upcycling ocean waste, circular packaging, and zero-waste supply chains for coastal industries', category: 'Pollution & Waste', color: '#dc2626', sortOrder: 4 },
// Climate & Carbon — aligned with MOPC OceanIssue: CLIMATE_MITIGATION, BLUE_CARBON, OCEAN_ACIDIFICATION
{ name: 'Blue Carbon Ecosystems', description: 'Conservation and restoration of mangroves, seagrass beds, and salt marshes for carbon sequestration', category: 'Climate & Carbon', color: '#0284c7', sortOrder: 10 },
{ name: 'Ocean Acidification Mitigation', description: 'Solutions addressing declining ocean pH, alkalinity enhancement, and impacts on calcifying organisms', category: 'Climate & Carbon', color: '#0284c7', sortOrder: 11 },
{ name: 'Climate Adaptation for Coasts', description: 'Nature-based solutions and infrastructure protecting coastal communities from rising seas and storms', category: 'Climate & Carbon', color: '#0284c7', sortOrder: 12 },
{ name: 'Renewable Ocean Energy', description: 'Wave, tidal, offshore wind, and ocean thermal energy conversion technologies', category: 'Climate & Carbon', color: '#0284c7', sortOrder: 13 },
{ name: 'Carbon Capture & Sequestration', description: 'Marine-based carbon dioxide removal technologies including algae, mineralization, and ocean fertilization', category: 'Climate & Carbon', color: '#0284c7', sortOrder: 14 },
// Sustainable Seafood & Aquaculture — aligned with MOPC OceanIssue: SUSTAINABLE_FISHING
{ name: 'Sustainable Aquaculture', description: 'Low-impact fish and shellfish farming, alternative feeds (e.g., seaweed, insect-based), and recirculating systems', category: 'Seafood & Aquaculture', color: '#059669', sortOrder: 20 },
{ name: 'Overfishing Prevention', description: 'Monitoring, traceability, and enforcement tools to combat illegal and unsustainable fishing', category: 'Seafood & Aquaculture', color: '#059669', sortOrder: 21 },
{ name: 'Seafood Traceability & Supply Chain', description: 'Blockchain, IoT, and certification systems ensuring sustainable and ethical seafood sourcing', category: 'Seafood & Aquaculture', color: '#059669', sortOrder: 22 },
{ name: 'Algae & Seaweed Innovation', description: 'Cultivation, processing, and applications of macroalgae and microalgae for food, feed, biomaterials, and biofuels', category: 'Seafood & Aquaculture', color: '#059669', sortOrder: 23 },
{ name: 'Small-Scale Fisheries & Hatcheries', description: 'Support for artisanal fishing communities, small-scale hatchery technology, and local fisheries management', category: 'Seafood & Aquaculture', color: '#059669', sortOrder: 24 },
// Marine Biodiversity & Habitat — aligned with MOPC OceanIssue: HABITAT_RESTORATION
{ name: 'Coral Reef Restoration', description: 'Technologies for coral propagation, transplantation, reef structure creation, and resilience monitoring', category: 'Biodiversity & Habitat', color: '#7c3aed', sortOrder: 30 },
{ name: 'Marine Protected Areas', description: 'Design, monitoring, and management of MPAs and marine spatial planning', category: 'Biodiversity & Habitat', color: '#7c3aed', sortOrder: 31 },
{ name: 'Endangered Species Conservation', description: 'Protection programs for marine mammals, sea turtles, sharks, and other threatened species', category: 'Biodiversity & Habitat', color: '#7c3aed', sortOrder: 32 },
{ name: 'Coastal & Wetland Restoration', description: 'Restoring marshes, estuaries, dunes, and other coastal habitats for biodiversity and resilience', category: 'Biodiversity & Habitat', color: '#7c3aed', sortOrder: 33 },
{ name: 'Invasive Species Management', description: 'Detection, monitoring, and control of invasive marine organisms and ballast water management', category: 'Biodiversity & Habitat', color: '#7c3aed', sortOrder: 34 },
// Ocean Technology & Innovation — aligned with MOPC OceanIssue: TECHNOLOGY_INNOVATION
{ name: 'Ocean Monitoring & Sensors', description: 'IoT sensors, buoys, and autonomous platforms for real-time ocean data collection', category: 'Ocean Technology', color: '#7c3aed', sortOrder: 40 },
{ name: 'Underwater Robotics & AUVs', description: 'Autonomous underwater vehicles, ROVs, and marine drones for exploration and monitoring', category: 'Ocean Technology', color: '#7c3aed', sortOrder: 41 },
{ name: 'AI & Data Analytics for Oceans', description: 'Machine learning and big data applications for ocean health prediction, species identification, and pattern detection', category: 'Ocean Technology', color: '#7c3aed', sortOrder: 42 },
{ name: 'Satellite & Remote Sensing', description: 'Earth observation, hyperspectral imaging, and satellite-based ocean monitoring', category: 'Ocean Technology', color: '#7c3aed', sortOrder: 43 },
{ name: 'Marine Biotechnology', description: 'Bio-inspired materials, biomimicry, marine-derived pharmaceuticals, and bioplastics from ocean organisms', category: 'Ocean Technology', color: '#7c3aed', sortOrder: 44 },
{ name: 'Desalination & Water Purification', description: 'Energy-efficient desalination, membrane technology, and portable water purification systems', category: 'Ocean Technology', color: '#7c3aed', sortOrder: 45 },
// Sustainable Shipping & Ports — aligned with MOPC OceanIssue: SUSTAINABLE_SHIPPING
{ name: 'Green Shipping & Fuels', description: 'Alternative marine fuels (hydrogen, ammonia, LNG), electric vessels, and emission reduction', category: 'Shipping & Ports', color: '#053d57', sortOrder: 50 },
{ name: 'Port Sustainability', description: 'Shore power, smart port logistics, and environmental impact reduction in harbors', category: 'Shipping & Ports', color: '#053d57', sortOrder: 51 },
{ name: 'Anti-fouling & Hull Technology', description: 'Non-toxic anti-fouling coatings, hull cleaning, and drag reduction for vessels', category: 'Shipping & Ports', color: '#053d57', sortOrder: 52 },
{ name: 'Underwater Noise Reduction', description: 'Technologies and practices to reduce vessel noise impact on marine life', category: 'Shipping & Ports', color: '#053d57', sortOrder: 53 },
// Community & Education — aligned with MOPC OceanIssue: COMMUNITY_CAPACITY, CONSUMER_AWARENESS
{ name: 'Coastal Community Development', description: 'Livelihood programs, capacity building, and economic alternatives for fishing-dependent communities', category: 'Community & Education', color: '#ea580c', sortOrder: 60 },
{ name: 'Ocean Literacy & Education', description: 'Educational programs, curricula, and outreach to increase public ocean awareness', category: 'Community & Education', color: '#ea580c', sortOrder: 61 },
{ name: 'Citizen Science & Engagement', description: 'Public participation platforms for ocean data collection, species reporting, and conservation', category: 'Community & Education', color: '#ea580c', sortOrder: 62 },
{ name: 'Ecotourism & Responsible Tourism', description: 'Sustainable marine tourism models that support conservation and local economies', category: 'Community & Education', color: '#ea580c', sortOrder: 63 },
{ name: 'Consumer Awareness & Labeling', description: 'Eco-labels, consumer apps, and awareness campaigns for sustainable ocean products', category: 'Community & Education', color: '#ea580c', sortOrder: 64 },
// Business & Investment — aligned with MOPC competition structure (Startup / Business Concept)
{ name: 'Blue Economy & Finance', description: 'Sustainable ocean economy models, blue bonds, and financial mechanisms for ocean projects', category: 'Business & Investment', color: '#557f8c', sortOrder: 70 },
{ name: 'Impact Investing & ESG', description: 'Ocean-focused impact funds, ESG frameworks, and blended finance for marine conservation', category: 'Business & Investment', color: '#557f8c', sortOrder: 71 },
{ name: 'Startup Acceleration', description: 'Scaling early-stage ocean startups, go-to-market strategy, and business model validation', category: 'Business & Investment', color: '#557f8c', sortOrder: 72 },
{ name: 'Ocean Policy & Governance', description: 'International maritime law, regulatory frameworks, and ocean governance institutions', category: 'Business & Investment', color: '#557f8c', sortOrder: 73 },
{ name: 'Mediterranean & Small Seas', description: 'Conservation and sustainable development specific to enclosed and semi-enclosed seas like the Mediterranean', category: 'Business & Investment', color: '#557f8c', sortOrder: 74 },
]
for (const tag of expertiseTags) {
await prisma.expertiseTag.upsert({
where: { name: tag.name },
update: {},
create: {
name: tag.name,
description: tag.description,
category: tag.category,
color: tag.color,
sortOrder: tag.sortOrder,
isActive: true,
},
})
}
console.log(` Created ${expertiseTags.length} expertise tags across ${new Set(expertiseTags.map(t => t.category)).size} categories`)
// ==========================================================================
// 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 user = await prisma.user.upsert({
where: { email: j.email },
update: {},
create: {
email: j.email,
name: j.name,
role: UserRole.JURY_MEMBER,
status: UserStatus.NONE,
country: j.country,
expertiseTags: j.tags,
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) {
await prisma.user.upsert({
where: { email: m.email },
update: {},
create: {
email: m.email,
name: m.name,
role: UserRole.MENTOR,
status: UserStatus.NONE,
country: m.country,
expertiseTags: m.tags,
},
})
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) {
await prisma.user.upsert({
where: { email: o.email },
update: {},
create: {
email: o.email,
name: o.name,
role: UserRole.OBSERVER,
status: UserStatus.NONE,
country: o.country,
},
})
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()
})