MOPC-App/prisma/seed.ts

1138 lines
49 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 tagGroups = [
{
category: 'Pollution Reduction',
color: '#dc2626',
tags: [
{ name: 'Marine Plastic & Ghost Gear Cleanup', description: 'Collection and processing of plastic waste, fishing nets, and marine debris from coastal and ocean environments' },
{ name: 'Industrial & Wastewater Marine Protection', description: 'Systems reducing chemical discharge, nutrient runoff, and wastewater pollution before ocean impact' },
{ name: 'Circular Materials from Marine Waste', description: 'Transformation of algae, fishery byproducts, and recovered ocean waste into useful products' },
],
},
{
category: 'Climate Mitigation',
color: '#0284c7',
tags: [
{ name: 'Low-Carbon Blue Supply Chains', description: 'Solutions reducing emissions in seafood logistics, cooling, and marine value chains' },
{ name: 'Ocean Renewable Energy', description: 'Wave, tidal, offshore, and hybrid marine energy technologies' },
{ name: 'Marine Carbon Removal & Sequestration', description: 'Approaches that remove and store carbon through ocean-linked biological or mineral pathways' },
],
},
{
category: 'Technology & Innovation',
color: '#7c3aed',
tags: [
{ name: 'Marine Robotics & Autonomous Systems', description: 'ROVs, AUVs, and marine drones used for restoration, monitoring, and intervention' },
{ name: 'AI Ocean Intelligence', description: 'Machine learning and advanced analytics for ocean health, biodiversity, or operations optimization' },
{ name: 'Marine Ecotoxicology & Environmental Testing', description: 'Testing platforms that evaluate product or discharge impacts on marine ecosystems' },
],
},
{
category: 'Sustainable Shipping',
color: '#053d57',
tags: [
{ name: 'Cleaner Maritime Operations', description: 'Operational innovations that reduce emissions, waste, and fuel intensity in maritime transport' },
{ name: 'Port Environmental Performance', description: 'Technologies and practices that improve sustainability outcomes in ports and harbors' },
{ name: 'Marine Noise & Vessel Impact Reduction', description: 'Solutions that mitigate underwater noise and ecological disturbance from vessel activity' },
],
},
{
category: 'Blue Carbon',
color: '#0ea5a4',
tags: [
{ name: 'Seagrass & Mangrove Carbon Projects', description: 'Restoration and protection programs for key blue carbon habitats' },
{ name: 'Blue Carbon Measurement & Verification', description: 'Monitoring and MRV tools for quantifying carbon outcomes in marine ecosystems' },
{ name: 'Financing Blue Carbon Conservation', description: 'Financial models enabling scalable protection and restoration of blue carbon assets' },
],
},
{
category: 'Habitat Restoration',
color: '#16a34a',
tags: [
{ name: 'Coral Restoration & Reef Resilience', description: 'Propagation, outplanting, and resilience strategies for coral ecosystems' },
{ name: 'Coastal Habitat Regeneration', description: 'Recovery of dunes, wetlands, estuaries, and nearshore biodiversity hotspots' },
{ name: 'Biodiversity Threat Mitigation', description: 'Targeted interventions for invasive species, habitat degradation, and species decline' },
],
},
{
category: 'Community Capacity',
color: '#ea580c',
tags: [
{ name: 'Coastal Livelihood & Inclusion Models', description: 'Community-led business models that improve income while protecting marine ecosystems' },
{ name: 'Women-Led Blue Economy Initiatives', description: 'Programs that strengthen women leadership and participation in sustainable marine enterprises' },
{ name: 'Ocean Skills & Entrepreneurship Training', description: 'Capacity-building and startup enablement for students and coastal entrepreneurs' },
],
},
{
category: 'Sustainable Fishing',
color: '#059669',
tags: [
{ name: 'Regenerative Aquaculture', description: 'Aquaculture systems integrating ecological restoration, animal welfare, and reduced environmental pressure' },
{ name: 'Seaweed & Algae Value Chains', description: 'Cultivation and commercialization of algae or seaweed for food, feed, and biomaterials' },
{ name: 'Cold Chain & Post-Harvest Seafood Efficiency', description: 'Technologies reducing fish loss and waste through sustainable preservation and handling' },
],
},
{
category: 'Consumer Awareness',
color: '#f59e0b',
tags: [
{ name: 'Ocean Literacy Platforms', description: 'Digital or physical tools that increase public understanding of ocean health issues' },
{ name: 'Behavior Change for Ocean Protection', description: 'Campaigns and products that help consumers reduce harmful marine impact' },
{ name: 'Traceability & Sustainable Choice Tools', description: 'Interfaces helping buyers identify responsible seafood and ocean-positive products' },
],
},
{
category: 'Ocean Acidification',
color: '#2563eb',
tags: [
{ name: 'Acidification Monitoring & Forecasting', description: 'Sensors and models tracking pH dynamics and acidification risk in marine environments' },
{ name: 'Alkalinity & Buffering Interventions', description: 'Interventions designed to reduce acidification pressure on vulnerable marine systems' },
{ name: 'Acidification-Resilient Aquaculture', description: 'Farming approaches and species strategies resilient to changing ocean chemistry' },
],
},
] as const
const expertiseTags = tagGroups.flatMap((group, groupIndex) =>
group.tags.map((tag, tagIndex) => ({
name: tag.name,
description: tag.description,
category: group.category,
color: group.color,
sortOrder: groupIndex * 10 + tagIndex,
}))
)
for (const tag of expertiseTags) {
await prisma.expertiseTag.upsert({
where: { name: tag.name },
update: {
description: tag.description,
category: tag.category,
color: tag.color,
sortOrder: tag.sortOrder,
isActive: true,
},
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 isSuperAdmin = account.role === UserRole.SUPER_ADMIN
const user = await prisma.user.upsert({
where: { email: account.email },
update: isSuperAdmin
? {
status: UserStatus.ACTIVE,
passwordHash,
mustSetPassword: false,
passwordSetAt: new Date(),
onboardingCompletedAt: new Date(),
}
: {
status: UserStatus.NONE,
passwordHash: null,
mustSetPassword: true,
passwordSetAt: null,
onboardingCompletedAt: null,
inviteToken: null,
inviteTokenExpiresAt: null,
},
create: {
email: account.email,
name: account.name,
role: account.role,
status: isSuperAdmin ? UserStatus.ACTIVE : UserStatus.NONE,
passwordHash: isSuperAdmin ? passwordHash : null,
mustSetPassword: !isSuperAdmin,
passwordSetAt: isSuperAdmin ? new Date() : null,
onboardingCompletedAt: isSuperAdmin ? new Date() : null,
},
})
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: {
status: UserStatus.NONE,
},
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: {
status: UserStatus.NONE,
},
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: {
status: UserStatus.NONE,
},
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.upsert({
where: { slug: 'mopc-2026' },
update: {
name: 'MOPC 2026 Main Pipeline',
status: 'ACTIVE',
},
create: {
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.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'main' } },
update: { name: 'Main Competition' },
create: {
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.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'innovation-award' } },
update: { name: 'Ocean Innovation Award' },
create: {
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.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'impact-award' } },
update: { name: 'Ocean Impact Award' },
create: {
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.upsert({
where: { pipelineId_slug: { pipelineId: pipeline.id, slug: 'peoples-choice' } },
update: { name: "People's Choice" },
create: {
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.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'intake' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'screening' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'evaluation' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'selection' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'grand-final' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: mainTrack.id, slug: 'results' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: innovationTrack.id, slug: 'innovation-review' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: innovationTrack.id, slug: 'innovation-results' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: impactTrack.id, slug: 'impact-review' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: impactTrack.id, slug: 'impact-results' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: peoplesTrack.id, slug: 'public-vote' } },
update: {},
create: {
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.upsert({
where: { trackId_slug: { trackId: peoplesTrack.id, slug: 'peoples-results' } },
update: {},
create: {
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.upsert({
where: {
fromStageId_toStageId: {
fromStageId: group.stages[i].id,
toStageId: group.stages[i + 1].id,
},
},
update: {},
create: {
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📄 Checking for existing projects...')
const existingProjectCount = await prisma.project.count({ where: { programId: program.id } })
let projectCount = 0
if (existingProjectCount > 0) {
projectCount = existingProjectCount
console.log(` ⏭️ ${existingProjectCount} projects already exist, skipping CSV import`)
} else {
console.log(' 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
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: {
status: UserStatus.NONE,
mustSetPassword: true,
},
create: {
email,
name,
role: UserRole.APPLICANT,
status: UserStatus.NONE,
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.upsert({
where: { trackId: innovationTrack.id },
update: {},
create: {
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.upsert({
where: { trackId: impactTrack.id },
update: {},
create: {
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...')
const existingTechRule = await prisma.routingRule.findFirst({
where: { pipelineId: pipeline.id, name: 'Route Tech Innovation to Innovation Award' },
})
if (!existingTechRule) {
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,
},
})
}
const existingImpactRule = await prisma.routingRule.findFirst({
where: { pipelineId: pipeline.id, name: 'Route Community Impact to Impact Award' },
})
if (!existingImpactRule) {
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. Notification Email Settings
// ==========================================================================
console.log('\n🔔 Creating notification email settings...')
const notificationSettings = [
// Team / Applicant notifications
{ notificationType: 'APPLICATION_SUBMITTED', category: 'team', label: 'Application Submitted', description: 'When a team submits their application', sendEmail: true },
{ notificationType: 'TEAM_INVITE_RECEIVED', category: 'team', label: 'Team Invitation Received', description: 'When someone is invited to join a team', sendEmail: true },
{ notificationType: 'TEAM_MEMBER_JOINED', category: 'team', label: 'Team Member Joined', description: 'When a new member joins the team', sendEmail: false },
{ notificationType: 'ADVANCED_SEMIFINAL', category: 'team', label: 'Advanced to Semi-Finals', description: 'When a project advances to semi-finals', sendEmail: true },
{ notificationType: 'ADVANCED_FINAL', category: 'team', label: 'Selected as Finalist', description: 'When a project is selected as a finalist', sendEmail: true },
{ notificationType: 'MENTOR_ASSIGNED', category: 'team', label: 'Mentor Assigned', description: 'When a mentor is assigned to the team', sendEmail: true },
{ notificationType: 'NOT_SELECTED', category: 'team', label: 'Not Selected', description: 'When a project is not selected for the next round', sendEmail: true },
{ notificationType: 'FEEDBACK_AVAILABLE', category: 'team', label: 'Feedback Available', description: 'When jury feedback becomes available', sendEmail: true },
{ notificationType: 'WINNER_ANNOUNCEMENT', category: 'team', label: 'Winner Announcement', description: 'When a project wins an award', sendEmail: true },
// Jury notifications
{ notificationType: 'ASSIGNED_TO_PROJECT', category: 'jury', label: 'Assigned to Project', description: 'When a jury member is assigned to a project', sendEmail: true },
{ notificationType: 'BATCH_ASSIGNED', category: 'jury', label: 'Batch Assignment', description: 'When multiple projects are assigned at once', sendEmail: true },
{ notificationType: 'ROUND_NOW_OPEN', category: 'jury', label: 'Round Now Open', description: 'When a round opens for evaluation', sendEmail: true },
{ notificationType: 'REMINDER_24H', category: 'jury', label: 'Reminder (24h)', description: 'Reminder 24 hours before deadline', sendEmail: true },
{ notificationType: 'REMINDER_1H', category: 'jury', label: 'Reminder (1h)', description: 'Urgent reminder 1 hour before deadline', sendEmail: true },
{ notificationType: 'ROUND_CLOSED', category: 'jury', label: 'Round Closed', description: 'When a round closes', sendEmail: false },
{ notificationType: 'AWARD_VOTING_OPEN', category: 'jury', label: 'Award Voting Open', description: 'When special award voting opens', sendEmail: true },
// Mentor notifications
{ notificationType: 'MENTEE_ASSIGNED', category: 'mentor', label: 'Mentee Assigned', description: 'When assigned as mentor to a project', sendEmail: true },
{ notificationType: 'MENTEE_UPLOADED_DOCS', category: 'mentor', label: 'Mentee Documents Updated', description: 'When a mentee uploads new documents', sendEmail: false },
{ notificationType: 'MENTEE_ADVANCED', category: 'mentor', label: 'Mentee Advanced', description: 'When a mentee advances to the next round', sendEmail: true },
{ notificationType: 'MENTEE_FINALIST', category: 'mentor', label: 'Mentee is Finalist', description: 'When a mentee is selected as finalist', sendEmail: true },
{ notificationType: 'MENTEE_WON', category: 'mentor', label: 'Mentee Won', description: 'When a mentee wins an award', sendEmail: true },
// Observer notifications
{ notificationType: 'ROUND_STARTED', category: 'observer', label: 'Round Started', description: 'When a new round begins', sendEmail: false },
{ notificationType: 'ROUND_COMPLETED', category: 'observer', label: 'Round Completed', description: 'When a round is completed', sendEmail: true },
{ notificationType: 'FINALISTS_ANNOUNCED', category: 'observer', label: 'Finalists Announced', description: 'When finalists are announced', sendEmail: true },
{ notificationType: 'WINNERS_ANNOUNCED', category: 'observer', label: 'Winners Announced', description: 'When winners are announced', sendEmail: true },
// Admin notifications
{ notificationType: 'FILTERING_COMPLETE', category: 'admin', label: 'AI Filtering Complete', description: 'When AI filtering job completes', sendEmail: false },
{ notificationType: 'FILTERING_FAILED', category: 'admin', label: 'AI Filtering Failed', description: 'When AI filtering job fails', sendEmail: true },
{ notificationType: 'NEW_APPLICATION', category: 'admin', label: 'New Application', description: 'When a new application is received', sendEmail: false },
{ notificationType: 'SYSTEM_ERROR', category: 'admin', label: 'System Error', description: 'When a system error occurs', sendEmail: true },
]
for (const setting of notificationSettings) {
await prisma.notificationEmailSetting.upsert({
where: { notificationType: setting.notificationType },
update: {
category: setting.category,
label: setting.label,
description: setting.description,
},
create: setting,
})
}
console.log(` ✓ Created ${notificationSettings.length} notification email settings`)
// ==========================================================================
// 16. 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()
})