import { PrismaClient } from "@prisma/client"; import * as bcrypt from "bcryptjs"; const prisma = new PrismaClient(); // ─── Helpers ───────────────────────────────────────────────────────────────── function cuid(seed: string): string { // Deterministic CUID-like string for idempotent seeding return seed.padStart(25, "c"); } function mockCmsId(prefix: string, index: number): string { return `${prefix}_${String(index).padStart(4, "0")}`; } async function ensureTenant( slug: string, data: Parameters[0]["data"], ) { const existing = await prisma.tenant.findUnique({ where: { slug } }); if (existing) { console.log(` ℹ Tenant "${slug}" already exists, skipping`); return existing; } return prisma.tenant.create({ data }); } async function ensureUser( email: string, data: Parameters[0]["data"], ) { const existing = await prisma.user.findUnique({ where: { email } }); if (existing) { console.log(` ℹ User "${email}" already exists, skipping`); return existing; } return prisma.user.create({ data }); } async function ensureModule( cmsId: string, data: Parameters[0]["data"], ) { const existing = await prisma.module.findFirst({ where: { cmsId } }); if (existing) { return existing; } return prisma.module.create({ data }); } async function ensureLesson( cmsId: string, data: Parameters[0]["data"], ) { const existing = await prisma.lesson.findFirst({ where: { cmsId } }); if (existing) { return existing; } return prisma.lesson.create({ data }); } async function ensureBadge( key: string, data: Parameters[0]["data"], ) { const existing = await prisma.badge.findUnique({ where: { key } }); if (existing) { console.log(` ℹ Badge "${key}" already exists, skipping`); return existing; } return prisma.badge.create({ data }); } // ─── Data Definitions ──────────────────────────────────────────────────────── const CURRICULUM_DATA: Array<{ track: string; modules: Array<{ title: string; description: string; order: number; estimatedHours: number; lessons: Array<{ title: string; description: string; order: number; estimatedMins: number; }>; }>; }> = [ { track: "SURVIVAL_BASICS", modules: [ { title: "Budgeting 101", description: "Learn how to create and maintain a personal budget", order: 1, estimatedHours: 1.5, lessons: [ { title: "What is a Budget?", description: "Understanding the fundamentals of budgeting", order: 1, estimatedMins: 10, }, { title: "Tracking Your Expenses", description: "How to monitor where your money goes", order: 2, estimatedMins: 15, }, { title: "The 50/30/20 Rule", description: "A simple framework for allocating your income", order: 3, estimatedMins: 12, }, ], }, { title: "Emergency Fund", description: "Build your financial safety net", order: 2, estimatedHours: 1.0, lessons: [ { title: "Why You Need an Emergency Fund", description: "The importance of having cash reserves", order: 1, estimatedMins: 8, }, { title: "How Much to Save", description: "Calculating your target emergency fund amount", order: 2, estimatedMins: 10, }, { title: "Where to Keep Your Fund", description: "High-yield savings accounts and other options", order: 3, estimatedMins: 7, }, ], }, ], }, { track: "CREDIT_DEBT", modules: [ { title: "Understanding Credit Scores", description: "Demystifying credit reporting and scoring", order: 1, estimatedHours: 1.5, lessons: [ { title: "What Is a Credit Score?", description: "The basics of credit scoring models", order: 1, estimatedMins: 10, }, { title: "Factors That Affect Your Score", description: "Payment history, utilization, and more", order: 2, estimatedMins: 15, }, { title: "How to Improve Your Credit", description: "Actionable steps to build better credit", order: 3, estimatedMins: 12, }, ], }, { title: "Debt Repayment Strategies", description: "Methods to eliminate debt efficiently", order: 2, estimatedHours: 1.0, lessons: [ { title: "Snowball vs Avalanche", description: "Comparing two popular debt payoff methods", order: 1, estimatedMins: 10, }, { title: "Debt Consolidation", description: "When and how to consolidate your debts", order: 2, estimatedMins: 8, }, { title: "Avoiding Debt Traps", description: "Predatory lending and high-interest pitfalls", order: 3, estimatedMins: 10, }, ], }, ], }, { track: "WEALTH_BUILDER", modules: [ { title: "Investing Basics", description: "Getting started with investing", order: 1, estimatedHours: 2.0, lessons: [ { title: "Stocks, Bonds & ETFs", description: "Understanding different asset classes", order: 1, estimatedMins: 15, }, { title: "Risk & Return", description: "The fundamental trade-off in investing", order: 2, estimatedMins: 12, }, { title: "Dollar-Cost Averaging", description: "A disciplined approach to investing regularly", order: 3, estimatedMins: 10, }, ], }, { title: "Compound Interest", description: "The eighth wonder of the world", order: 2, estimatedHours: 1.0, lessons: [ { title: "How Compounding Works", description: "The math behind exponential growth", order: 1, estimatedMins: 10, }, { title: "Time Is Your Superpower", description: "Why starting early matters more than timing", order: 2, estimatedMins: 8, }, { title: "The Rule of 72", description: "A quick way to estimate doubling time", order: 3, estimatedMins: 6, }, ], }, ], }, { track: "ANTI_HYPE", modules: [ { title: "Critical Thinking in Finance", description: "Developing a skeptic's mindset", order: 1, estimatedHours: 1.5, lessons: [ { title: "Identifying Hype Cycles", description: "Recognizing when euphoria drives markets", order: 1, estimatedMins: 12, }, { title: "Social Media & Investing", description: "How online echo chambers affect decisions", order: 2, estimatedMins: 10, }, { title: "Due Diligence 101", description: "Researching before you invest", order: 3, estimatedMins: 15, }, ], }, { title: "Recognizing Bubbles", description: "Historical and modern market bubbles", order: 2, estimatedHours: 1.5, lessons: [ { title: "Tulip Mania to Dot-Com", description: "Classic bubble case studies", order: 1, estimatedMins: 15, }, { title: "Crypto Mania", description: "Evaluating digital asset hype", order: 2, estimatedMins: 12, }, { title: "Bubble Indicators", description: "Signs that a market is overheating", order: 3, estimatedMins: 10, }, ], }, ], }, { track: "MACROECONOMICS", modules: [ { title: "Supply and Demand", description: "The foundation of economic thinking", order: 1, estimatedHours: 1.5, lessons: [ { title: "Market Equilibrium", description: "How prices are determined", order: 1, estimatedMins: 12, }, { title: "Elasticity", description: "How quantity responds to price changes", order: 2, estimatedMins: 10, }, { title: "Market Failures", description: "Externalities, public goods, and inefficiencies", order: 3, estimatedMins: 14, }, ], }, { title: "Monetary Policy", description: "How central banks shape the economy", order: 2, estimatedHours: 1.5, lessons: [ { title: "Interest Rates & Inflation", description: "The tools central banks use", order: 1, estimatedMins: 12, }, { title: "Quantitative Easing", description: "Unconventional monetary policy explained", order: 2, estimatedMins: 10, }, { title: "Global Economic Indicators", description: "GDP, unemployment, and CPI", order: 3, estimatedMins: 15, }, ], }, ], }, ]; const BADGE_DATA: Array<{ key: string; name: string; description: string; icon: string; category: string; criteria: Record; xpReward: number; }> = [ { key: "first-trade", name: "First Trade", description: "Execute your very first trade in any simulation", icon: "trophy", category: "MILESTONE", criteria: { type: "trade_count", threshold: 1 }, xpReward: 25, }, { key: "diversifier", name: "Diversifier", description: "Hold 5 or more different assets in a single portfolio", icon: "pie-chart", category: "SKILL", criteria: { type: "asset_diversity", threshold: 5 }, xpReward: 75, }, { key: "crash-survivor", name: "Crash Survivor", description: "Survive a market crash simulation without going bankrupt", icon: "shield", category: "CHALLENGE", criteria: { type: "simulation_survive", simulationType: "MARKET_CRASH" }, xpReward: 100, }, { key: "debt-destroyer", name: "Debt Destroyer", description: "Successfully complete the debt repayment simulation", icon: "broken-chain", category: "CHALLENGE", criteria: { type: "simulation_complete", simulationType: "DEBT_REPAYMENT" }, xpReward: 100, }, { key: "the-skeptic", name: "The Skeptic", description: "Correctly identify and avoid 3 hype traps", icon: "magnifying-glass", category: "SKILL", criteria: { type: "hype_avoided", threshold: 3 }, xpReward: 150, }, { key: "streak-master", name: "Streak Master", description: "Maintain a 30-day login streak", icon: "fire", category: "STREAK", criteria: { type: "streak_days", threshold: 30 }, xpReward: 200, }, { key: "portfolio-millionaire", name: "Portfolio Millionaire", description: "Reach a portfolio value of $1,000,000", icon: "diamond", category: "MILESTONE", criteria: { type: "portfolio_value", threshold: 1_000_000 }, xpReward: 500, }, { key: "quick-learner", name: "Quick Learner", description: "Complete 10 lessons in a single day", icon: "rocket", category: "CHALLENGE", criteria: { type: "lessons_in_day", threshold: 10 }, xpReward: 100, }, { key: "teacher-pet", name: "Teacher's Pet", description: "Score 100% on 5 different quizzes", icon: "apple", category: "MILESTONE", criteria: { type: "perfect_quizzes", threshold: 5 }, xpReward: 150, }, { key: "risk-avoider", name: "Risk Avoider", description: "Heed 5 risk warnings instead of ignoring them", icon: "umbrella", category: "SKILL", criteria: { type: "risk_warnings_heeded", threshold: 5 }, xpReward: 75, }, { key: "simulation-champion", name: "Simulation Champion", description: "Win or top the leaderboard in 3 different simulations", icon: "crown", category: "CHALLENGE", criteria: { type: "simulation_wins", threshold: 3 }, xpReward: 250, }, { key: "hype-avoider", name: "Hype Avoider", description: "Resist the urge to buy into a trending hype asset", icon: "eye", category: "SPECIAL", criteria: { type: "hype_trap_resisted", threshold: 1 }, xpReward: 50, }, ]; // ─── Main Seeding Logic ────────────────────────────────────────────────────── async function main() { console.log("🌱 Seeding InvestPlay database...\n"); // ── 1. Tenants ────────────────────────────────────────────────────────── console.log("📋 Creating tenants..."); const platformTenant = await ensureTenant("investplay", { id: cuid("tenant-platform"), name: "InvestPlay", slug: "investplay", domain: "app.investplay.dev", domains: ["app.investplay.dev"], branding: { logoUrl: "https://investplay.dev/logo.png", primaryColor: "#6366f1", accentColor: "#06b6d4", }, featureFlags: { aiCoach: true, simulations: true, classrooms: true, newsFeed: true, }, aiMode: "MANAGED", aiMonthlyTokenBudget: 10_000_000, defaultLocale: "en", subscriptionStatus: "ACTIVE", maxStudents: 1000, }); console.log(` ✅ Platform tenant: ${platformTenant.slug}`); const demoTenant = await ensureTenant("demo-university", { id: cuid("tenant-demo"), name: "Demo University", slug: "demo-university", domain: "demo.university.edu", domains: ["demo.university.edu", "student.demo.edu"], branding: { logoUrl: "https://demo.university.edu/logo.png", primaryColor: "#059669", accentColor: "#f59e0b", }, featureFlags: { aiCoach: true, simulations: true, classrooms: true, newsFeed: false, }, aiMode: "MANAGED", aiMonthlyTokenBudget: 5_000_000, defaultLocale: "en", subscriptionStatus: "ACTIVE", maxStudents: 200, }); console.log(` ✅ Demo tenant: ${demoTenant.slug}\n`); // ── 2. Users ──────────────────────────────────────────────────────────── console.log("👤 Creating users..."); const passwordHash = await bcrypt.hash("Admin123!", 10); const adminUser = await ensureUser("admin@investplay.dev", { id: cuid("user-admin"), email: "admin@investplay.dev", passwordHash, firstName: "Platform", lastName: "Admin", role: "PLATFORM_ADMIN", locale: "en", tenantId: platformTenant.id, xp: 9999, level: 50, streakDays: 100, emailVerifiedAt: new Date(), }); console.log(` ✅ Platform admin: ${adminUser.email}`); const teacherUser = await ensureUser("teacher@demouni.edu", { id: cuid("user-teacher"), email: "teacher@demouni.edu", passwordHash, firstName: "Jane", lastName: "Educator", role: "TEACHER", locale: "en", tenantId: demoTenant.id, xp: 2500, level: 15, streakDays: 30, emailVerifiedAt: new Date(), }); console.log(` ✅ Demo teacher: ${teacherUser.email}`); const studentUser = await ensureUser("student@demo.edu", { id: cuid("user-student"), email: "student@demo.edu", passwordHash, firstName: "Alex", lastName: "Learner", role: "STUDENT", locale: "en", tenantId: demoTenant.id, xp: 450, level: 5, streakDays: 7, emailVerifiedAt: new Date(), }); console.log(` ✅ Demo student: ${studentUser.email}\n`); // ── 3. Curriculum ─────────────────────────────────────────────────────── console.log("📚 Creating curriculum..."); let lessonIdCounter = 0; const createdLessons: Array<{ cmsId: string; lessonId: string; track: string }> = []; for (const trackData of CURRICULUM_DATA) { let moduleIndex = 0; for (const moduleData of trackData.modules) { moduleIndex++; const moduleCmsId = mockCmsId( trackData.track.substring(0, 3), moduleIndex, ); const module = await ensureModule(moduleCmsId, { id: cuid(`module-${trackData.track}-${moduleIndex}`), cmsId: moduleCmsId, track: trackData.track as any, title: moduleData.title, description: moduleData.description, order: moduleData.order, estimatedHours: moduleData.estimatedHours, xpReward: 100, tenantId: null, isActive: true, }); let lessonIndex = 0; for (const lessonData of moduleData.lessons) { lessonIndex++; lessonIdCounter++; const lessonCmsId = mockCmsId( `${trackData.track.substring(0, 3)}${moduleIndex}L`, lessonIndex, ); // Determine gating: first lesson of each module has no prerequisite // Subsequent lessons require the previous lesson in the module const requiredLessonCmsId = lessonIndex > 1 ? mockCmsId( `${trackData.track.substring(0, 3)}${moduleIndex}L`, lessonIndex - 1, ) : null; const requiredLesson = requiredLessonCmsId !== null ? createdLessons.find( (l) => l.cmsId === requiredLessonCmsId && l.track === trackData.track, ) : null; const lesson = await ensureLesson(lessonCmsId, { id: cuid(`lesson-${trackData.track}-${moduleIndex}-${lessonIndex}`), cmsId: lessonCmsId, moduleId: module.id, title: lessonData.title, description: lessonData.description, order: lessonData.order, estimatedMins: lessonData.estimatedMins, xpReward: 25, requiredLessonId: requiredLesson?.lessonId ?? null, blocks: { type: "doc", content: [ { type: "heading", attrs: { level: 1 }, content: [{ type: "text", text: lessonData.title }], }, { type: "paragraph", content: [ { type: "text", text: lessonData.description, }, ], }, ], }, isActive: true, }); createdLessons.push({ cmsId: lessonCmsId, lessonId: lesson.id, track: trackData.track, }); } } } console.log(` ✅ Created ${createdLessons.length} lessons across 10 modules\n`); // ── 4. Badges ─────────────────────────────────────────────────────────── console.log("🏅 Creating badges..."); for (const badgeData of BADGE_DATA) { await ensureBadge(badgeData.key, { id: cuid(`badge-${badgeData.key}`), key: badgeData.key, name: badgeData.name, description: badgeData.description, icon: badgeData.icon, category: badgeData.category as any, criteria: badgeData.criteria, xpReward: badgeData.xpReward, }); } console.log(` ✅ Created ${BADGE_DATA.length} badges\n`); // ── 5. Classroom ──────────────────────────────────────────────────────── console.log("🏫 Creating demo classroom..."); const classroomId = cuid("classroom-demo"); const existingClassroom = await prisma.classroom.findFirst({ where: { inviteCode: "DEMO123" }, }); let classroom; if (existingClassroom) { classroom = existingClassroom; console.log(` ℹ Classroom "Demo 101" already exists, skipping`); } else { classroom = await prisma.classroom.create({ data: { id: classroomId, name: "Demo 101 – Personal Finance", description: "Introduction to personal finance for first-year students. This classroom covers budgeting, credit, and investing fundamentals.", tenantId: demoTenant.id, teacherId: teacherUser.id, inviteCode: "DEMO123", isActive: true, }, }); console.log(` ✅ Created classroom: ${classroom.name}`); // Enroll the student const existingEnrollment = await prisma.classroomStudent.findUnique({ where: { classroomId_userId: { classroomId: classroom.id, userId: studentUser.id, }, }, }); if (!existingEnrollment) { await prisma.classroomStudent.create({ data: { id: cuid("enrollment-demo"), classroomId: classroom.id, userId: studentUser.id, }, }); console.log(` ✅ Enrolled ${studentUser.email} in ${classroom.name}`); } else { console.log(` ℹ ${studentUser.email} already enrolled, skipping`); } } // ── 6. Assign first-module lesson progress for the demo student ──────── const firstLesson = createdLessons.find((l) => l.track === "SURVIVAL_BASICS"); if (firstLesson) { const existingProgress = await prisma.userProgress.findUnique({ where: { userId_lessonId: { userId: studentUser.id, lessonId: firstLesson.lessonId, }, }, }); if (!existingProgress) { await prisma.userProgress.create({ data: { id: cuid("progress-demo-1"), userId: studentUser.id, lessonId: firstLesson.lessonId, status: "COMPLETED", quizScore: 90, quizAttempts: 1, timeSpentSeconds: 420, completedAt: new Date(), }, }); // Also grant XP for the completed lesson await prisma.xPEvent.create({ data: { id: cuid("xpevent-demo-1"), userId: studentUser.id, reason: "LESSON_COMPLETED", amount: 25, context: { lessonId: firstLesson.lessonId }, }, }); console.log(` ✅ Demo student progress created for first lesson\n`); } else { console.log(` ℹ Demo student progress exists, skipping\n`); } } console.log("────────────────────────────────────────────"); console.log("✅ Seeding complete!"); console.log("────────────────────────────────────────────"); console.log(" Admin: admin@investplay.dev / Admin123!"); console.log(" Teacher: teacher@demouni.edu / Admin123!"); console.log(" Student: student@demo.edu / Admin123!"); console.log("────────────────────────────────────────────"); } main() .catch((e) => { console.error("❌ Seeding failed:", e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });