// 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" previewFeatures = ["postgresqlExtensions"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") extensions = [pgvector(map: "vector"), uuidOssp(map: "uuid-ossp")] } // ============================================ // User & Authentication // ============================================ model User { id String @id @default(uuid()) supabaseId String @unique @map("supabase_id") email String @unique name String? avatarUrl String? @map("avatar_url") // Subscription subscriptionTier SubscriptionTier @default(FREE) @map("subscription_tier") // Timestamps createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Role role UserRole @default(USER) // Relations settings UserSettings? subscription Subscription? courses Course[] @relation("AuthoredCourses") enrollments Enrollment[] lessonProgress LessonProgress[] purchases Purchase[] reviews Review[] generations CourseGeneration[] groupMembers GroupMember[] groupMessages GroupMessage[] uploadedSourceFiles CourseSourceFile[] homeworkSubmissions HomeworkSubmission[] supportTickets SupportTicket[] ticketMessages TicketMessage[] statusChanges CourseStatusHistory[] @relation("StatusChangedBy") cooperationRequests CooperationRequest[] @@map("users") } model UserSettings { id String @id @default(uuid()) userId String @unique @map("user_id") // AI Settings - user can override default model customAiModel String? @map("custom_ai_model") // e.g., "qwen/qwen3-coder-next" // Notification settings emailNotifications Boolean @default(true) @map("email_notifications") marketingEmails Boolean @default(false) @map("marketing_emails") // UI Preferences theme String @default("system") // "light", "dark", "system" language String @default("ru") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_settings") } enum UserRole { USER MODERATOR ADMIN } // ============================================ // Subscription & Payments // ============================================ enum SubscriptionTier { FREE PREMIUM PRO } model Subscription { id String @id @default(uuid()) userId String @unique @map("user_id") tier SubscriptionTier @default(FREE) // Stripe stripeCustomerId String? @unique @map("stripe_customer_id") stripeSubscriptionId String? @unique @map("stripe_subscription_id") stripePriceId String? @map("stripe_price_id") // Billing cycle currentPeriodStart DateTime? @map("current_period_start") currentPeriodEnd DateTime? @map("current_period_end") cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end") // Usage tracking coursesCreatedThisMonth Int @default(0) @map("courses_created_this_month") usageResetDate DateTime? @map("usage_reset_date") // Status status String @default("active") // active, canceled, past_due, incomplete createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("subscriptions") } // ============================================ // Courses // ============================================ enum CourseStatus { DRAFT GENERATING PENDING_MODERATION PENDING_REVIEW APPROVED PUBLISHED REJECTED ARCHIVED } model Course { id String @id @default(uuid()) authorId String @map("author_id") // Basic info title String description String? @db.Text slug String @unique coverImage String? @map("cover_image") // Status status CourseStatus @default(DRAFT) // Marketplace isPublished Boolean @default(false) @map("is_published") price Decimal? @db.Decimal(10, 2) // null = free course currency String @default("USD") // Author verification — author checked the content and vouches for quality isVerified Boolean @default(false) @map("is_verified") // Categorization categoryId String? @map("category_id") tags String[] @default([]) difficulty String? // "beginner", "intermediate", "advanced" estimatedHours Int? @map("estimated_hours") // SEO & metadata metaTitle String? @map("meta_title") metaDescription String? @map("meta_description") // Stats viewCount Int @default(0) @map("view_count") enrollmentCount Int @default(0) @map("enrollment_count") averageRating Float? @map("average_rating") // Timestamps publishedAt DateTime? @map("published_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Moderation moderationNote String? @db.Text @map("moderation_note") moderatedAt DateTime? @map("moderated_at") // Relations author User @relation("AuthoredCourses", fields: [authorId], references: [id], onDelete: Cascade) category Category? @relation(fields: [categoryId], references: [id]) chapters Chapter[] enrollments Enrollment[] purchases Purchase[] reviews Review[] generation CourseGeneration? groups CourseGroup[] statusHistory CourseStatusHistory[] sourceFiles CourseSourceFile[] // Vector embedding for semantic search embedding Unsupported("vector(1536)")? @@index([authorId]) @@index([status]) @@index([isPublished]) @@index([categoryId]) @@map("courses") } model Chapter { id String @id @default(uuid()) courseId String @map("course_id") title String description String? @db.Text order Int @default(0) // Timestamps createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) lessons Lesson[] @@index([courseId]) @@map("chapters") } model Lesson { id String @id @default(uuid()) chapterId String @map("chapter_id") title String content Json? // TipTap JSON content order Int @default(0) // Duration estimate durationMinutes Int? @map("duration_minutes") // Media videoUrl String? @map("video_url") // Timestamps createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) homework Homework[] quiz Quiz? groupMessages GroupMessage[] // Vector embedding for semantic search embedding Unsupported("vector(1536)")? @@index([chapterId]) @@map("lessons") } model Quiz { id String @id @default(uuid()) lessonId String @unique @map("lesson_id") questions Json // Array of {id, question, options, correctAnswer} createdAt DateTime @default(now()) @map("created_at") lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) @@map("quizzes") } // ============================================ // AI Course Generation // ============================================ enum GenerationStatus { PENDING ANALYZING ASKING_QUESTIONS WAITING_FOR_ANSWERS RESEARCHING GENERATING_OUTLINE GENERATING_CONTENT COMPLETED FAILED CANCELLED } model CourseGeneration { id String @id @default(uuid()) userId String @map("user_id") courseId String? @unique @map("course_id") // Input initialPrompt String @db.Text @map("initial_prompt") // AI Configuration aiModel String @map("ai_model") // Status & Progress status GenerationStatus @default(PENDING) progress Int @default(0) // 0-100 currentStep String? @map("current_step") // Clarifying questions flow questions Json? // Array of questions to ask answers Json? // User's answers // Generated outline (intermediate state) generatedOutline Json? @map("generated_outline") // Error handling errorMessage String? @db.Text @map("error_message") retryCount Int @default(0) @map("retry_count") // Job tracking jobId String? @map("job_id") // BullMQ job ID // Timestamps startedAt DateTime? @map("started_at") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) course Course? @relation(fields: [courseId], references: [id], onDelete: SetNull) @@index([userId]) @@index([status]) @@map("course_generations") } // ============================================ // Marketplace (Future) // ============================================ model Category { id String @id @default(uuid()) name String @unique slug String @unique description String? @db.Text icon String? parentId String? @map("parent_id") order Int @default(0) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id]) children Category[] @relation("CategoryHierarchy") courses Course[] @@map("categories") } enum PaymentMode { DEV PROD } enum PaymentProvider { STRIPE YOOMONEY } model Purchase { id String @id @default(uuid()) userId String @map("user_id") courseId String @map("course_id") // Payment details amount Decimal @db.Decimal(10, 2) currency String @default("USD") stripePaymentId String? @map("stripe_payment_id") provider PaymentProvider @default(STRIPE) mode PaymentMode @default(PROD) eventCode String? @map("event_code") metadata Json? // Status status String @default("completed") // pending, completed, refunded // Access accessGrantedAt DateTime @default(now()) @map("access_granted_at") accessExpiresAt DateTime? @map("access_expires_at") // null = lifetime access createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) @@unique([userId, courseId]) @@index([userId]) @@index([courseId]) @@map("purchases") } model Review { id String @id @default(uuid()) userId String @map("user_id") courseId String @map("course_id") rating Int // 1-5 title String? content String? @db.Text // Moderation isApproved Boolean @default(true) @map("is_approved") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) @@unique([userId, courseId]) @@index([courseId]) @@map("reviews") } // ============================================ // Enrollment & Progress // ============================================ model Enrollment { id String @id @default(uuid()) userId String @map("user_id") courseId String @map("course_id") // Progress progress Int @default(0) // 0-100 completedAt DateTime? @map("completed_at") // Certificate certificateUrl String? @map("certificate_url") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) lessons LessonProgress[] @@unique([userId, courseId]) @@index([userId]) @@index([courseId]) @@map("enrollments") } model LessonProgress { id String @id @default(uuid()) userId String @map("user_id") enrollmentId String @map("enrollment_id") lessonId String @map("lesson_id") completedAt DateTime? @map("completed_at") quizScore Int? @map("quiz_score") // 0-100 quizPassed Boolean @default(false) @map("quiz_passed") quizPassedAt DateTime? @map("quiz_passed_at") homeworkSubmitted Boolean @default(false) @map("homework_submitted") homeworkSubmittedAt DateTime? @map("homework_submitted_at") createdAt DateTime @default(now()) @map("created_at") // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) enrollment Enrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade) @@unique([userId, lessonId]) @@index([enrollmentId]) @@map("lesson_progress") } // ============================================ // Course Groups & Collaboration // ============================================ model CourseGroup { id String @id @default(uuid()) courseId String @map("course_id") name String description String? @db.Text isDefault Boolean @default(false) @map("is_default") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) members GroupMember[] messages GroupMessage[] @@index([courseId]) @@map("course_groups") } model GroupMember { id String @id @default(uuid()) groupId String @map("group_id") userId String @map("user_id") role String @default("student") // "teacher", "student" joinedAt DateTime @default(now()) @map("joined_at") group CourseGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([groupId, userId]) @@map("group_members") } model GroupMessage { id String @id @default(uuid()) groupId String @map("group_id") userId String @map("user_id") lessonId String? @map("lesson_id") content String @db.Text createdAt DateTime @default(now()) @map("created_at") group CourseGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) lesson Lesson? @relation(fields: [lessonId], references: [id], onDelete: SetNull) @@index([groupId]) @@index([lessonId]) @@map("group_messages") } model CourseStatusHistory { id String @id @default(uuid()) courseId String @map("course_id") fromStatus CourseStatus? @map("from_status") toStatus CourseStatus @map("to_status") note String? @db.Text changedById String? @map("changed_by_id") createdAt DateTime @default(now()) @map("created_at") course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) changedBy User? @relation("StatusChangedBy", fields: [changedById], references: [id], onDelete: SetNull) @@index([courseId, createdAt]) @@map("course_status_history") } enum CourseSourceType { PDF DOCX TXT PPTX IMAGE ZIP OTHER } enum CourseSourceParseStatus { PENDING PARSED FAILED SKIPPED } model CourseSourceFile { id String @id @default(uuid()) courseId String @map("course_id") uploadedById String @map("uploaded_by_id") fileName String @map("file_name") mimeType String @map("mime_type") fileSize Int @map("file_size") sourceType CourseSourceType @map("source_type") storagePath String @map("storage_path") parseStatus CourseSourceParseStatus @default(PENDING) @map("parse_status") extractedText String? @db.Text @map("extracted_text") extractedMeta Json? @map("extracted_meta") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) uploadedBy User @relation(fields: [uploadedById], references: [id], onDelete: Cascade) @@index([courseId, createdAt]) @@map("course_source_files") } // ============================================ // Homework & Assignments // ============================================ model Homework { id String @id @default(uuid()) lessonId String @unique @map("lesson_id") title String description String @db.Text type HomeworkType @default(TEXT) config Json? dueDate DateTime? @map("due_date") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) submissions HomeworkSubmission[] @@index([lessonId]) @@map("homework") } enum HomeworkReviewStatus { SUBMITTED AI_REVIEWED TEACHER_REVIEWED } enum HomeworkType { TEXT FILE PROJECT QUIZ GITHUB } model HomeworkSubmission { id String @id @default(uuid()) homeworkId String @map("homework_id") userId String @map("user_id") content String? @db.Text answerType HomeworkType @default(TEXT) @map("answer_type") attachmentUrl String? @map("attachment_url") githubUrl String? @map("github_url") projectMeta Json? @map("project_meta") quizAnswers Json? @map("quiz_answers") // AI grading aiScore Int? @map("ai_score") // 1-5 aiFeedback String? @db.Text @map("ai_feedback") // Teacher grading teacherScore Int? @map("teacher_score") // 1-5 teacherFeedback String? @db.Text @map("teacher_feedback") reviewStatus HomeworkReviewStatus @default(SUBMITTED) @map("review_status") submittedAt DateTime @default(now()) @map("submitted_at") gradedAt DateTime? @map("graded_at") homework Homework @relation(fields: [homeworkId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([homeworkId, userId]) @@map("homework_submissions") } // ============================================ // Support Tickets // ============================================ model SupportTicket { id String @id @default(uuid()) userId String @map("user_id") title String status String @default("open") // "open", "in_progress", "resolved", "closed" priority String @default("normal") // "low", "normal", "high" createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) messages TicketMessage[] @@index([userId]) @@index([status]) @@map("support_tickets") } model TicketMessage { id String @id @default(uuid()) ticketId String @map("ticket_id") userId String @map("user_id") content String @db.Text isStaff Boolean @default(false) @map("is_staff") createdAt DateTime @default(now()) @map("created_at") ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([ticketId]) @@map("ticket_messages") } model CooperationRequest { id String @id @default(uuid()) organization String contactName String @map("contact_name") email String phone String? role String? organizationType String? @map("organization_type") message String @db.Text status String @default("new") source String? @default("landing") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") userId String? @map("user_id") user User? @relation(fields: [userId], references: [id], onDelete: SetNull) @@index([status, createdAt]) @@map("cooperation_requests") }