import { PrismaClient, UserRole, UserStatus, ProgramStatus, SettingType, SettingCategory, CompetitionCategory, OceanIssue, ProjectStatus, SubmissionSource, // Competition architecture enums CompetitionStatus, RoundType, RoundStatus, CapMode, JuryGroupMemberRole, AdvancementRuleType, } from '@prisma/client' import bcrypt from 'bcryptjs' // Inline default configs so seed has ZERO dependency on src/ (not available in Docker prod image) function defaultRoundConfig(roundType: RoundType): Record { const defaults: Record Record> = { INTAKE: () => ({ allowDrafts: true, draftExpiryDays: 30, acceptedCategories: ['STARTUP', 'BUSINESS_CONCEPT'], maxFileSizeMB: 50, maxFilesPerSlot: 1, allowedMimeTypes: ['application/pdf'], lateSubmissionNotification: true, publicFormEnabled: false, customFields: [], }), FILTERING: () => ({ rules: [], aiScreeningEnabled: true, manualReviewEnabled: true, aiConfidenceThresholds: { high: 0.85, medium: 0.6, low: 0.4 }, autoAdvanceEligible: false, duplicateDetectionEnabled: true, batchSize: 20, }), EVALUATION: () => ({ requiredReviewsPerProject: 3, scoringMode: 'criteria', requireFeedback: true, feedbackMinLength: 0, requireAllCriteriaScored: true, coiRequired: true, peerReviewEnabled: false, anonymizationLevel: 'fully_anonymous', aiSummaryEnabled: false, generateAiShortlist: false, advancementMode: 'admin_selection', }), SUBMISSION: () => ({ eligibleStatuses: ['PASSED'], notifyEligibleTeams: true, lockPreviousWindows: true, }), MENTORING: () => ({ eligibility: 'requested_only', chatEnabled: true, fileUploadEnabled: true, fileCommentsEnabled: true, filePromotionEnabled: true, autoAssignMentors: false, }), LIVE_FINAL: () => ({ juryVotingEnabled: true, votingMode: 'simple', audienceVotingEnabled: false, audienceVoteWeight: 0, audienceVotingMode: 'per_project', audienceMaxFavorites: 3, audienceRequireIdentification: false, audienceRevealTiming: 'at_deliberation', deliberationEnabled: false, deliberationDurationMinutes: 30, showAudienceVotesToJury: false, presentationOrderMode: 'manual', presentationDurationMinutes: 15, qaDurationMinutes: 5, revealPolicy: 'ceremony', }), DELIBERATION: () => ({ juryGroupId: 'PLACEHOLDER', mode: 'SINGLE_WINNER_VOTE', showCollectiveRankings: false, showPriorJuryData: false, tieBreakMethod: 'ADMIN_DECIDES', votingDuration: 60, topN: 3, allowAdminOverride: true, }), } return defaults[roundType]() } 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 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): 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 = {} 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}`) // Legacy Pipeline/Track/Stage system removed - Competition/Round architecture now in use // ========================================================================== // 7. 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[] = 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) => !isEmptyRow(row)) console.log(` Entries to seed: ${validRecords.length}`) // Create applicant users and projects console.log('\nπŸš€ Creating applicant users and projects...') 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 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, }, }) projectCount++ if (projectCount % 50 === 0) { console.log(` ... ${projectCount} projects created`) } } console.log(` βœ“ Created ${projectCount} projects`) if (skippedNoEmail > 0) { console.log(` ⚠ Skipped ${skippedNoEmail} rows with no valid email`) } } // Legacy evaluation forms and special awards removed - Competition/Round architecture now in use // ========================================================================== // 8. Competition Architecture // ========================================================================== console.log('\nπŸ—οΈ Creating competition architecture...') const competition = await prisma.competition.upsert({ where: { slug: 'mopc-2026' }, update: {}, create: { programId: program.id, name: 'MOPC 2026', slug: 'mopc-2026', status: CompetitionStatus.ACTIVE, categoryMode: 'SHARED', startupFinalistCount: 3, conceptFinalistCount: 3, notifyOnRoundAdvance: true, notifyOnDeadlineApproach: true, deadlineReminderDays: [7, 3, 1], }, }) console.log(` βœ“ Competition: ${competition.name}`) // --- Jury Groups --- const juryGroup1 = await prisma.juryGroup.upsert({ where: { competitionId_slug: { competitionId: competition.id, slug: 'screening-jury' } }, update: {}, create: { competitionId: competition.id, name: 'Screening Jury', slug: 'screening-jury', sortOrder: 0, defaultMaxAssignments: 30, defaultCapMode: CapMode.SOFT, softCapBuffer: 5, categoryQuotasEnabled: false, }, }) const juryGroup2 = await prisma.juryGroup.upsert({ where: { competitionId_slug: { competitionId: competition.id, slug: 'expert-jury' } }, update: {}, create: { competitionId: competition.id, name: 'Expert Jury', slug: 'expert-jury', sortOrder: 1, defaultMaxAssignments: 20, defaultCapMode: CapMode.SOFT, softCapBuffer: 2, categoryQuotasEnabled: true, defaultCategoryQuotas: { STARTUP: { min: 5, max: 15 }, BUSINESS_CONCEPT: { min: 3, max: 10 }, }, }, }) const juryGroup3 = await prisma.juryGroup.upsert({ where: { competitionId_slug: { competitionId: competition.id, slug: 'finals-jury' } }, update: {}, create: { competitionId: competition.id, name: 'Finals Jury', slug: 'finals-jury', sortOrder: 2, defaultMaxAssignments: 10, defaultCapMode: CapMode.HARD, softCapBuffer: 0, categoryQuotasEnabled: false, }, }) console.log(' βœ“ Jury Groups: Screening, Expert, Finals') // --- Add jury members to groups --- // Split 8 jurors: 4 in screening, 6 in expert (some overlap), all 8 in finals const juryGroupAssignments = [ { groupId: juryGroup1.id, userIds: juryUserIds.slice(0, 4), role: JuryGroupMemberRole.MEMBER }, { groupId: juryGroup2.id, userIds: juryUserIds.slice(0, 6), role: JuryGroupMemberRole.MEMBER }, { groupId: juryGroup3.id, userIds: juryUserIds, role: JuryGroupMemberRole.MEMBER }, ] let memberCount = 0 for (const assignment of juryGroupAssignments) { for (let i = 0; i < assignment.userIds.length; i++) { const userId = assignment.userIds[i] await prisma.juryGroupMember.upsert({ where: { juryGroupId_userId: { juryGroupId: assignment.groupId, userId }, }, update: {}, create: { juryGroupId: assignment.groupId, userId, role: i === 0 ? JuryGroupMemberRole.CHAIR : assignment.role, }, }) memberCount++ } } console.log(` βœ“ ${memberCount} jury group memberships created`) // --- Demo self-service preferences --- // Enable self-service on the Expert Panel and set preferences for first 2 members await prisma.juryGroup.update({ where: { id: juryGroup2.id }, data: { allowJurorCapAdjustment: true, allowJurorRatioAdjustment: true }, }) // Juror 0 sets a lower cap and prefers startups const selfServiceMember1 = await prisma.juryGroupMember.findUnique({ where: { juryGroupId_userId: { juryGroupId: juryGroup2.id, userId: juryUserIds[0] } }, }) if (selfServiceMember1) { await prisma.juryGroupMember.update({ where: { id: selfServiceMember1.id }, data: { selfServiceCap: 12, selfServiceRatio: 0.7 }, }) } // Juror 1 sets a moderate ratio preference const selfServiceMember2 = await prisma.juryGroupMember.findUnique({ where: { juryGroupId_userId: { juryGroupId: juryGroup2.id, userId: juryUserIds[1] } }, }) if (selfServiceMember2) { await prisma.juryGroupMember.update({ where: { id: selfServiceMember2.id }, data: { selfServiceRatio: 0.4 }, }) } console.log(' βœ“ Self-service preferences: 2 jurors in Expert Panel') // --- Submission Windows --- const submissionWindow1 = await prisma.submissionWindow.upsert({ where: { competitionId_slug: { competitionId: competition.id, slug: 'r1-application-docs' } }, update: {}, create: { competitionId: competition.id, name: 'R1 Application Documents', slug: 'r1-application-docs', roundNumber: 1, sortOrder: 0, windowOpenAt: new Date('2026-01-01'), windowCloseAt: new Date('2026-01-31'), isLocked: true, }, }) const submissionWindow2 = await prisma.submissionWindow.upsert({ where: { competitionId_slug: { competitionId: competition.id, slug: 'r4-semifinal-docs' } }, update: {}, create: { competitionId: competition.id, name: 'R4 Semi-Finalist Documents', slug: 'r4-semifinal-docs', roundNumber: 4, sortOrder: 1, windowOpenAt: new Date('2026-04-01'), windowCloseAt: new Date('2026-04-30'), isLocked: false, }, }) console.log(' βœ“ Submission Windows: R1 Application, R4 Semi-finalist') // --- File Requirements --- await prisma.submissionFileRequirement.upsert({ where: { submissionWindowId_slug: { submissionWindowId: submissionWindow1.id, slug: 'executive-summary' } }, update: {}, create: { submissionWindowId: submissionWindow1.id, label: 'Executive Summary', slug: 'executive-summary', description: 'PDF document summarizing the project', mimeTypes: ['application/pdf'], maxSizeMb: 50, required: true, sortOrder: 0, }, }) await prisma.submissionFileRequirement.upsert({ where: { submissionWindowId_slug: { submissionWindowId: submissionWindow1.id, slug: 'video-pitch' } }, update: {}, create: { submissionWindowId: submissionWindow1.id, label: 'Video Pitch', slug: 'video-pitch', description: 'Short video pitching the project (max 5 minutes)', mimeTypes: ['video/mp4', 'video/quicktime'], maxSizeMb: 500, required: false, sortOrder: 1, }, }) await prisma.submissionFileRequirement.upsert({ where: { submissionWindowId_slug: { submissionWindowId: submissionWindow2.id, slug: 'updated-business-plan' } }, update: {}, create: { submissionWindowId: submissionWindow2.id, label: 'Updated Business Plan', slug: 'updated-business-plan', description: 'Updated business plan with financials', mimeTypes: ['application/pdf'], maxSizeMb: 50, required: true, sortOrder: 0, }, }) console.log(' βœ“ File Requirements: Exec Summary, Video Pitch, Business Plan') // --- Rounds (8-round Monaco flow) --- const roundDefs = [ { name: 'R1 - Application Intake', slug: 'r1-intake', roundType: RoundType.INTAKE, sortOrder: 0, status: RoundStatus.ROUND_CLOSED, juryGroupId: null, submissionWindowId: submissionWindow1.id }, { name: 'R2 - AI Screening', slug: 'r2-screening', roundType: RoundType.FILTERING, sortOrder: 1, status: RoundStatus.ROUND_ACTIVE, juryGroupId: juryGroup1.id, submissionWindowId: null }, { name: 'R3 - Expert Evaluation', slug: 'r3-evaluation', roundType: RoundType.EVALUATION, sortOrder: 2, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup2.id, submissionWindowId: null }, { name: 'R4 - Document Submission', slug: 'r4-submission', roundType: RoundType.SUBMISSION, sortOrder: 3, status: RoundStatus.ROUND_DRAFT, juryGroupId: null, submissionWindowId: submissionWindow2.id }, { name: 'R5 - Semi-Final Evaluation', slug: 'r5-semi-eval', roundType: RoundType.EVALUATION, sortOrder: 4, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup2.id, submissionWindowId: null }, { name: 'R6 - Mentoring', slug: 'r6-mentoring', roundType: RoundType.MENTORING, sortOrder: 5, status: RoundStatus.ROUND_DRAFT, juryGroupId: null, submissionWindowId: null }, { name: 'R7 - Grand Final', slug: 'r7-grand-final', roundType: RoundType.LIVE_FINAL, sortOrder: 6, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup3.id, submissionWindowId: null }, { name: 'R8 - Deliberation', slug: 'r8-deliberation', roundType: RoundType.DELIBERATION, sortOrder: 7, status: RoundStatus.ROUND_DRAFT, juryGroupId: juryGroup3.id, submissionWindowId: null }, ] const rounds = [] for (const def of roundDefs) { const config = defaultRoundConfig(def.roundType) const round = await prisma.round.upsert({ where: { competitionId_slug: { competitionId: competition.id, slug: def.slug } }, update: {}, create: { competitionId: competition.id, name: def.name, slug: def.slug, roundType: def.roundType, status: def.status, sortOrder: def.sortOrder, configJson: config as object, juryGroupId: def.juryGroupId, submissionWindowId: def.submissionWindowId, }, }) rounds.push(round) } console.log(` βœ“ ${rounds.length} rounds created (R1-R8)`) // --- Advancement Rules (auto-advance between rounds) --- for (let i = 0; i < rounds.length - 1; i++) { await prisma.advancementRule.upsert({ where: { roundId_sortOrder: { roundId: rounds[i].id, sortOrder: 0 }, }, update: {}, create: { roundId: rounds[i].id, ruleType: AdvancementRuleType.AUTO_ADVANCE, sortOrder: 0, targetRoundId: rounds[i + 1].id, configJson: {}, }, }) } console.log(` βœ“ ${rounds.length - 1} advancement rules created`) // --- Round-Submission Visibility (which rounds can see which submission windows) --- // R2 and R3 can see R1 docs, R5 can see R4 docs const visibilityLinks = [ { roundId: rounds[1].id, submissionWindowId: submissionWindow1.id }, // R2 sees R1 docs { roundId: rounds[2].id, submissionWindowId: submissionWindow1.id }, // R3 sees R1 docs { roundId: rounds[4].id, submissionWindowId: submissionWindow1.id }, // R5 sees R1 docs { roundId: rounds[4].id, submissionWindowId: submissionWindow2.id }, // R5 sees R4 docs ] for (const link of visibilityLinks) { await prisma.roundSubmissionVisibility.upsert({ where: { roundId_submissionWindowId: { roundId: link.roundId, submissionWindowId: link.submissionWindowId, }, }, update: {}, create: link, }) } console.log(` βœ“ ${visibilityLinks.length} submission visibility links created`) // --- Feature flag: enable competition model --- await prisma.systemSettings.upsert({ where: { key: 'feature.useCompetitionModel' }, update: { value: 'true' }, create: { key: 'feature.useCompetitionModel', value: 'true', type: SettingType.BOOLEAN, category: SettingCategory.FEATURE_FLAGS, description: 'Use Competition/Round model (legacy Pipeline system removed)', }, }) console.log(' βœ“ Feature flag: feature.useCompetitionModel = true') // ========================================================================== // 9. 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`) // ========================================================================== // 10. Summary // ========================================================================== console.log('\n' + '='.repeat(60)) console.log('βœ… SEEDING COMPLETE') console.log('='.repeat(60)) console.log(`\n Program: ${program.name} ${program.year}\n\n Competition: ${competition.name} (${competition.slug})\n Rounds: ${rounds.length} (R1-R8)\n Jury Groups: 3 (Screening, Expert, Finals)\n Sub. Windows: 2 (R1 Application, R4 Semi-finalist)\n\n Projects: ${projectCount} (from CSV)\n Users: ${3 + juryMembers.length + mentors.length + observers.length + projectCount} total\n - Admin/Staff: 3\n - Jury: ${juryMembers.length}\n - Mentors: ${mentors.length}\n - Observers: ${observers.length}\n - Applicants: ${projectCount}\n\n Login: matt@monaco-opc.com / 195260Mp!\n `) } main() .catch((e) => { console.error('❌ Seeding failed:', e) process.exit(1) }) .finally(async () => { await prisma.$disconnect() })