489 lines
14 KiB
TypeScript
489 lines
14 KiB
TypeScript
|
|
import {
|
||
|
|
PrismaClient,
|
||
|
|
UserRole,
|
||
|
|
UserStatus,
|
||
|
|
ProgramStatus,
|
||
|
|
RoundStatus,
|
||
|
|
SettingType,
|
||
|
|
SettingCategory,
|
||
|
|
} from '@prisma/client'
|
||
|
|
|
||
|
|
const prisma = new PrismaClient()
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
console.log('🌱 Seeding database...')
|
||
|
|
|
||
|
|
// ==========================================================================
|
||
|
|
// Create System Settings
|
||
|
|
// ==========================================================================
|
||
|
|
console.log('📋 Creating system settings...')
|
||
|
|
|
||
|
|
const settings = [
|
||
|
|
// AI 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',
|
||
|
|
},
|
||
|
|
// Branding Settings
|
||
|
|
{
|
||
|
|
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)',
|
||
|
|
},
|
||
|
|
// Security Settings
|
||
|
|
{
|
||
|
|
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',
|
||
|
|
},
|
||
|
|
// Storage Settings
|
||
|
|
{
|
||
|
|
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',
|
||
|
|
},
|
||
|
|
// Default Settings
|
||
|
|
{
|
||
|
|
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',
|
||
|
|
},
|
||
|
|
// WhatsApp Settings (Phase 2)
|
||
|
|
{
|
||
|
|
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: 'whatsapp_meta_phone_number_id',
|
||
|
|
value: '',
|
||
|
|
type: SettingType.STRING,
|
||
|
|
category: SettingCategory.WHATSAPP,
|
||
|
|
description: 'Meta WhatsApp Phone Number ID',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'whatsapp_meta_access_token',
|
||
|
|
value: '',
|
||
|
|
type: SettingType.SECRET,
|
||
|
|
category: SettingCategory.WHATSAPP,
|
||
|
|
description: 'Meta WhatsApp Access Token',
|
||
|
|
isSecret: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'whatsapp_meta_business_account_id',
|
||
|
|
value: '',
|
||
|
|
type: SettingType.STRING,
|
||
|
|
category: SettingCategory.WHATSAPP,
|
||
|
|
description: 'Meta WhatsApp Business Account ID',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'whatsapp_twilio_account_sid',
|
||
|
|
value: '',
|
||
|
|
type: SettingType.SECRET,
|
||
|
|
category: SettingCategory.WHATSAPP,
|
||
|
|
description: 'Twilio Account SID',
|
||
|
|
isSecret: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'whatsapp_twilio_auth_token',
|
||
|
|
value: '',
|
||
|
|
type: SettingType.SECRET,
|
||
|
|
category: SettingCategory.WHATSAPP,
|
||
|
|
description: 'Twilio Auth Token',
|
||
|
|
isSecret: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'whatsapp_twilio_phone_number',
|
||
|
|
value: '',
|
||
|
|
type: SettingType.STRING,
|
||
|
|
category: SettingCategory.WHATSAPP,
|
||
|
|
description: 'Twilio WhatsApp Phone Number',
|
||
|
|
},
|
||
|
|
// OpenAI API Key (Phase 2)
|
||
|
|
{
|
||
|
|
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,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==========================================================================
|
||
|
|
// Create Super Admin
|
||
|
|
// ==========================================================================
|
||
|
|
console.log('👤 Creating super admin...')
|
||
|
|
|
||
|
|
const admin = await prisma.user.upsert({
|
||
|
|
where: { email: 'matt.ciaccio@gmail.com' },
|
||
|
|
update: {},
|
||
|
|
create: {
|
||
|
|
email: 'matt.ciaccio@gmail.com',
|
||
|
|
name: 'Matt Ciaccio',
|
||
|
|
role: UserRole.SUPER_ADMIN,
|
||
|
|
status: UserStatus.ACTIVE,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
console.log(` Created admin: ${admin.email}`)
|
||
|
|
|
||
|
|
// ==========================================================================
|
||
|
|
// Create Sample Program
|
||
|
|
// ==========================================================================
|
||
|
|
console.log('📁 Creating sample 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(` Created program: ${program.name} ${program.year}`)
|
||
|
|
|
||
|
|
// ==========================================================================
|
||
|
|
// Create Round 1
|
||
|
|
// ==========================================================================
|
||
|
|
console.log('🔄 Creating Round 1...')
|
||
|
|
|
||
|
|
const round1 = await prisma.round.upsert({
|
||
|
|
where: {
|
||
|
|
id: 'round-1-2026', // Use a deterministic ID for upsert
|
||
|
|
},
|
||
|
|
update: {},
|
||
|
|
create: {
|
||
|
|
id: 'round-1-2026',
|
||
|
|
programId: program.id,
|
||
|
|
name: 'Round 1 - Semi-Finalists Selection',
|
||
|
|
status: RoundStatus.DRAFT,
|
||
|
|
requiredReviews: 3,
|
||
|
|
votingStartAt: new Date('2026-02-18T09:00:00Z'),
|
||
|
|
votingEndAt: new Date('2026-02-23T18:00:00Z'),
|
||
|
|
settingsJson: {
|
||
|
|
allowGracePeriods: true,
|
||
|
|
showAggregatesAfterClose: true,
|
||
|
|
juryCanSeeOwnPastEvaluations: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
console.log(` Created round: ${round1.name}`)
|
||
|
|
|
||
|
|
// ==========================================================================
|
||
|
|
// Create Evaluation Form for Round 1
|
||
|
|
// ==========================================================================
|
||
|
|
console.log('📝 Creating evaluation form...')
|
||
|
|
|
||
|
|
await prisma.evaluationForm.upsert({
|
||
|
|
where: {
|
||
|
|
roundId_version: {
|
||
|
|
roundId: round1.id,
|
||
|
|
version: 1,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
update: {},
|
||
|
|
create: {
|
||
|
|
roundId: round1.id,
|
||
|
|
version: 1,
|
||
|
|
isActive: true,
|
||
|
|
criteriaJson: [
|
||
|
|
{
|
||
|
|
id: 'need_clarity',
|
||
|
|
label: 'Need Clarity',
|
||
|
|
description: 'How clearly is the problem/need articulated?',
|
||
|
|
scale: '1-5',
|
||
|
|
weight: 1,
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'solution_relevance',
|
||
|
|
label: 'Solution Relevance',
|
||
|
|
description: 'How relevant and innovative is the proposed solution?',
|
||
|
|
scale: '1-5',
|
||
|
|
weight: 1,
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'gap_analysis',
|
||
|
|
label: 'Gap Analysis',
|
||
|
|
description: 'How well does the project analyze existing gaps in the market?',
|
||
|
|
scale: '1-5',
|
||
|
|
weight: 1,
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'target_customers',
|
||
|
|
label: 'Target Customer Clarity',
|
||
|
|
description: 'How clearly are target customers/beneficiaries defined?',
|
||
|
|
scale: '1-5',
|
||
|
|
weight: 1,
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'ocean_impact',
|
||
|
|
label: 'Ocean Impact',
|
||
|
|
description: 'What is the potential positive impact on ocean conservation?',
|
||
|
|
scale: '1-5',
|
||
|
|
weight: 2, // Higher weight for ocean impact
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
scalesJson: {
|
||
|
|
'1-5': {
|
||
|
|
min: 1,
|
||
|
|
max: 5,
|
||
|
|
labels: {
|
||
|
|
1: 'Poor',
|
||
|
|
2: 'Below Average',
|
||
|
|
3: 'Average',
|
||
|
|
4: 'Good',
|
||
|
|
5: 'Excellent',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
'1-10': {
|
||
|
|
min: 1,
|
||
|
|
max: 10,
|
||
|
|
labels: {
|
||
|
|
1: 'Poor',
|
||
|
|
5: 'Average',
|
||
|
|
10: 'Excellent',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
console.log(' Created evaluation form v1')
|
||
|
|
|
||
|
|
// ==========================================================================
|
||
|
|
// Create Sample Jury Members
|
||
|
|
// ==========================================================================
|
||
|
|
console.log('👥 Creating sample jury members...')
|
||
|
|
|
||
|
|
const juryMembers = [
|
||
|
|
{
|
||
|
|
email: 'jury1@example.com',
|
||
|
|
name: 'Dr. Marine Expert',
|
||
|
|
expertiseTags: ['marine_biology', 'conservation', 'policy'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
email: 'jury2@example.com',
|
||
|
|
name: 'Tech Innovator',
|
||
|
|
expertiseTags: ['technology', 'innovation', 'startups'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
email: 'jury3@example.com',
|
||
|
|
name: 'Ocean Advocate',
|
||
|
|
expertiseTags: ['conservation', 'sustainability', 'education'],
|
||
|
|
},
|
||
|
|
]
|
||
|
|
|
||
|
|
for (const jury of juryMembers) {
|
||
|
|
await prisma.user.upsert({
|
||
|
|
where: { email: jury.email },
|
||
|
|
update: {},
|
||
|
|
create: {
|
||
|
|
email: jury.email,
|
||
|
|
name: jury.name,
|
||
|
|
role: UserRole.JURY_MEMBER,
|
||
|
|
status: UserStatus.INVITED,
|
||
|
|
expertiseTags: jury.expertiseTags,
|
||
|
|
maxAssignments: 15,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
console.log(` Created jury member: ${jury.email}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==========================================================================
|
||
|
|
// Create Sample Projects
|
||
|
|
// ==========================================================================
|
||
|
|
console.log('📦 Creating sample projects...')
|
||
|
|
|
||
|
|
const sampleProjects = [
|
||
|
|
{
|
||
|
|
title: 'OceanAI - Plastic Detection System',
|
||
|
|
teamName: 'BlueWave Tech',
|
||
|
|
description: 'AI-powered system using satellite imagery and drones to detect and map ocean plastic concentrations for targeted cleanup operations.',
|
||
|
|
tags: ['technology', 'ai', 'plastic_pollution'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Coral Restoration Network',
|
||
|
|
teamName: 'ReefGuard Foundation',
|
||
|
|
description: 'Community-driven coral nursery and transplantation program using innovative 3D-printed substrates.',
|
||
|
|
tags: ['conservation', 'coral', 'community'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'SeaTrack - Sustainable Fishing Tracker',
|
||
|
|
teamName: 'FishRight Solutions',
|
||
|
|
description: 'Blockchain-based supply chain tracking system ensuring sustainable fishing practices from ocean to table.',
|
||
|
|
tags: ['technology', 'sustainable_fishing', 'blockchain'],
|
||
|
|
},
|
||
|
|
]
|
||
|
|
|
||
|
|
for (const project of sampleProjects) {
|
||
|
|
await prisma.project.create({
|
||
|
|
data: {
|
||
|
|
roundId: round1.id,
|
||
|
|
title: project.title,
|
||
|
|
teamName: project.teamName,
|
||
|
|
description: project.description,
|
||
|
|
tags: project.tags,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
console.log(` Created project: ${project.title}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('')
|
||
|
|
console.log('✅ Seeding completed successfully!')
|
||
|
|
console.log('')
|
||
|
|
console.log('📧 Admin login: matt.ciaccio@gmail.com')
|
||
|
|
console.log(' (Use magic link authentication)')
|
||
|
|
}
|
||
|
|
|
||
|
|
main()
|
||
|
|
.catch((e) => {
|
||
|
|
console.error('❌ Seeding failed:', e)
|
||
|
|
process.exit(1)
|
||
|
|
})
|
||
|
|
.finally(async () => {
|
||
|
|
await prisma.$disconnect()
|
||
|
|
})
|