MOPC-App/prisma/seed.ts

971 lines
46 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
PrismaClient,
UserRole,
UserStatus,
ProgramStatus,
SettingType,
SettingCategory,
CompetitionCategory,
OceanIssue,
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<string, unknown> {
const defaults: Record<RoundType, () => Record<string, unknown>> = {
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<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}`)
// 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<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...')
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()
})