Files
InvestPlay/apps/api/prisma/schema.prisma
Lefteris Notas e75f913f1c
Some checks failed
CI / Lint (push) Has been cancelled
CI / TypeCheck (push) Has been cancelled
CI / Test (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/api/Dockerfile name:api]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/cms/Dockerfile name:cms]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/web/Dockerfile name:web]) (push) Has been cancelled
CI / Build (push) Has been cancelled
Deploy / Deploy (push) Has been cancelled
feat: complete Phase 1 foundation scaffold
- Monorepo: Turborepo + pnpm workspaces with 7 apps/packages
- Backend: NestJS scaffold with 9 modules (auth, tenant, curriculum, simulation, portfolio, ai-coach, analytics, classroom, gamification)
- Frontend: React 18 + Vite + TailwindCSS + shadcn/ui with 10 pages, 14 UI primitives, 3 Zustand stores
- Database: Prisma schema with 19 models, 13 enums, seed script, multi-tenant ready
- Docker: Dev, production, and Portainer Compose files with Traefik reverse proxy
- Configuration: .env.example (47 vars), Zod validation, frontend-safe env exposure
- i18n: English + Greek locale files (10 files), ICU MessageFormat, react-i18next
- Shared packages: @investplay/types, @investplay/ui, @investplay/i18n, @investplay/utils
- CI/CD: GitHub Actions (lint, typecheck, test, build, deploy, PR checks)
- Documentation: CONTEXT.md, ARCHITECTURE.md (10 Mermaid diagrams), API.md, LOCALIZATION.md, DEVELOPMENT-ROADMAP.md
- Infrastructure: Dockerfiles (dev + prod), nginx configs, backup/healthcheck scripts
- Security: JWT auth guards, role-based access, rate limiting, Helmet, CORS, PII sanitization
2026-06-12 19:32:57 +03:00

573 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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")
}