934 lines
38 KiB
TypeScript
934 lines
38 KiB
TypeScript
import {
|
||
PrismaClient,
|
||
UserRole,
|
||
UserStatus,
|
||
ProgramStatus,
|
||
SettingType,
|
||
SettingCategory,
|
||
CompetitionCategory,
|
||
OceanIssue,
|
||
StageType,
|
||
TrackKind,
|
||
RoutingMode,
|
||
DecisionMode,
|
||
StageStatus,
|
||
ProjectStageStateValue,
|
||
ProjectStatus,
|
||
SubmissionSource,
|
||
} from '@prisma/client'
|
||
import bcrypt from 'bcryptjs'
|
||
import { readFileSync } from 'fs'
|
||
import { parse } from 'csv-parse/sync'
|
||
import { join, dirname } from 'path'
|
||
import { fileURLToPath } from 'url'
|
||
|
||
const __filename = fileURLToPath(import.meta.url)
|
||
const __dirname = dirname(__filename)
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
// =============================================================================
|
||
// CSV Column Mapping Helpers
|
||
// =============================================================================
|
||
|
||
const categoryMap: Record<string, CompetitionCategory> = {
|
||
'the « Start-ups » category': CompetitionCategory.STARTUP,
|
||
'the « Business concepts » category': CompetitionCategory.BUSINESS_CONCEPT,
|
||
}
|
||
|
||
const issueMap: Record<string, OceanIssue> = {
|
||
'Reduction of pollution': OceanIssue.POLLUTION_REDUCTION,
|
||
'Mitigation of climate change': OceanIssue.CLIMATE_MITIGATION,
|
||
'Technology & innovations': OceanIssue.TECHNOLOGY_INNOVATION,
|
||
'Sustainable shipping': OceanIssue.SUSTAINABLE_SHIPPING,
|
||
'Blue Carbon': OceanIssue.BLUE_CARBON,
|
||
'Restoration of marine': OceanIssue.HABITAT_RESTORATION,
|
||
'Capacity building': OceanIssue.COMMUNITY_CAPACITY,
|
||
'Sustainable fishing': OceanIssue.SUSTAINABLE_FISHING,
|
||
'Consumer awareness': OceanIssue.CONSUMER_AWARENESS,
|
||
'Mitigation of ocean acidification': OceanIssue.OCEAN_ACIDIFICATION,
|
||
'Other': OceanIssue.OTHER,
|
||
}
|
||
|
||
function mapCategory(raw: string | undefined): CompetitionCategory | null {
|
||
if (!raw) return null
|
||
const trimmed = raw.trim()
|
||
for (const [prefix, value] of Object.entries(categoryMap)) {
|
||
if (trimmed.startsWith(prefix)) return value
|
||
}
|
||
return null
|
||
}
|
||
|
||
function mapIssue(raw: string | undefined): OceanIssue | null {
|
||
if (!raw) return null
|
||
const trimmed = raw.trim()
|
||
for (const [prefix, value] of Object.entries(issueMap)) {
|
||
if (trimmed.startsWith(prefix)) return value
|
||
}
|
||
return OceanIssue.OTHER
|
||
}
|
||
|
||
function parseFoundedDate(raw: string | undefined): Date | null {
|
||
if (!raw) return null
|
||
const trimmed = raw.trim()
|
||
if (!trimmed) return null
|
||
const d = new Date(trimmed)
|
||
return isNaN(d.getTime()) ? null : d
|
||
}
|
||
|
||
function isValidEntry(row: Record<string, string>): boolean {
|
||
const status = (row['Application status'] || '').trim().toLowerCase()
|
||
if (status === 'ignore' || status === 'doublon') return false
|
||
|
||
const name = (row['Full name'] || '').trim()
|
||
if (name.length <= 2) return false // skip test entries
|
||
|
||
const email = (row['E-mail'] || '').trim()
|
||
if (!email || !email.includes('@')) return false
|
||
|
||
return true
|
||
}
|
||
|
||
// =============================================================================
|
||
// Main Seed Function
|
||
// =============================================================================
|
||
|
||
async function main() {
|
||
console.log('🌱 Seeding database with MOPC 2026 real data...\n')
|
||
|
||
// ==========================================================================
|
||
// 1. System Settings
|
||
// ==========================================================================
|
||
console.log('📋 Creating system settings...')
|
||
|
||
const settings = [
|
||
{ key: 'ai_enabled', value: 'false', type: SettingType.BOOLEAN, category: SettingCategory.AI, description: 'Enable AI-powered jury assignment suggestions' },
|
||
{ key: 'ai_provider', value: 'openai', type: SettingType.STRING, category: SettingCategory.AI, description: 'AI provider for smart assignment (openai)' },
|
||
{ key: 'ai_model', value: 'gpt-4o', type: SettingType.STRING, category: SettingCategory.AI, description: 'OpenAI model to use for suggestions' },
|
||
{ key: 'ai_send_descriptions', value: 'false', type: SettingType.BOOLEAN, category: SettingCategory.AI, description: 'Send anonymized project descriptions to AI' },
|
||
{ key: 'platform_name', value: 'Monaco Ocean Protection Challenge', type: SettingType.STRING, category: SettingCategory.BRANDING, description: 'Platform display name' },
|
||
{ key: 'primary_color', value: '#de0f1e', type: SettingType.STRING, category: SettingCategory.BRANDING, description: 'Primary brand color (hex)' },
|
||
{ key: 'secondary_color', value: '#053d57', type: SettingType.STRING, category: SettingCategory.BRANDING, description: 'Secondary brand color (hex)' },
|
||
{ key: 'accent_color', value: '#557f8c', type: SettingType.STRING, category: SettingCategory.BRANDING, description: 'Accent color (hex)' },
|
||
{ key: 'session_duration_hours', value: '24', type: SettingType.NUMBER, category: SettingCategory.SECURITY, description: 'Session duration in hours' },
|
||
{ key: 'magic_link_expiry_minutes', value: '15', type: SettingType.NUMBER, category: SettingCategory.SECURITY, description: 'Magic link expiry time in minutes' },
|
||
{ key: 'rate_limit_requests_per_minute', value: '60', type: SettingType.NUMBER, category: SettingCategory.SECURITY, description: 'API rate limit per minute' },
|
||
{ key: 'storage_provider', value: 's3', type: SettingType.STRING, category: SettingCategory.STORAGE, description: 'Storage provider: s3 (MinIO) or local (filesystem)' },
|
||
{ key: 'local_storage_path', value: './uploads', type: SettingType.STRING, category: SettingCategory.STORAGE, description: 'Base path for local file storage' },
|
||
{ key: 'max_file_size_mb', value: '500', type: SettingType.NUMBER, category: SettingCategory.STORAGE, description: 'Maximum file upload size in MB' },
|
||
{ key: 'avatar_max_size_mb', value: '5', type: SettingType.NUMBER, category: SettingCategory.STORAGE, description: 'Maximum avatar image size in MB' },
|
||
{ key: 'allowed_file_types', value: JSON.stringify(['application/pdf', 'video/mp4', 'video/quicktime', 'image/png', 'image/jpeg']), type: SettingType.JSON, category: SettingCategory.STORAGE, description: 'Allowed MIME types for file uploads' },
|
||
{ key: 'allowed_image_types', value: JSON.stringify(['image/png', 'image/jpeg', 'image/webp']), type: SettingType.JSON, category: SettingCategory.STORAGE, description: 'Allowed MIME types for avatar/logo uploads' },
|
||
{ key: 'default_timezone', value: 'Europe/Monaco', type: SettingType.STRING, category: SettingCategory.DEFAULTS, description: 'Default timezone for date displays' },
|
||
{ key: 'default_page_size', value: '20', type: SettingType.NUMBER, category: SettingCategory.DEFAULTS, description: 'Default pagination size' },
|
||
{ key: 'autosave_interval_seconds', value: '30', type: SettingType.NUMBER, category: SettingCategory.DEFAULTS, description: 'Autosave interval for evaluation forms' },
|
||
{ key: 'whatsapp_enabled', value: 'false', type: SettingType.BOOLEAN, category: SettingCategory.WHATSAPP, description: 'Enable WhatsApp notifications' },
|
||
{ key: 'whatsapp_provider', value: 'META', type: SettingType.STRING, category: SettingCategory.WHATSAPP, description: 'WhatsApp provider (META or TWILIO)' },
|
||
{ key: 'openai_api_key', value: '', type: SettingType.SECRET, category: SettingCategory.AI, description: 'OpenAI API Key for AI-powered features', isSecret: true },
|
||
]
|
||
|
||
for (const setting of settings) {
|
||
await prisma.systemSettings.upsert({
|
||
where: { key: setting.key },
|
||
update: {},
|
||
create: setting,
|
||
})
|
||
}
|
||
console.log(` Created ${settings.length} settings`)
|
||
|
||
// ==========================================================================
|
||
// 1b. Expertise Tags
|
||
// ==========================================================================
|
||
console.log('\n🏷️ Creating expertise tags...')
|
||
|
||
const 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<string, string> = {}
|
||
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<string, string>[] = parse(cleanContent, {
|
||
columns: true,
|
||
skip_empty_lines: true,
|
||
relax_column_count: true,
|
||
trim: true,
|
||
})
|
||
|
||
console.log(` Raw CSV rows: ${records.length}`)
|
||
|
||
// Filter and deduplicate
|
||
const seenEmails = new Set<string>()
|
||
const validRecords: Record<string, string>[] = []
|
||
|
||
for (const row of records) {
|
||
if (!isValidEntry(row)) continue
|
||
|
||
const email = (row['E-mail'] || '').trim().toLowerCase()
|
||
if (seenEmails.has(email)) continue
|
||
|
||
seenEmails.add(email)
|
||
validRecords.push(row)
|
||
}
|
||
|
||
console.log(` Valid entries after filtering: ${validRecords.length}`)
|
||
|
||
// Create applicant users and projects
|
||
console.log('\n🚀 Creating applicant users and projects...')
|
||
|
||
const intakeStage = mainStages[0] // INTAKE - CLOSED
|
||
const filterStage = mainStages[1] // FILTER - ACTIVE
|
||
|
||
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()
|
||
})
|