feat: complete Phase 1 foundation scaffold
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
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
- 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
This commit is contained in:
572
apps/api/prisma/schema.prisma
Normal file
572
apps/api/prisma/schema.prisma
Normal file
@@ -0,0 +1,572 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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")
|
||||
}
|
||||
Reference in New Issue
Block a user