// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================ // User Management // ============================================ model Role { id Int @id @default(autoincrement()) code String @unique @db.VarChar(50) name Json // { th: "", en: "" } description Json? created_at DateTime @default(now()) users User[] @@index([code]) @@map("roles") } model User { id Int @id @default(autoincrement()) username String @unique @db.VarChar(50) email String @unique @db.VarChar(255) password String @db.VarChar(255) role_id Int email_verified_at DateTime? is_deactivated Boolean @default(false) created_at DateTime @default(now()) updated_at DateTime? @updatedAt role Role @relation(fields: [role_id], references: [id], onDelete: Restrict) profile UserProfile? // Relations created_courses Course[] @relation("CourseCreator") approved_courses Course[] @relation("CourseApprover") instructor_courses CourseInstructor[] enrollments Enrollment[] lesson_progress LessonProgress[] quiz_attempts QuizAttempt[] certificates Certificate[] created_categories Category[] @relation("CategoryCreator") updated_categories Category[] @relation("CategoryUpdater") updated_profiles UserProfile[] @relation("ProfileUpdater") created_quizzes Quiz[] @relation("QuizCreator") updated_quizzes Quiz[] @relation("QuizUpdater") created_announcements Announcement[] @relation("AnnouncementCreator") updated_announcements Announcement[] @relation("AnnouncementUpdater") updated_courses Course[] @relation("CourseUpdater") orders Order[] instructor_balance InstructorBalance? withdrawal_requests WithdrawalRequest[] @relation("WithdrawalInstructor") approved_withdrawals WithdrawalRequest[] @relation("WithdrawalApprover") updated_withdrawals WithdrawalRequest[] @relation("WithdrawalUpdater") submitted_approvals CourseApproval[] @relation("ApprovalSubmitter") reviewed_approvals CourseApproval[] @relation("ApprovalReviewer") audit_logs AuditLog[] @@index([username]) @@index([email]) @@index([role_id]) @@map("users") } model UserProfile { id Int @id @default(autoincrement()) user_id Int @unique prefix Json? // { th: "นาย", en: "Mr." } first_name String @db.VarChar(100) last_name String @db.VarChar(100) phone String? @db.VarChar(20) avatar_url String? @db.VarChar(500) birth_date DateTime? created_at DateTime @default(now()) updated_at DateTime? @updatedAt updated_by Int? user User @relation(fields: [user_id], references: [id], onDelete: Cascade) updater User? @relation("ProfileUpdater", fields: [updated_by], references: [id]) @@index([user_id]) @@map("user_profiles") } model Category { id Int @id @default(autoincrement()) name Json // { th: "", en: "" } slug String @unique @db.VarChar(100) description Json? icon String? @db.VarChar(100) sort_order Int @default(0) is_active Boolean @default(true) created_at DateTime @default(now()) created_by Int updated_at DateTime? @updatedAt updated_by Int? courses Course[] creator User @relation("CategoryCreator", fields: [created_by], references: [id], onDelete: Restrict) updater User? @relation("CategoryUpdater", fields: [updated_by], references: [id]) @@index([slug]) @@index([is_active]) @@index([sort_order]) @@map("categories") } // ============================================ // Course Structure // ============================================ enum CourseStatus { DRAFT PENDING APPROVED REJECTED ARCHIVED } model Course { id Int @id @default(autoincrement()) category_id Int? title Json // { th: "", en: "" } slug String @unique @db.VarChar(200) description Json thumbnail_url String? @db.VarChar(500) price Decimal @default(0) @db.Decimal(10, 2) is_free Boolean @default(false) have_certificate Boolean @default(false) is_recommended Boolean @default(false) status CourseStatus @default(DRAFT) approved_by Int? approved_at DateTime? rejection_reason String? @db.Text created_at DateTime @default(now()) created_by Int updated_at DateTime? @updatedAt updated_by Int? category Category? @relation(fields: [category_id], references: [id], onDelete: SetNull) creator User @relation("CourseCreator", fields: [created_by], references: [id], onDelete: Restrict) approver User? @relation("CourseApprover", fields: [approved_by], references: [id]) updater User? @relation("CourseUpdater", fields: [updated_by], references: [id]) chapters Chapter[] instructors CourseInstructor[] enrollments Enrollment[] announcements Announcement[] courseApprovals CourseApproval[] @@index([category_id]) @@index([slug]) @@index([status]) @@index([created_by]) @@index([status, is_free]) @@index([category_id, status]) @@map("courses") } model CourseInstructor { id Int @id @default(autoincrement()) course_id Int user_id Int is_primary Boolean @default(false) joined_at DateTime @default(now()) course Course @relation(fields: [course_id], references: [id], onDelete: Cascade) user User @relation(fields: [user_id], references: [id], onDelete: Cascade) @@unique([course_id, user_id]) @@index([course_id]) @@index([user_id]) @@index([is_primary]) @@map("course_instructors") } enum ApprovalAction { SUBMITTED APPROVED REJECTED } model CourseApproval { id Int @id @default(autoincrement()) course_id Int submitted_by Int reviewed_by Int? action ApprovalAction @default(SUBMITTED) previous_status CourseStatus? new_status CourseStatus? comment String? @db.Text created_at DateTime @default(now()) course Course @relation(fields: [course_id], references: [id], onDelete: Cascade) submitter User @relation("ApprovalSubmitter", fields: [submitted_by], references: [id], onDelete: Restrict) reviewer User? @relation("ApprovalReviewer", fields: [reviewed_by], references: [id]) @@index([course_id]) @@index([submitted_by]) @@index([reviewed_by]) @@index([action]) @@index([created_at]) @@index([course_id, created_at]) @@map("course_approvals") } model Chapter { id Int @id @default(autoincrement()) course_id Int title Json // { th: "", en: "" } description Json? sort_order Int @default(0) is_published Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime? @updatedAt course Course @relation(fields: [course_id], references: [id], onDelete: Cascade) lessons Lesson[] @@index([course_id]) @@index([course_id, sort_order]) @@map("chapters") } // ============================================ // Lessons // ============================================ enum LessonType { VIDEO QUIZ } model Lesson { id Int @id @default(autoincrement()) chapter_id Int title Json // { th: "", en: "" } content Json? // multi-language lesson content type LessonType duration_minutes Int? sort_order Int @default(0) is_sequential Boolean @default(true) prerequisite_lesson_ids Json? // array of lesson IDs require_pass_quiz Boolean @default(false) is_published Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime? @updatedAt chapter Chapter @relation(fields: [chapter_id], references: [id], onDelete: Cascade) attachments LessonAttachment[] quiz Quiz? progress LessonProgress[] @@index([chapter_id]) @@index([chapter_id, sort_order]) @@index([type]) @@map("lessons") } model LessonAttachment { id Int @id @default(autoincrement()) lesson_id Int file_name String @db.VarChar(255) file_path String @db.VarChar(500) file_size Int mime_type String @db.VarChar(100) description Json? sort_order Int @default(0) created_at DateTime @default(now()) lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade) @@index([lesson_id]) @@index([lesson_id, sort_order]) @@map("lesson_attachments") } // ============================================ // Quiz System // ============================================ enum QuestionType { MULTIPLE_CHOICE TRUE_FALSE SHORT_ANSWER } model Quiz { id Int @id @default(autoincrement()) lesson_id Int @unique title Json // { th: "", en: "" } description Json? passing_score Int @default(60) time_limit Int? // in minutes shuffle_questions Boolean @default(false) shuffle_choices Boolean @default(false) show_answers_after_completion Boolean @default(true) is_skippable Boolean @default(true) allow_multiple_attempts Boolean @default(true) created_at DateTime @default(now()) created_by Int updated_at DateTime? @updatedAt updated_by Int? lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade) creator User @relation("QuizCreator", fields: [created_by], references: [id], onDelete: Restrict) updater User? @relation("QuizUpdater", fields: [updated_by], references: [id]) questions Question[] attempts QuizAttempt[] @@index([lesson_id]) @@map("quizzes") } model Question { id Int @id @default(autoincrement()) quiz_id Int question Json // { th: "", en: "" } explanation Json? // answer explanation question_type QuestionType @default(MULTIPLE_CHOICE) score Int @default(1) sort_order Int @default(0) created_at DateTime @default(now()) updated_at DateTime? @updatedAt quiz Quiz @relation(fields: [quiz_id], references: [id], onDelete: Cascade) choices Choice[] @@index([quiz_id]) @@index([quiz_id, sort_order]) @@map("questions") } model Choice { id Int @id @default(autoincrement()) question_id Int text Json // { th: "", en: "" } is_correct Boolean @default(false) sort_order Int @default(0) question Question @relation(fields: [question_id], references: [id], onDelete: Cascade) @@index([question_id]) @@map("choices") } // ============================================ // Student Progress // ============================================ enum EnrollmentStatus { ENROLLED IN_PROGRESS COMPLETED DROPPED } model Enrollment { id Int @id @default(autoincrement()) user_id Int course_id Int status EnrollmentStatus @default(ENROLLED) progress_percentage Int @default(0) enrolled_at DateTime @default(now()) started_at DateTime? completed_at DateTime? last_accessed_at DateTime? user User @relation(fields: [user_id], references: [id], onDelete: Cascade) course Course @relation(fields: [course_id], references: [id], onDelete: Cascade) certificates Certificate[] @@unique([user_id, course_id], name: "unique_enrollment") @@index([user_id]) @@index([course_id]) @@index([status]) @@index([last_accessed_at]) @@map("enrollments") } model Certificate { id Int @id @default(autoincrement()) user_id Int course_id Int enrollment_id Int @unique file_path String @db.VarChar(500) issued_at DateTime @default(now()) user User @relation(fields: [user_id], references: [id], onDelete: Cascade) enrollment Enrollment @relation(fields: [enrollment_id], references: [id], onDelete: Cascade) @@index([user_id]) @@index([course_id]) @@index([enrollment_id]) @@index([user_id, course_id]) @@map("certificates") } model LessonProgress { id Int @id @default(autoincrement()) user_id Int lesson_id Int is_completed Boolean @default(false) completed_at DateTime? video_progress_seconds Int @default(0) video_duration_seconds Int? video_progress_percentage Decimal? @db.Decimal(5, 2) last_watched_at DateTime? created_at DateTime @default(now()) updated_at DateTime? @updatedAt user User @relation(fields: [user_id], references: [id], onDelete: Cascade) lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade) @@unique([user_id, lesson_id]) @@index([user_id]) @@index([lesson_id]) @@index([last_watched_at]) @@map("lesson_progress") } model QuizAttempt { id Int @id @default(autoincrement()) user_id Int quiz_id Int score Int @default(0) total_questions Int correct_answers Int @default(0) is_passed Boolean @default(false) attempt_number Int @default(1) answers Json? // student answers for review started_at DateTime @default(now()) completed_at DateTime? quiz Quiz @relation(fields: [quiz_id], references: [id], onDelete: Cascade) user User @relation(fields: [user_id], references: [id], onDelete: Cascade) @@index([user_id]) @@index([quiz_id]) @@index([user_id, quiz_id]) @@index([user_id, quiz_id, attempt_number]) @@map("quiz_attempts") } // ============================================ // Communication // ============================================ enum AnnouncementStatus { DRAFT PUBLISHED ARCHIVED } model Announcement { id Int @id @default(autoincrement()) course_id Int title Json // { th: "", en: "" } content Json // { th: "", en: "" } status AnnouncementStatus @default(DRAFT) is_pinned Boolean @default(false) published_at DateTime? created_at DateTime @default(now()) created_by Int updated_at DateTime? @updatedAt updated_by Int? course Course @relation(fields: [course_id], references: [id], onDelete: Cascade) creator User @relation("AnnouncementCreator", fields: [created_by], references: [id], onDelete: Restrict) updater User? @relation("AnnouncementUpdater", fields: [updated_by], references: [id]) attachments AnnouncementAttachment[] @@index([course_id]) @@index([created_by]) @@index([status]) @@index([course_id, status, is_pinned, published_at]) @@map("announcements") } model AnnouncementAttachment { id Int @id @default(autoincrement()) announcement_id Int file_name String @db.VarChar(255) file_path String @db.VarChar(500) file_size Int mime_type String @db.VarChar(100) created_at DateTime @default(now()) announcement Announcement @relation(fields: [announcement_id], references: [id], onDelete: Cascade) @@index([announcement_id]) @@map("announcement_attachments") } // ============================================ // Payment System // ============================================ enum OrderStatus { PENDING PAID CANCELLED REFUNDED } enum PaymentStatus { PENDING SUCCESS FAILED } enum WithdrawalStatus { PENDING APPROVED REJECTED PAID } model Order { id Int @id @default(autoincrement()) user_id Int total_amount Decimal @default(0) @db.Decimal(10, 2) status OrderStatus @default(PENDING) created_at DateTime @default(now()) updated_at DateTime? @updatedAt user User @relation(fields: [user_id], references: [id], onDelete: Cascade) items OrderItem[] payments Payment[] @@index([user_id]) @@index([status]) @@index([user_id, status]) @@map("orders") } model OrderItem { id Int @id @default(autoincrement()) order_id Int course_id Int price Decimal @db.Decimal(10, 2) created_at DateTime @default(now()) order Order @relation(fields: [order_id], references: [id], onDelete: Cascade) @@index([order_id]) @@index([course_id]) @@map("order_items") } model Payment { id Int @id @default(autoincrement()) order_id Int provider String @db.VarChar(50) transaction_id String? @unique @db.VarChar(255) amount Decimal @db.Decimal(10, 2) status PaymentStatus @default(PENDING) paid_at DateTime? created_at DateTime @default(now()) updated_at DateTime? @updatedAt order Order @relation(fields: [order_id], references: [id], onDelete: Cascade) @@index([order_id]) @@index([transaction_id]) @@index([status]) @@map("payments") } model InstructorBalance { id Int @id @default(autoincrement()) instructor_id Int @unique available_amount Decimal @default(0) @db.Decimal(10, 2) withdrawn_amount Decimal @default(0) @db.Decimal(10, 2) created_at DateTime @default(now()) updated_at DateTime? @updatedAt instructor User @relation(fields: [instructor_id], references: [id], onDelete: Cascade) @@index([instructor_id]) @@map("instructor_balances") } model WithdrawalRequest { id Int @id @default(autoincrement()) instructor_id Int amount Decimal @db.Decimal(10, 2) status WithdrawalStatus @default(PENDING) approved_by Int? approved_at DateTime? rejected_reason String? @db.Text created_at DateTime @default(now()) updated_at DateTime? @updatedAt updated_by Int? instructor User @relation("WithdrawalInstructor", fields: [instructor_id], references: [id], onDelete: Cascade) approver User? @relation("WithdrawalApprover", fields: [approved_by], references: [id]) updater User? @relation("WithdrawalUpdater", fields: [updated_by], references: [id]) @@index([instructor_id]) @@index([status]) @@index([instructor_id, status]) @@map("withdrawal_requests") } // ============================================ // Audit Log System // ============================================ enum AuditAction { CREATE UPDATE DELETE LOGIN LOGOUT ENROLL UNENROLL SUBMIT_QUIZ APPROVE_COURSE REJECT_COURSE UPLOAD_FILE DELETE_FILE CHANGE_PASSWORD RESET_PASSWORD VERIFY_EMAIL DEACTIVATE_USER ACTIVATE_USER ERROR WARNING } model AuditLog { id Int @id @default(autoincrement()) user_id Int? action AuditAction entity_type String @db.VarChar(100) // Course, User, Lesson, Quiz, etc. entity_id Int? old_value Json? // ค่าเดิม (สำหรับ UPDATE/DELETE) new_value Json? // ค่าใหม่ (สำหรับ CREATE/UPDATE) ip_address String? @db.VarChar(45) // รองรับ IPv6 user_agent String? @db.Text metadata Json? // ข้อมูลเพิ่มเติม เช่น request_id, session_id created_at DateTime @default(now()) user User? @relation(fields: [user_id], references: [id], onDelete: SetNull) @@index([user_id]) @@index([action]) @@index([entity_type]) @@index([entity_type, entity_id]) @@index([created_at]) @@index([user_id, created_at]) @@index([action, created_at]) @@map("audit_logs") }