MOPC-App/prisma/seed.ts

1086 lines
48 KiB
TypeScript
Raw Normal View History

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 normalizeSpaces(s: string): string {
// Replace non-breaking spaces (U+00A0) and other whitespace variants with regular spaces
return s.replace(/\u00A0/g, ' ')
}
function mapCategory(raw: string | undefined): CompetitionCategory | null {
if (!raw) return null
const trimmed = normalizeSpaces(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 = normalizeSpaces(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 isEmptyRow(row: Record<string, string>): boolean {
const name = (row['Full name'] || '').trim()
const email = (row['E-mail'] || '').trim()
const project = (row["Project's name"] || '').trim()
return !name && !email && !project
}
// =============================================================================
// 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.SHARED,
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.SHARED,
sortOrder: 3,
settingsJson: { description: 'Public audience voting for fan favorite' },
},
})
console.log(` ✓ Main Competition (MAIN)`)
console.log(` ✓ Ocean Innovation Award (AWARD, SHARED)`)
console.log(` ✓ Ocean Impact Award (AWARD, EXCLUSIVE)`)
console.log(` ✓ People's Choice (SHOWCASE, SHARED)`)
// ==========================================================================
// 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}`)
// Skip only completely empty rows (no name, no email, no project)
const validRecords = records.filter((row: Record<string, string>) => !isEmptyRow(row))
console.log(` Entries to seed: ${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 skippedNoEmail = 0
for (let rowIdx = 0; rowIdx < validRecords.length; rowIdx++) {
const row = validRecords[rowIdx]
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'])
// Skip rows with no usable email (can't create user without one)
if (!email || !email.includes('@')) {
skippedNoEmail++
console.log(` ⚠ Row ${rowIdx + 2}: skipped (no valid email)`)
continue
}
// Create or get applicant user (upsert handles duplicate emails)
const user = await prisma.user.upsert({
where: { email },
update: {
status: UserStatus.NONE,
mustSetPassword: true,
},
create: {
email,
name: name || `Applicant ${rowIdx + 1}`,
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`)
if (skippedNoEmail > 0) {
console.log(` ⚠ Skipped ${skippedNoEmail} rows with no valid email`)
}
}
// ==========================================================================
// 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. 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()
})