// ───────────────────────────────────────────────────────────────────────────── // InvestPlay – Prisma Schema (PostgreSQL) // Multi-tenant financial education platform // ───────────────────────────────────────────────────────────────────────────── generator client { provider = "prisma-client-js" previewFeatures = ["multiSchema", "fullTextSearch"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ─── Enums ─────────────────────────────────────────────────────────────────── enum UserRole { STUDENT TEACHER TENANT_ADMIN PLATFORM_ADMIN CONTENT_MANAGER RESEARCHER } enum AIMode { MANAGED BYOK DISABLED } enum SubscriptionStatus { TRIALING ACTIVE PAST_DUE CANCELED EXPIRED } enum CurriculumTrack { SURVIVAL_BASICS CREDIT_DEBT WEALTH_BUILDER ANTI_HYPE MACROECONOMICS } enum ProgressStatus { NOT_STARTED IN_PROGRESS COMPLETED FAILED } enum SubmissionStatus { PENDING SUBMITTED GRADED RETURNED } enum SimulationType { BUDGET_CHALLENGE EMERGENCY_FUND COST_OF_LIVING BLIND_ASSET MARKET_CRASH RENT_VS_TRANSPORT SAVINGS_HABIT ESG_PORTFOLIO INTEREST_APY DEBT_REPAYMENT } enum SessionStatus { WAITING IN_PROGRESS ENDED CANCELLED } enum AssetType { STOCK ETF BOND CRYPTO CASH REAL_ESTATE COMMODITY } enum TradeType { BUY SELL } enum XPReason { LESSON_COMPLETED QUIZ_PASSED STREAK_MAINTAINED CHALLENGE_COMPLETED SIMULATION_COMPLETED DIVERSIFICATION RISK_MANAGEMENT HYPE_AVOIDED DEBT_PAID_OFF } enum BadgeCategory { MILESTONE SKILL CHALLENGE STREAK SPECIAL } enum AnalyticsEventType { LESSON_STARTED LESSON_COMPLETED QUIZ_ATTEMPTED SIMULATION_JOINED TRADE_EXECUTED RISK_WARNING_SHOWN RISK_WARNING_IGNORED PANIC_SELL_DETECTED HYPE_TRAP_TRIGGERED AI_COACH_CONSULTED } // ─── Core Identity ─────────────────────────────────────────────────────────── model User { id String @id @default(cuid()) email String @unique passwordHash String? firstName String lastName String role UserRole locale String @default("en") avatarUrl String? tenantId String xp Int @default(0) level Int @default(1) streakDays Int @default(0) lastActiveAt DateTime? emailVerifiedAt DateTime? ssoProvider String? ssoSubject String? deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Restrict, onUpdate: Cascade) progress UserProgress[] portfolios Portfolio[] @relation("UserPortfolio") trades Trade[] badges UserBadge[] classrooms ClassroomStudent[] aiLogs AILog[] refreshTokens RefreshToken[] quizAttempts QuizAttempt[] xpEvents XPEvent[] dailyStreaks DailyStreak[] analyticsEvents AnalyticsEvent[] createdClassrooms Classroom[] @relation("ClassroomTeacher") createdSimulations SimulationSession[] @relation("SimulationCreator") submittedAssignments AssignmentSubmission[] @@index([tenantId]) @@index([email]) @@index([role]) @@unique([ssoProvider, ssoSubject]) @@map("users") } model Tenant { id String @id @default(cuid()) name String slug String @unique domain String? domains Json? branding Json? featureFlags Json? aiMode AIMode @default(MANAGED) aiApiKey String? aiMonthlyTokenBudget Int @default(1000000) aiTokensUsedThisMonth Int @default(0) defaultLocale String @default("en") excludeFromResearch Boolean @default(false) subscriptionStatus SubscriptionStatus @default(TRIALING) stripeCustomerId String? stripeSubscriptionId String? maxStudents Int? deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt users User[] classrooms Classroom[] modules Module[] aiLogs AILog[] @@index([slug]) @@index([domain]) @@index([subscriptionStatus]) @@map("tenants") } model RefreshToken { id String @id @default(cuid()) token String @unique userId String expiresAt DateTime revokedAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@index([userId]) @@index([token]) @@map("refresh_tokens") } // ─── Curriculum ────────────────────────────────────────────────────────────── model Module { id String @id @default(cuid()) cmsId String track CurriculumTrack title String description String? order Int estimatedHours Float? xpReward Int @default(100) tenantId String? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lessons Lesson[] tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@index([track]) @@index([tenantId]) @@index([cmsId]) @@map("modules") } model Lesson { id String @id @default(cuid()) cmsId String moduleId String title String description String? order Int estimatedMins Int @default(5) xpReward Int @default(25) requiredLessonId String? blocks Json? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade, onUpdate: Cascade) requiredLesson Lesson? @relation("LessonPrerequisite", fields: [requiredLessonId], references: [id], onDelete: SetNull, onUpdate: Cascade) dependentLessons Lesson[] @relation("LessonPrerequisite") progress UserProgress[] assignments Assignment[] quizAttempts QuizAttempt[] @@index([moduleId]) @@index([cmsId]) @@map("lessons") } model UserProgress { id String @id @default(cuid()) userId String lessonId String status ProgressStatus quizScore Float? quizAttempts Int @default(0) timeSpentSeconds Int @default(0) completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Restrict, onUpdate: Cascade) @@unique([userId, lessonId]) @@index([userId]) @@index([lessonId]) @@index([status]) @@map("user_progress") } model QuizAttempt { id String @id @default(cuid()) userId String lessonId String answers Json score Float passed Boolean attemptedAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Restrict, onUpdate: Cascade) @@index([userId]) @@index([lessonId]) @@map("quiz_attempts") } // ─── Classroom ─────────────────────────────────────────────────────────────── model Classroom { id String @id @default(cuid()) name String description String? tenantId String teacherId String inviteCode String @unique inviteExpiresAt DateTime? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade, onUpdate: Cascade) teacher User @relation("ClassroomTeacher", fields: [teacherId], references: [id], onDelete: Restrict, onUpdate: Cascade) students ClassroomStudent[] assignments Assignment[] simulationSessions SimulationSession[] @@map("classrooms") } model ClassroomStudent { id String @id @default(cuid()) classroomId String userId String joinedAt DateTime @default(now()) leftAt DateTime? classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade, onUpdate: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@unique([classroomId, userId]) @@index([userId]) @@map("classroom_students") } model Assignment { id String @id @default(cuid()) classroomId String lessonId String title String description String? dueDate DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade, onUpdate: Cascade) lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Restrict, onUpdate: Cascade) submissions AssignmentSubmission[] @@map("assignments") } model AssignmentSubmission { id String @id @default(cuid()) assignmentId String userId String status SubmissionStatus score Float? feedback String? submittedAt DateTime? gradedAt DateTime? assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@unique([assignmentId, userId]) @@map("assignment_submissions") } // ─── Simulation & Portfolio ────────────────────────────────────────────────── model SimulationSession { id String @id @default(cuid()) type SimulationType title String description String? classroomId String? createdById String scenarioData Json status SessionStatus startedAt DateTime? endedAt DateTime? createdAt DateTime @default(now()) classroom Classroom? @relation(fields: [classroomId], references: [id], onDelete: SetNull, onUpdate: Cascade) creator User @relation("SimulationCreator", fields: [createdById], references: [id], onDelete: Restrict, onUpdate: Cascade) portfolios Portfolio[] @@map("simulation_sessions") } model Portfolio { id String @id @default(cuid()) userId String simulationSessionId String? name String @default("My Portfolio") cashBalance Decimal @default(0) initialBalance Decimal @default(10000) currentValue Decimal? roi Float? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation("UserPortfolio", fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) simulationSession SimulationSession? @relation(fields: [simulationSessionId], references: [id], onDelete: SetNull, onUpdate: Cascade) holdings Holding[] trades Trade[] @@map("portfolios") } model Holding { id String @id @default(cuid()) portfolioId String assetSymbol String assetName String? assetType AssetType quantity Decimal averagePrice Decimal currentPrice Decimal? allocationPercentage Float? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt portfolio Portfolio @relation(fields: [portfolioId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@index([portfolioId]) @@map("holdings") } model Trade { id String @id @default(cuid()) portfolioId String userId String assetSymbol String type TradeType quantity Decimal price Decimal totalValue Decimal simulationTick Int? executedAt DateTime @default(now()) portfolio Portfolio @relation(fields: [portfolioId], references: [id], onDelete: Cascade, onUpdate: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@index([portfolioId]) @@index([userId]) @@index([executedAt]) @@map("trades") } // ─── Gamification ──────────────────────────────────────────────────────────── model Badge { id String @id @default(cuid()) key String @unique name String description String icon String category BadgeCategory criteria Json xpReward Int @default(50) createdAt DateTime @default(now()) users UserBadge[] @@map("badges") } model UserBadge { id String @id @default(cuid()) userId String badgeId String earnedAt DateTime @default(now()) context Json? user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) badge Badge @relation(fields: [badgeId], references: [id], onDelete: Restrict, onUpdate: Cascade) @@unique([userId, badgeId]) @@index([userId]) @@map("user_badges") } model XPEvent { id String @id @default(cuid()) userId String reason XPReason amount Int context Json? awardedAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@index([userId]) @@index([awardedAt]) @@map("xp_events") } model DailyStreak { id String @id @default(cuid()) userId String date DateTime @db.Date activities Int user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@unique([userId, date]) @@index([userId]) @@map("daily_streaks") } // ─── AI Coach ──────────────────────────────────────────────────────────────── model AILog { id String @id @default(cuid()) tenantId String userId String sessionId String? message String responsePreview String? tokensUsed Int modelUsed String contextInjected Json? createdAt DateTime @default(now()) tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade, onUpdate: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@index([tenantId]) @@index([userId]) @@index([createdAt]) @@index([tenantId, createdAt]) @@map("ai_logs") } // ─── Analytics ─────────────────────────────────────────────────────────────── model AnalyticsEvent { id String @id @default(cuid()) userId String eventType AnalyticsEventType metadata Json sessionId String? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) @@index([userId]) @@index([eventType]) @@index([createdAt]) @@index([eventType, createdAt]) @@map("analytics_events") }