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:
38
packages/types/src/ai.ts
Normal file
38
packages/types/src/ai.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface AICoachMessage {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
metadata: {
|
||||
tokensUsed: number;
|
||||
processingTimeMs: number;
|
||||
model: string;
|
||||
} | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AICoachContext {
|
||||
userId: string;
|
||||
lessonId: string | null;
|
||||
simulationType: string | null;
|
||||
portfolio: Record<string, unknown> | null;
|
||||
recentDecisions: string[];
|
||||
userLevel: number;
|
||||
preferredLocale: string;
|
||||
}
|
||||
|
||||
export interface AIModeConfig {
|
||||
mode: "MANAGED" | "BYOK" | "DISABLED";
|
||||
apiEndpoint: string | null;
|
||||
modelName: string | null;
|
||||
maxTokensPerMessage: number;
|
||||
temperature: number;
|
||||
}
|
||||
|
||||
export interface AITokenBudget {
|
||||
tenantId: string;
|
||||
monthlyLimit: number;
|
||||
usedThisMonth: number;
|
||||
remaining: number;
|
||||
resetDate: string;
|
||||
}
|
||||
22
packages/types/src/analytics.ts
Normal file
22
packages/types/src/analytics.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export enum AnalyticsEventType {
|
||||
LESSON_STARTED = "LESSON_STARTED",
|
||||
LESSON_COMPLETED = "LESSON_COMPLETED",
|
||||
QUIZ_ATTEMPTED = "QUIZ_ATTEMPTED",
|
||||
SIMULATION_JOINED = "SIMULATION_JOINED",
|
||||
TRADE_EXECUTED = "TRADE_EXECUTED",
|
||||
RISK_WARNING_SHOWN = "RISK_WARNING_SHOWN",
|
||||
RISK_WARNING_IGNORED = "RISK_WARNING_IGNORED",
|
||||
PANIC_SELL_DETECTED = "PANIC_SELL_DETECTED",
|
||||
HYPE_TRAP_TRIGGERED = "HYPE_TRAP_TRIGGERED",
|
||||
AI_COACH_CONSULTED = "AI_COACH_CONSULTED",
|
||||
}
|
||||
|
||||
export interface AnalyticsEvent {
|
||||
id: string;
|
||||
eventType: AnalyticsEventType;
|
||||
userId: string;
|
||||
tenantId: string;
|
||||
sessionId: string | null;
|
||||
payload: Record<string, unknown>;
|
||||
timestamp: string;
|
||||
}
|
||||
76
packages/types/src/classroom.ts
Normal file
76
packages/types/src/classroom.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export interface Classroom {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
tenantId: string;
|
||||
teacherId: string;
|
||||
inviteCode: string;
|
||||
students: ClassroomStudent[];
|
||||
assignments: Assignment[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ClassroomStudent {
|
||||
id: string;
|
||||
classroomId: string;
|
||||
userId: string;
|
||||
joinedAt: string;
|
||||
studentName: string;
|
||||
studentEmail: string;
|
||||
}
|
||||
|
||||
export interface Assignment {
|
||||
id: string;
|
||||
classroomId: string;
|
||||
lessonId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
dueDate: string | null;
|
||||
requiredScore: number;
|
||||
maxAttempts: number;
|
||||
submissions: AssignmentSubmission[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AssignmentSubmission {
|
||||
id: string;
|
||||
assignmentId: string;
|
||||
userId: string;
|
||||
score: number | null;
|
||||
completed: boolean;
|
||||
attempts: number;
|
||||
submittedAt: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TeacherReport {
|
||||
classroomId: string;
|
||||
classroomName: string;
|
||||
totalStudents: number;
|
||||
averageScore: number;
|
||||
completionRate: number;
|
||||
studentReports: StudentReport[];
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
export interface StudentReport {
|
||||
userId: string;
|
||||
studentName: string;
|
||||
compositeScore: CompositeScore;
|
||||
lessonsCompleted: number;
|
||||
averageQuizScore: number;
|
||||
simulationsPlayed: number;
|
||||
totalXp: number;
|
||||
lastActivity: string | null;
|
||||
}
|
||||
|
||||
export interface CompositeScore {
|
||||
overall: number;
|
||||
roi: number;
|
||||
riskManagement: number;
|
||||
esgAlignment: number;
|
||||
quizScore: number;
|
||||
lessonCompletion: number;
|
||||
}
|
||||
70
packages/types/src/curriculum.ts
Normal file
70
packages/types/src/curriculum.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export enum BlockType {
|
||||
HERO = "HERO",
|
||||
TEXT = "TEXT",
|
||||
MEDIA = "MEDIA",
|
||||
QUIZ = "QUIZ",
|
||||
CALCULATOR = "CALCULATOR",
|
||||
BUDGET_GAME = "BUDGET_GAME",
|
||||
BLIND_SIMULATION = "BLIND_SIMULATION",
|
||||
DRAG_DROP = "DRAG_DROP",
|
||||
REFLECTION = "REFLECTION",
|
||||
TEACHER_NOTE = "TEACHER_NOTE",
|
||||
ASSIGNMENT = "ASSIGNMENT",
|
||||
SUMMARY = "SUMMARY",
|
||||
}
|
||||
|
||||
export enum CurriculumTrack {
|
||||
SURVIVAL_BASICS = "SURVIVAL_BASICS",
|
||||
CREDIT_DEBT = "CREDIT_DEBT",
|
||||
WEALTH_BUILDER = "WEALTH_BUILDER",
|
||||
ANTI_HYPE = "ANTI_HYPE",
|
||||
MACROECONOMICS = "MACROECONOMICS",
|
||||
}
|
||||
|
||||
export interface LessonBlock {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
order: number;
|
||||
content: Record<string, unknown>;
|
||||
config: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
track: CurriculumTrack;
|
||||
moduleId: string;
|
||||
order: number;
|
||||
estimatedMinutes: number;
|
||||
xpReward: number;
|
||||
blocks: LessonBlock[];
|
||||
tags: string[];
|
||||
prerequisites: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Module {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
track: CurriculumTrack;
|
||||
order: number;
|
||||
lessons: Lesson[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserProgress {
|
||||
userId: string;
|
||||
lessonId: string;
|
||||
completed: boolean;
|
||||
score: number | null;
|
||||
timeSpentSeconds: number;
|
||||
attempts: number;
|
||||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
60
packages/types/src/gamification.ts
Normal file
60
packages/types/src/gamification.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export enum XPReason {
|
||||
LESSON_COMPLETED = "LESSON_COMPLETED",
|
||||
QUIZ_PASSED = "QUIZ_PASSED",
|
||||
STREAK_MAINTAINED = "STREAK_MAINTAINED",
|
||||
CHALLENGE_COMPLETED = "CHALLENGE_COMPLETED",
|
||||
SIMULATION_COMPLETED = "SIMULATION_COMPLETED",
|
||||
DIVERSIFICATION = "DIVERSIFICATION",
|
||||
RISK_MANAGEMENT = "RISK_MANAGEMENT",
|
||||
HYPE_AVOIDED = "HYPE_AVOIDED",
|
||||
DEBT_PAID_OFF = "DEBT_PAID_OFF",
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
iconUrl: string;
|
||||
category: "milestone" | "skill" | "behavior" | "special";
|
||||
requiredXp: number | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Achievement {
|
||||
id: string;
|
||||
badgeId: string;
|
||||
userId: string;
|
||||
unlockedAt: string;
|
||||
badge: Badge;
|
||||
}
|
||||
|
||||
export interface UserBadge {
|
||||
id: string;
|
||||
userId: string;
|
||||
badgeId: string;
|
||||
badge: Badge;
|
||||
earnedAt: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export interface LeaderboardEntry {
|
||||
userId: string;
|
||||
rank: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
avatarUrl: string | null;
|
||||
xp: number;
|
||||
level: number;
|
||||
badges: number;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
export interface XPEvent {
|
||||
userId: string;
|
||||
amount: number;
|
||||
reason: XPReason;
|
||||
referenceId: string;
|
||||
referenceType: "lesson" | "quiz" | "simulation" | "challenge" | "streak";
|
||||
createdAt: string;
|
||||
}
|
||||
40
packages/types/src/index.ts
Normal file
40
packages/types/src/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export type { User, UserRole } from "./user.js";
|
||||
export type { Tenant, TenantFeatureFlags, AIMode } from "./tenant.js";
|
||||
export type {
|
||||
BlockType,
|
||||
CurriculumTrack,
|
||||
LessonBlock,
|
||||
Lesson,
|
||||
Module,
|
||||
UserProgress,
|
||||
} from "./curriculum.js";
|
||||
export type {
|
||||
SimulationType,
|
||||
SimulationTick,
|
||||
Portfolio,
|
||||
Trade,
|
||||
SimulationSession,
|
||||
} from "./simulation.js";
|
||||
export type {
|
||||
Badge,
|
||||
Achievement,
|
||||
UserBadge,
|
||||
LeaderboardEntry,
|
||||
XPEvent,
|
||||
XPReason,
|
||||
} from "./gamification.js";
|
||||
export type {
|
||||
Classroom,
|
||||
ClassroomStudent,
|
||||
Assignment,
|
||||
AssignmentSubmission,
|
||||
TeacherReport,
|
||||
CompositeScore,
|
||||
} from "./classroom.js";
|
||||
export type { AnalyticsEvent, AnalyticsEventType } from "./analytics.js";
|
||||
export type {
|
||||
AICoachMessage,
|
||||
AICoachContext,
|
||||
AIModeConfig,
|
||||
AITokenBudget,
|
||||
} from "./ai.js";
|
||||
91
packages/types/src/simulation.ts
Normal file
91
packages/types/src/simulation.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export enum SimulationType {
|
||||
BUDGET_CHALLENGE = "BUDGET_CHALLENGE",
|
||||
EMERGENCY_FUND = "EMERGENCY_FUND",
|
||||
COST_OF_LIVING = "COST_OF_LIVING",
|
||||
BLIND_ASSET = "BLIND_ASSET",
|
||||
MARKET_CRASH = "MARKET_CRASH",
|
||||
RENT_VS_TRANSPORT = "RENT_VS_TRANSPORT",
|
||||
SAVINGS_HABIT = "SAVINGS_HABIT",
|
||||
ESG_PORTFOLIO = "ESG_PORTFOLIO",
|
||||
INTEREST_APY = "INTEREST_APY",
|
||||
DEBT_REPAYMENT = "DEBT_REPAYMENT",
|
||||
}
|
||||
|
||||
export interface SimulationTick {
|
||||
tick: number;
|
||||
label: string;
|
||||
values: Record<string, number>;
|
||||
events: SimulationEvent[];
|
||||
}
|
||||
|
||||
export interface SimulationEvent {
|
||||
type: string;
|
||||
description: string;
|
||||
impact: Record<string, number>;
|
||||
choice?: {
|
||||
options: SimulationChoice[];
|
||||
chosenOptionId: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SimulationChoice {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
impacts: Record<string, number>;
|
||||
isTrap?: boolean;
|
||||
trapReveal?: string;
|
||||
}
|
||||
|
||||
export interface Portfolio {
|
||||
cash: number;
|
||||
investments: Investment[];
|
||||
debt: Debt[];
|
||||
monthlyIncome: number;
|
||||
monthlyExpenses: number;
|
||||
}
|
||||
|
||||
export interface Investment {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "stock" | "bond" | "etf" | "crypto" | "real_estate" | "commodity" | "cash";
|
||||
amount: number;
|
||||
value: number;
|
||||
risk: "low" | "medium" | "high";
|
||||
esgScore: number | null;
|
||||
}
|
||||
|
||||
export interface Debt {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "credit_card" | "student_loan" | "mortgage" | "personal_loan";
|
||||
principal: number;
|
||||
interestRate: number;
|
||||
minimumPayment: number;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
assetName: string;
|
||||
type: "buy" | "sell";
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalValue: number;
|
||||
timestamp: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface SimulationSession {
|
||||
id: string;
|
||||
userId: string;
|
||||
simulationType: SimulationType;
|
||||
ticks: SimulationTick[];
|
||||
portfolio: Portfolio;
|
||||
trades: Trade[];
|
||||
score: number;
|
||||
decisions: number;
|
||||
completed: boolean;
|
||||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
32
packages/types/src/tenant.ts
Normal file
32
packages/types/src/tenant.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export enum AIMode {
|
||||
MANAGED = "MANAGED",
|
||||
BYOK = "BYOK",
|
||||
DISABLED = "DISABLED",
|
||||
}
|
||||
|
||||
export interface TenantFeatureFlags {
|
||||
aiCoach: boolean;
|
||||
classroom: boolean;
|
||||
simulations: boolean;
|
||||
gamification: boolean;
|
||||
tenantDashboard: boolean;
|
||||
researchProgram: boolean;
|
||||
}
|
||||
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
domain: string | null;
|
||||
branding: {
|
||||
logoUrl: string | null;
|
||||
primaryColor: string | null;
|
||||
accentColor: string | null;
|
||||
faviconUrl: string | null;
|
||||
} | null;
|
||||
features: TenantFeatureFlags;
|
||||
aiMode: AIMode;
|
||||
monthlyTokenBudget: number;
|
||||
defaultLocale: string;
|
||||
excludeFromResearch: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
28
packages/types/src/user.ts
Normal file
28
packages/types/src/user.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export enum UserRole {
|
||||
STUDENT = "STUDENT",
|
||||
TEACHER = "TEACHER",
|
||||
TENANT_ADMIN = "TENANT_ADMIN",
|
||||
PLATFORM_ADMIN = "PLATFORM_ADMIN",
|
||||
CONTENT_MANAGER = "CONTENT_MANAGER",
|
||||
RESEARCHER = "RESEARCHER",
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
role: UserRole;
|
||||
locale: string;
|
||||
tenantId: string;
|
||||
xp: number;
|
||||
level: number;
|
||||
streaks: {
|
||||
current: number;
|
||||
longest: number;
|
||||
lastActivityDate: string | null;
|
||||
};
|
||||
avatarUrl: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user