Files
pwa/prisma/schema.prisma
ar.badieiandCursor 370ac15471 Ship Novya PWA with backend for deployable production use.
Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 21:01:52 +03:30

1045 lines
39 KiB
Plaintext

// DESIGN ARTIFACT — not the runtime backend schema.
// Active backend schema: backend/prisma/schema.prisma
// Do not run migrations from this file against the backend MySQL database.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
enum UserStatus {
active
pending_verification
suspended
deleted
}
enum AuthProvider {
password
google
phone_otp
}
enum SessionStatus {
active
revoked
expired
}
enum EnrollmentStatus {
active
paused
completed
dropped
}
enum UnlockReason {
schedule
manual
prerequisite_completed
retry
}
enum LessonProgressStatus {
not_started
in_progress
completed
skipped
}
enum SubmissionStatus {
draft
submitted
reviewed
}
enum EventSource {
user
system
ai
admin
}
enum AttemptPhase {
pre
post
}
enum AttemptStatus {
in_progress
submitted
scored
canceled
}
enum AiRunStatus {
queued
running
succeeded
failed
canceled
}
enum AiRunType {
assessment_analysis
final_report
exercise_feedback
lesson_chat
}
enum ChatRole {
system
user
assistant
tool
}
enum RoleCode {
admin
researcher
user
}
enum JourneyStep {
registered
pre_assessment_assigned
pre_assessment_completed
in_program
lesson_in_progress
exercise_pending
post_assessment_assigned
post_assessment_completed
completed
}
enum DailyCheckinSource {
manual
ai
system
}
enum AssessmentAssignmentStatus {
assigned
available
started
completed
expired
canceled
}
enum NotificationQueueStatus {
pending
sent
failed
canceled
}
model Organization {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
externalId String @unique @default(uuid()) @db.Char(36)
name String @db.VarChar(191)
slug String @unique @db.VarChar(191)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
programs Program[]
providers AiProvider[]
}
model User {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
externalId String @unique @default(uuid()) @db.Char(36)
organizationId BigInt? @db.UnsignedBigInt
email String? @unique(map: "uq_users_email") @db.VarChar(191)
phone String? @unique(map: "uq_users_phone") @db.VarChar(32)
status UserStatus @default(active)
locale String @default("fa-IR") @db.VarChar(16)
timezone String @default("Asia/Tehran") @db.VarChar(64)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
profile UserProfile?
authIdentities AuthIdentity[]
authSessions AuthSession[]
userRoles UserRole[]
userConsents UserConsent[]
assessmentAssignments AssessmentAssignment[]
attempts AssessmentAttempt[]
enrollments ProgramEnrollment[]
journeyState UserJourneyState?
dailyCheckins DailyCheckin[]
journeyEvents UserJourneyEvent[]
dailySummaries DailyActivitySummary[]
aiRuns AiAnalysisRun[]
aiMemories AiMemory[]
chatSessions ChatSession[]
chatMessages ChatMessage[]
sessionSummaries SessionSummary[]
feedbacks AiFeedback[]
notificationQueue NotificationQueue[]
behaviorLogs BehaviorLog[]
auditLogs AuditLog[]
assessmentComparisons AssessmentComparison[]
@@index([organizationId], map: "idx_users_organization_id")
@@index([createdAt], map: "idx_users_created_at")
}
model AuthIdentity {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
provider AuthProvider
providerUserId String @db.VarChar(191)
providerEmail String? @db.VarChar(191)
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerUserId], map: "uq_auth_identities_provider_provider_user_id")
@@index([userId], map: "idx_auth_identities_user_id")
}
model AuthSession {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
refreshTokenHash String @unique @db.VarChar(255)
status SessionStatus @default(active)
ipAddress String? @db.VarChar(45)
userAgent String? @db.VarChar(512)
expiresAt DateTime
revokedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, status], map: "idx_auth_sessions_user_id_status")
@@index([expiresAt], map: "idx_auth_sessions_expires_at")
}
model Role {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
code RoleCode @unique
displayName String @db.VarChar(128)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userRoles UserRole[]
}
model UserRole {
userId BigInt @db.UnsignedBigInt
roleId BigInt @db.UnsignedBigInt
assignedAt DateTime @default(now())
assignedById BigInt? @db.UnsignedBigInt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@id([userId, roleId])
@@index([roleId], map: "idx_user_roles_role_id")
}
model UserProfile {
userId BigInt @id @db.UnsignedBigInt
firstName String? @db.VarChar(100)
lastName String? @db.VarChar(100)
displayName String? @db.VarChar(191)
avatarUrl String? @db.VarChar(512)
birthDate DateTime?
gender String? @db.VarChar(32)
countryCode String? @db.VarChar(8)
city String? @db.VarChar(100)
educationLevel String? @db.VarChar(100)
occupation String? @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Consent {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
key String @db.VarChar(100)
policyVersion String @db.VarChar(32)
title String @db.VarChar(191)
body String @db.Text
required Boolean @default(true)
active Boolean @default(true)
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userConsents UserConsent[]
@@unique([key, policyVersion], map: "uq_consents_key_policy_version")
}
model UserConsent {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
consentId BigInt @db.UnsignedBigInt
accepted Boolean @default(true)
acceptedAt DateTime @default(now())
ipAddress String? @db.VarChar(45)
userAgent String? @db.VarChar(512)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
consent Consent @relation(fields: [consentId], references: [id], onDelete: Cascade)
@@unique([userId, consentId], map: "uq_user_consents_user_id_consent_id")
@@index([consentId], map: "idx_user_consents_consent_id")
}
model AssessmentScale {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
key String @unique @db.VarChar(64)
title String @db.VarChar(191)
description String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
versions AssessmentScaleVersion[]
}
model AssessmentScaleVersion {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
scaleId BigInt @db.UnsignedBigInt
version String @db.VarChar(32)
isActive Boolean @default(false)
scoringMethod String @db.VarChar(64)
metadata Json?
effectiveFrom DateTime?
effectiveTo DateTime?
createdAt DateTime @default(now())
scale AssessmentScale @relation(fields: [scaleId], references: [id], onDelete: Cascade)
questions AssessmentQuestion[]
attempts AssessmentAttempt[]
assignments AssessmentAssignment[]
@@unique([scaleId, version], map: "uq_assessment_scale_versions_scale_id_version")
@@index([scaleId, isActive], map: "idx_assessment_scale_versions_scale_id_is_active")
}
model AssessmentQuestion {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
scaleVersionId BigInt @db.UnsignedBigInt
code String @db.VarChar(64)
orderNo Int
prompt String @db.Text
reverseScored Boolean @default(false)
createdAt DateTime @default(now())
scaleVersion AssessmentScaleVersion @relation(fields: [scaleVersionId], references: [id], onDelete: Cascade)
options AssessmentOption[]
answers AssessmentAnswer[]
@@unique([scaleVersionId, code], map: "uq_assessment_questions_scale_version_id_code")
@@unique([scaleVersionId, orderNo], map: "uq_assessment_questions_scale_version_id_order_no")
}
model AssessmentOption {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
questionId BigInt @db.UnsignedBigInt
value Int
label String @db.VarChar(191)
points Int
orderNo Int
question AssessmentQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
@@unique([questionId, value], map: "uq_assessment_options_question_id_value")
@@unique([questionId, orderNo], map: "uq_assessment_options_question_id_order_no")
}
model AssessmentAttempt {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
scaleVersionId BigInt @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
assignmentId BigInt? @db.UnsignedBigInt
phase AttemptPhase
status AttemptStatus @default(in_progress)
startedAt DateTime @default(now())
submittedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
scaleVersion AssessmentScaleVersion @relation(fields: [scaleVersionId], references: [id], onDelete: Restrict)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
assignment AssessmentAssignment? @relation(fields: [assignmentId], references: [id], onDelete: SetNull)
answers AssessmentAnswer[]
scores AssessmentScore[]
comparisonsAsPre AssessmentComparison[] @relation("ComparisonPreAttempt")
comparisonsAsPost AssessmentComparison[] @relation("ComparisonPostAttempt")
@@index([userId, phase, createdAt], map: "idx_assessment_attempts_user_id_phase_created_at")
@@index([enrollmentId], map: "idx_assessment_attempts_enrollment_id")
@@index([assignmentId], map: "idx_assessment_attempts_assignment_id")
}
model AssessmentAssignment {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
scaleVersionId BigInt @db.UnsignedBigInt
phase AttemptPhase
status AssessmentAssignmentStatus @default(assigned)
eligibleFrom DateTime?
dueAt DateTime?
attemptLimit Int?
attemptsUsed Int @default(0)
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
scaleVersion AssessmentScaleVersion @relation(fields: [scaleVersionId], references: [id], onDelete: Restrict)
attempts AssessmentAttempt[]
@@index([userId, phase, status], map: "idx_assessment_assignments_user_phase_status")
@@index([enrollmentId], map: "idx_assessment_assignments_enrollment_id")
@@index([dueAt], map: "idx_assessment_assignments_due_at")
@@map("assessment_assignments")
}
model AssessmentAnswer {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
attemptId BigInt @db.UnsignedBigInt
questionId BigInt @db.UnsignedBigInt
selectedValue Int?
answerText String? @db.Text
points Int @default(0)
createdAt DateTime @default(now())
attempt AssessmentAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade)
question AssessmentQuestion @relation(fields: [questionId], references: [id], onDelete: Restrict)
@@unique([attemptId, questionId], map: "uq_assessment_answers_attempt_id_question_id")
}
model AssessmentScore {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
attemptId BigInt @db.UnsignedBigInt
metricKey String @db.VarChar(100)
rawScore Decimal @db.Decimal(10, 2)
normalizedScore Decimal? @db.Decimal(10, 2)
interpretation String? @db.Text
createdAt DateTime @default(now())
attempt AssessmentAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade)
@@unique([attemptId, metricKey], map: "uq_assessment_scores_attempt_id_metric_key")
}
model AssessmentComparison {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
preAttemptId BigInt @unique
postAttemptId BigInt @unique
summaryText String? @db.Text
deltaJson Json?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
preAttempt AssessmentAttempt @relation("ComparisonPreAttempt", fields: [preAttemptId], references: [id], onDelete: Restrict)
postAttempt AssessmentAttempt @relation("ComparisonPostAttempt", fields: [postAttemptId], references: [id], onDelete: Restrict)
@@index([userId, createdAt], map: "idx_assessment_comparisons_user_id_created_at")
}
model Program {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
organizationId BigInt? @db.UnsignedBigInt
key String @unique @db.VarChar(64)
title String @db.VarChar(191)
description String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
versions ProgramVersion[]
}
model ProgramVersion {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
programId BigInt @db.UnsignedBigInt
version String @db.VarChar(32)
isActive Boolean @default(false)
totalDays Int @default(0)
metadata Json?
createdAt DateTime @default(now())
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
modules Module[]
enrollments ProgramEnrollment[]
chatSessions ChatSession[]
journeyStates UserJourneyState[]
@@unique([programId, version], map: "uq_program_versions_program_id_version")
}
model Module {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
programVersionId BigInt @db.UnsignedBigInt
code String @db.VarChar(64)
title String @db.VarChar(191)
summary String? @db.Text
sequenceNo Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
programVersion ProgramVersion @relation(fields: [programVersionId], references: [id], onDelete: Cascade)
lessons Lesson[]
journeyStates UserJourneyState[]
chatSessions ChatSession[]
@@unique([programVersionId, code], map: "uq_modules_program_version_id_code")
@@unique([programVersionId, sequenceNo], map: "uq_modules_program_version_id_sequence_no")
@@unique([id, programVersionId], map: "uq_modules_id_program_version_id")
@@map("modules")
}
model Lesson {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
moduleId BigInt @db.UnsignedBigInt
code String @db.VarChar(64)
title String @db.VarChar(191)
summary String? @db.Text
dayNumber Int
sequenceNo Int
unlockRuleType String @db.VarChar(64)
estimatedMinutes Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
contents LessonContent[]
exercises Exercise[]
unlocks UserLessonUnlock[]
progresses UserLessonProgress[]
chatSessions ChatSession[]
aiMemories AiMemory[]
journeyStates UserJourneyState[]
sessionSummaries SessionSummary[]
@@unique([moduleId, code], map: "uq_lessons_module_id_code")
@@unique([moduleId, sequenceNo], map: "uq_lessons_module_id_sequence_no")
@@unique([id, moduleId], map: "uq_lessons_id_module_id")
}
model LessonContent {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
lessonId BigInt @db.UnsignedBigInt
contentType String @db.VarChar(32)
title String? @db.VarChar(191)
body String? @db.LongText
mediaUrl String? @db.VarChar(512)
orderNo Int
metadata Json?
createdAt DateTime @default(now())
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
@@unique([lessonId, orderNo], map: "uq_lesson_contents_lesson_id_order_no")
}
model Exercise {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
lessonId BigInt @db.UnsignedBigInt
code String @db.VarChar(64)
title String @db.VarChar(191)
instructions String? @db.Text
required Boolean @default(true)
orderNo Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
prompts ExercisePrompt[]
submissions UserExerciseSubmission[]
@@unique([lessonId, code], map: "uq_exercises_lesson_id_code")
@@unique([lessonId, orderNo], map: "uq_exercises_lesson_id_order_no")
}
model ExercisePrompt {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
exerciseId BigInt @db.UnsignedBigInt
code String @db.VarChar(64)
promptText String @db.Text
inputType String @db.VarChar(32)
orderNo Int
required Boolean @default(true)
exercise Exercise @relation(fields: [exerciseId], references: [id], onDelete: Cascade)
answers UserExerciseAnswer[]
@@unique([exerciseId, code], map: "uq_exercise_prompts_exercise_id_code")
@@unique([exerciseId, orderNo], map: "uq_exercise_prompts_exercise_id_order_no")
}
model ProgramEnrollment {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
programVersionId BigInt @db.UnsignedBigInt
status EnrollmentStatus @default(active)
startedAt DateTime @default(now())
endedAt DateTime?
currentDay Int @default(0)
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
programVersion ProgramVersion @relation(fields: [programVersionId], references: [id], onDelete: Restrict)
attempts AssessmentAttempt[]
assessmentAssignments AssessmentAssignment[]
comparisons AssessmentComparison[]
lessonUnlocks UserLessonUnlock[]
lessonProgresses UserLessonProgress[]
exerciseSubmissions UserExerciseSubmission[]
chatSessions ChatSession[]
aiRuns AiAnalysisRun[]
journeyEvents UserJourneyEvent[]
journeyStates UserJourneyState[]
dailyCheckins DailyCheckin[]
aiMemories AiMemory[]
sessionSummaries SessionSummary[]
notificationQueue NotificationQueue[]
@@unique([userId, programVersionId, startedAt], map: "uq_user_enrollments_user_program_started")
@@unique([id, userId, programVersionId], map: "uq_program_enrollments_id_user_program_version_id")
@@index([userId, status], map: "idx_user_program_enrollments_user_id_status")
@@map("program_enrollments")
}
model UserJourneyState {
userId BigInt @id @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
currentStep JourneyStep @default(registered)
currentProgramVersionId BigInt? @db.UnsignedBigInt
currentModuleId BigInt? @db.UnsignedBigInt
currentLessonId BigInt? @db.UnsignedBigInt
nextUnlockAt DateTime?
finishedAt DateTime?
lastActivityAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
currentProgramVersion ProgramVersion? @relation(fields: [currentProgramVersionId], references: [id], onDelete: SetNull)
currentModule Module? @relation(fields: [currentModuleId], references: [id], onDelete: SetNull)
currentLesson Lesson? @relation(fields: [currentLessonId], references: [id], onDelete: SetNull)
@@index([enrollmentId], map: "idx_user_journey_state_enrollment_id")
@@index([currentStep, nextUnlockAt], map: "idx_user_journey_state_step_next_unlock")
@@map("user_journey_state")
}
model DailyCheckin {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
checkinDate DateTime @db.Date
mood Int
energy Int
stress Int
sleep Int
note String? @db.Text
source DailyCheckinSource @default(manual)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
@@unique([userId, checkinDate], map: "uq_daily_checkins_user_id_checkin_date")
@@index([enrollmentId], map: "idx_daily_checkins_enrollment_id")
@@map("daily_checkins")
}
model UserLessonUnlock {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
enrollmentId BigInt @db.UnsignedBigInt
lessonId BigInt @db.UnsignedBigInt
reason UnlockReason
scheduledAt DateTime?
unlockedAt DateTime @default(now())
createdAt DateTime @default(now())
enrollment ProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
@@unique([enrollmentId, lessonId], map: "uq_user_lesson_unlocks_enrollment_id_lesson_id")
@@index([lessonId], map: "idx_user_lesson_unlocks_lesson_id")
}
model UserLessonProgress {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
enrollmentId BigInt @db.UnsignedBigInt
lessonId BigInt @db.UnsignedBigInt
status LessonProgressStatus @default(not_started)
startedAt DateTime?
completedAt DateTime?
progressPercent Decimal @default(0) @db.Decimal(5, 2)
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
enrollment ProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
chatSessions ChatSession[]
feedbacks AiFeedback[]
@@unique([enrollmentId, lessonId], map: "uq_user_lesson_progress_enrollment_id_lesson_id")
@@index([status, updatedAt], map: "idx_user_lesson_progress_status_updated_at")
}
model UserExerciseSubmission {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
enrollmentId BigInt @db.UnsignedBigInt
exerciseId BigInt @db.UnsignedBigInt
attemptNo Int @default(1)
status SubmissionStatus @default(draft)
submittedAt DateTime?
score Decimal? @db.Decimal(10, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
enrollment ProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
exercise Exercise @relation(fields: [exerciseId], references: [id], onDelete: Restrict)
answers UserExerciseAnswer[]
feedbacks AiFeedback[]
@@unique([enrollmentId, exerciseId, attemptNo], map: "uq_exercise_submissions_enrollment_exercise_attempt")
@@index([status, submittedAt], map: "idx_user_exercise_submissions_status_submitted_at")
}
model UserExerciseAnswer {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
submissionId BigInt @db.UnsignedBigInt
promptId BigInt @db.UnsignedBigInt
answerText String? @db.LongText
answerJson Json?
createdAt DateTime @default(now())
submission UserExerciseSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
prompt ExercisePrompt @relation(fields: [promptId], references: [id], onDelete: Restrict)
@@unique([submissionId, promptId], map: "uq_user_exercise_answers_submission_id_prompt_id")
}
model UserJourneyEvent {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
eventName String @db.VarChar(100)
source EventSource @default(system)
eventAt DateTime @default(now())
payload Json?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
@@index([userId, eventAt], map: "idx_user_journey_events_user_id_event_at")
@@index([eventName, eventAt], map: "idx_user_journey_events_event_name_event_at")
}
model DailyActivitySummary {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
activityDate DateTime @db.Date
lessonsCompleted Int @default(0)
exercisesSubmitted Int @default(0)
chatMessagesCount Int @default(0)
totalStudyMinutes Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, activityDate], map: "uq_daily_activity_summaries_user_id_activity_date")
}
model AiProvider {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
organizationId BigInt? @db.UnsignedBigInt
key String @unique @db.VarChar(64)
displayName String @db.VarChar(191)
endpointBaseUrl String @db.VarChar(512)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
models AiModel[]
runs AiAnalysisRun[]
}
model AiModel {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
providerId BigInt @db.UnsignedBigInt
modelKey String @db.VarChar(128)
displayName String @db.VarChar(191)
isDefault Boolean @default(false)
maxInputTokens Int?
maxOutputTokens Int?
active Boolean @default(true)
createdAt DateTime @default(now())
provider AiProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)
runs AiAnalysisRun[]
@@unique([providerId, modelKey], map: "uq_ai_models_provider_id_model_key")
}
model AiPrompt {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
key String @db.VarChar(100)
version String @db.VarChar(32)
promptText String @db.LongText
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
runs AiAnalysisRun[]
@@unique([key, version], map: "uq_ai_prompts_key_version")
}
model AiAnalysisRun {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
providerId BigInt @db.UnsignedBigInt
modelId BigInt @db.UnsignedBigInt
promptId BigInt? @db.UnsignedBigInt
runType AiRunType
status AiRunStatus @default(queued)
inputRefType String? @db.VarChar(64)
inputRefId BigInt? @db.UnsignedBigInt
startedAt DateTime?
finishedAt DateTime?
latencyMs Int?
totalTokens Int?
errorCode String? @db.VarChar(64)
errorMessage String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
provider AiProvider @relation(fields: [providerId], references: [id], onDelete: Restrict)
model AiModel @relation(fields: [modelId], references: [id], onDelete: Restrict)
prompt AiPrompt? @relation(fields: [promptId], references: [id], onDelete: SetNull)
outputs AiAnalysisOutput[]
aiFeedbacks AiFeedback[]
aiMemories AiMemory[]
sessionSummaries SessionSummary[]
@@index([userId, createdAt], map: "idx_ai_analysis_runs_user_id_created_at")
@@index([status, createdAt], map: "idx_ai_analysis_runs_status_created_at")
}
model AiAnalysisOutput {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
runId BigInt @db.UnsignedBigInt
outputType String @db.VarChar(64)
outputText String? @db.LongText
outputJson Json?
createdAt DateTime @default(now())
run AiAnalysisRun @relation(fields: [runId], references: [id], onDelete: Cascade)
@@index([runId], map: "idx_ai_analysis_outputs_run_id")
}
model AiMemory {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
lessonId BigInt? @db.UnsignedBigInt
sourceRunId BigInt? @db.UnsignedBigInt
memoryType String @db.VarChar(64)
summaryText String @db.Text
summaryJson Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
lesson Lesson? @relation(fields: [lessonId], references: [id], onDelete: SetNull)
sourceRun AiAnalysisRun? @relation(fields: [sourceRunId], references: [id], onDelete: SetNull)
@@index([userId, createdAt], map: "idx_ai_memories_user_id_created_at")
@@index([enrollmentId], map: "idx_ai_memories_enrollment_id")
@@map("ai_memories")
}
model SessionSummary {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt @db.UnsignedBigInt
lessonId BigInt @db.UnsignedBigInt
sessionId BigInt @unique @db.UnsignedBigInt
sourceRunId BigInt? @db.UnsignedBigInt
summaryText String @db.Text
summaryJson Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Restrict)
session ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
sourceRun AiAnalysisRun? @relation(fields: [sourceRunId], references: [id], onDelete: SetNull)
@@index([userId, createdAt], map: "idx_session_summaries_user_id_created_at")
@@index([enrollmentId, lessonId], map: "idx_session_summaries_enrollment_id_lesson_id")
@@map("session_summaries")
}
model ChatSession {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt @db.UnsignedBigInt
programVersionId BigInt @db.UnsignedBigInt
moduleId BigInt @db.UnsignedBigInt
lessonId BigInt @db.UnsignedBigInt
lessonProgressId BigInt? @db.UnsignedBigInt
title String? @db.VarChar(191)
startedAt DateTime @default(now())
endedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment @relation(fields: [enrollmentId, userId, programVersionId], references: [id, userId, programVersionId], onDelete: Cascade)
programVersion ProgramVersion @relation(fields: [programVersionId], references: [id], onDelete: Restrict)
module Module @relation(fields: [moduleId, programVersionId], references: [id, programVersionId], onDelete: Restrict)
lesson Lesson @relation(fields: [lessonId, moduleId], references: [id, moduleId], onDelete: Restrict)
lessonProgress UserLessonProgress? @relation(fields: [lessonProgressId], references: [id], onDelete: SetNull)
messages ChatMessage[]
summary SessionSummary?
@@index([userId, startedAt], map: "idx_chat_sessions_user_id_started_at")
@@index([userId, enrollmentId], map: "idx_chat_sessions_user_id_enrollment_id")
@@index([enrollmentId, lessonId], map: "idx_chat_sessions_enrollment_id_lesson_id")
}
model ChatMessage {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
sessionId BigInt @db.UnsignedBigInt
userId BigInt? @db.UnsignedBigInt
role ChatRole
content String @db.LongText
tokenCount Int?
moderationFlag Boolean @default(false)
createdAt DateTime @default(now())
session ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([sessionId, createdAt], map: "idx_chat_messages_session_id_created_at")
@@index([userId, createdAt], map: "idx_chat_messages_user_id_created_at")
}
model AiFeedback {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
lessonProgressId BigInt? @db.UnsignedBigInt
submissionId BigInt? @db.UnsignedBigInt
runId BigInt? @db.UnsignedBigInt
feedbackType String @db.VarChar(64)
summary String? @db.Text
feedbackJson Json?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
lessonProgress UserLessonProgress? @relation(fields: [lessonProgressId], references: [id], onDelete: SetNull)
submission UserExerciseSubmission? @relation(fields: [submissionId], references: [id], onDelete: SetNull)
run AiAnalysisRun? @relation(fields: [runId], references: [id], onDelete: SetNull)
@@index([userId, createdAt], map: "idx_ai_feedback_user_id_created_at")
}
model NotificationQueue {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt @db.UnsignedBigInt
enrollmentId BigInt? @db.UnsignedBigInt
type String @db.VarChar(100)
title String @db.VarChar(191)
body String @db.Text
payload Json?
scheduledAt DateTime
status NotificationQueueStatus @default(pending)
sentAt DateTime?
attemptCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
@@index([userId, status, scheduledAt], map: "idx_notification_queue_user_status_scheduled")
@@index([status, scheduledAt], map: "idx_notification_queue_status_scheduled")
@@map("notification_queue")
}
model BehaviorLog {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
userId BigInt? @db.UnsignedBigInt
eventName String @db.VarChar(100)
source String @db.VarChar(64)
payload Json?
ipAddress String? @db.VarChar(45)
userAgent String? @db.VarChar(512)
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([eventName, createdAt], map: "idx_behavior_logs_event_name_created_at")
@@index([userId, createdAt], map: "idx_behavior_logs_user_id_created_at")
}
model AuditLog {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
actorUserId BigInt? @db.UnsignedBigInt
action String @db.VarChar(100)
resourceType String @db.VarChar(100)
resourceId String? @db.VarChar(191)
metadata Json?
createdAt DateTime @default(now())
actor User? @relation(fields: [actorUserId], references: [id], onDelete: SetNull)
@@index([action, createdAt], map: "idx_audit_logs_action_created_at")
@@index([actorUserId, createdAt], map: "idx_audit_logs_actor_user_id_created_at")
}
model AdminReportSnapshot {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
reportDate DateTime @db.Date
metricKey String @db.VarChar(100)
metricValue Decimal @db.Decimal(16, 4)
dimensions Json?
createdAt DateTime @default(now())
@@index([reportDate, metricKey], map: "idx_admin_report_snapshots_report_date_metric_key")
}