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 = { 'the Β« Start-ups Β» category': CompetitionCategory.STARTUP, 'the Β« Business concepts Β» category': CompetitionCategory.BUSINESS_CONCEPT, } const issueMap: Record = { '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): 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 = [ // Marine Science { name: 'Marine Biology', description: 'Study of marine organisms and ecosystems', category: 'Marine Science', color: '#0284c7', sortOrder: 0 }, { name: 'Oceanography', description: 'Physical, chemical, and biological ocean science', category: 'Marine Science', color: '#0284c7', sortOrder: 1 }, { name: 'Coral Reef Ecology', description: 'Coral reef ecosystems, health, and restoration', category: 'Marine Science', color: '#0284c7', sortOrder: 2 }, { name: 'Marine Biodiversity', description: 'Species diversity and conservation in marine environments', category: 'Marine Science', color: '#0284c7', sortOrder: 3 }, { name: 'Ocean Acidification', description: 'Chemical changes in ocean pH and their impacts', category: 'Marine Science', color: '#0284c7', sortOrder: 4 }, { name: 'Deep Sea Research', description: 'Exploration and study of deep ocean environments', category: 'Marine Science', color: '#0284c7', sortOrder: 5 }, // Technology { name: 'Ocean Sensors & IoT', description: 'Sensor networks and IoT for ocean monitoring', category: 'Technology', color: '#7c3aed', sortOrder: 10 }, { name: 'AI & Machine Learning', description: 'AI applications for ocean data analysis and prediction', category: 'Technology', color: '#7c3aed', sortOrder: 11 }, { name: 'Robotics & AUVs', description: 'Autonomous underwater vehicles and marine robotics', category: 'Technology', color: '#7c3aed', sortOrder: 12 }, { name: 'Satellite Remote Sensing', description: 'Earth observation and satellite-based ocean monitoring', category: 'Technology', color: '#7c3aed', sortOrder: 13 }, { name: 'Marine Biotechnology', description: 'Biotechnological solutions from marine organisms', category: 'Technology', color: '#7c3aed', sortOrder: 14 }, { name: 'Desalination', description: 'Water desalination and purification technologies', category: 'Technology', color: '#7c3aed', sortOrder: 15 }, // Policy { name: 'Maritime Law', description: 'International maritime regulations and legal frameworks', category: 'Policy', color: '#053d57', sortOrder: 20 }, { name: 'Ocean Governance', description: 'International ocean policy and governance frameworks', category: 'Policy', color: '#053d57', sortOrder: 21 }, { name: 'Marine Protected Areas', description: 'MPA design, management, and policy', category: 'Policy', color: '#053d57', sortOrder: 22 }, { name: 'Climate Policy', description: 'Climate change mitigation and adaptation policy', category: 'Policy', color: '#053d57', sortOrder: 23 }, { name: 'Sustainable Development Goals', description: 'SDG 14 (Life Below Water) and related goals', category: 'Policy', color: '#053d57', sortOrder: 24 }, // Conservation { name: 'Habitat Restoration', description: 'Restoration of mangroves, seagrass, and coastal habitats', category: 'Conservation', color: '#059669', sortOrder: 30 }, { name: 'Species Protection', description: 'Endangered marine species conservation programs', category: 'Conservation', color: '#059669', sortOrder: 31 }, { name: 'Pollution Reduction', description: 'Marine pollution prevention and cleanup', category: 'Conservation', color: '#059669', sortOrder: 32 }, { name: 'Plastic Waste', description: 'Plastic pollution reduction and circular solutions', category: 'Conservation', color: '#059669', sortOrder: 33 }, { name: 'Blue Carbon', description: 'Carbon sequestration in coastal and marine ecosystems', category: 'Conservation', color: '#059669', sortOrder: 34 }, { name: 'Coastal Resilience', description: 'Building resilience in coastal communities and ecosystems', category: 'Conservation', color: '#059669', sortOrder: 35 }, // Business { name: 'Blue Economy', description: 'Sustainable economic use of ocean resources', category: 'Business', color: '#557f8c', sortOrder: 40 }, { name: 'Impact Investing', description: 'Investment strategies for ocean-positive outcomes', category: 'Business', color: '#557f8c', sortOrder: 41 }, { name: 'Startup Scaling', description: 'Scaling ocean-focused startups and ventures', category: 'Business', color: '#557f8c', sortOrder: 42 }, { name: 'Sustainable Aquaculture', description: 'Sustainable fish farming and aquaculture practices', category: 'Business', color: '#557f8c', sortOrder: 43 }, { name: 'Sustainable Shipping', description: 'Green shipping, fuel alternatives, and port operations', category: 'Business', color: '#557f8c', sortOrder: 44 }, { name: 'Circular Economy', description: 'Circular models for marine industries and products', category: 'Business', color: '#557f8c', sortOrder: 45 }, // Education { name: 'Ocean Literacy', description: 'Public education and awareness about ocean health', category: 'Education', color: '#ea580c', sortOrder: 50 }, { name: 'Community Engagement', description: 'Engaging coastal communities in conservation', category: 'Education', color: '#ea580c', sortOrder: 51 }, { name: 'Citizen Science', description: 'Public participation in ocean data collection', category: 'Education', color: '#ea580c', sortOrder: 52 }, { name: 'Capacity Building', description: 'Training and skill development for ocean stewardship', category: 'Education', color: '#ea580c', sortOrder: 53 }, // Engineering { name: 'Renewable Ocean Energy', description: 'Wave, tidal, and offshore wind energy systems', category: 'Engineering', color: '#be185d', sortOrder: 60 }, { name: 'Coastal Engineering', description: 'Infrastructure design for coastal protection', category: 'Engineering', color: '#be185d', sortOrder: 61 }, { name: 'Water Treatment', description: 'Wastewater treatment and water quality engineering', category: 'Engineering', color: '#be185d', sortOrder: 62 }, { name: 'Marine Materials', description: 'Biodegradable and sustainable materials for marine use', category: 'Engineering', color: '#be185d', sortOrder: 63 }, ] 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 = {} 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[] = 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() const validRecords: Record[] = [] 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() })