import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; import { config } from '../config'; import { logger } from '../config/logger'; import { generateFilePath, uploadFile } from '../config/minio'; import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; import jwt from 'jsonwebtoken'; import { LessonAttachmentData, LessonData, ChapterData, CreateLessonInput, UpdateLessonInput, CreateChapterInput, UpdateChapterInput, ChaptersRequest, DeleteChapterRequest, ReorderChapterRequest, ListChaptersResponse, GetChapterResponse, CreateChapterResponse, UpdateChapterResponse, DeleteChapterResponse, ReorderChapterResponse, ChapterWithLessonsResponse, ListLessonsRequest, GetLessonRequest, CreateLessonRequest, UpdateLessonRequest, DeleteLessonRequest, ReorderLessonsRequest, ListLessonsResponse, GetLessonResponse, CreateLessonResponse, UpdateLessonResponse, DeleteLessonResponse, ReorderLessonsResponse, } from "../types/ChaptersLesson.typs"; import { CoursesInstructorService } from './CoursesInstructor.service'; export class ChaptersLessonService { async listChapters(request: ChaptersRequest): Promise { try { const { token, course_id } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const chapters = await prisma.chapter.findMany({ where: { course_id }, orderBy: { sort_order: 'asc' }, include: { lessons: { orderBy: { sort_order: 'asc' } } } }); return { code: 200, message: 'Chapters fetched successfully', data: chapters as ChapterData[], total: chapters.length }; } catch (error) { logger.error(`Error fetching chapters: ${error}`); throw error; } } async createChapter(request: CreateChapterInput): Promise { try { const { token, course_id, title, description, sort_order } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to create chapter'); } const chapter = await prisma.chapter.create({ data: { course_id, title, description, sort_order } }); return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData }; } catch (error) { logger.error(`Error creating chapter: ${error}`); throw error; } } async updateChapter(request: UpdateChapterInput): Promise { try { const { token, course_id, chapter_id, title, description, sort_order } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to update chapter'); } const chapter = await prisma.chapter.update({ where: { id: chapter_id }, data: { title, description, sort_order } }); return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData }; } catch (error) { logger.error(`Error updating chapter: ${error}`); throw error; } } async deleteChapter(request: DeleteChapterRequest): Promise { try { const { token, course_id, chapter_id } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to delete chapter'); } await prisma.chapter.delete({ where: { id: chapter_id } }); return { code: 200, message: 'Chapter deleted successfully' }; } catch (error) { logger.error(`Error deleting chapter: ${error}`); throw error; } } async reorderChapter(request: ReorderChapterRequest): Promise { try { const { token, course_id, chapter_id, sort_order } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to reorder chapter'); } const chapter = await prisma.chapter.update({ where: { id: chapter_id }, data: { sort_order } }); return { code: 200, message: 'Chapter reordered successfully', data: [chapter as ChapterData] }; } catch (error) { logger.error(`Error reordering chapter: ${error}`); throw error; } } async createLesson(request: CreateLessonInput): Promise { try { const { token, course_id, chapter_id, title, content, type, sort_order, video, attachments } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to create lesson'); } // Create the lesson first const lesson = await prisma.lesson.create({ data: { chapter_id, title, content, type, sort_order } }); const uploadedAttachments: { file_name: string; file_path: string; file_size: number; mime_type: string }[] = []; // Handle video upload for VIDEO type lessons if (type === 'VIDEO' && video) { const videoPath = generateFilePath(course_id, lesson.id, 'video', video.originalname); await uploadFile(videoPath, video.buffer, video.mimetype); // Save video as attachment await prisma.lessonAttachment.create({ data: { lesson_id: lesson.id, file_name: video.originalname, file_size: video.size, mime_type: video.mimetype, file_path: videoPath, sort_order: 0, } }); uploadedAttachments.push({ file_name: video.originalname, file_path: videoPath, file_size: video.size, mime_type: video.mimetype, }); // Handle additional attachments (PDFs, documents, etc.) if (attachments && attachments.length > 0) { for (let i = 0; i < attachments.length; i++) { const attachment = attachments[i]; const attachmentPath = generateFilePath(course_id, lesson.id, 'attachment', attachment.originalname); await uploadFile(attachmentPath, attachment.buffer, attachment.mimetype); await prisma.lessonAttachment.create({ data: { lesson_id: lesson.id, file_name: attachment.originalname, file_size: attachment.size, mime_type: attachment.mimetype, file_path: attachmentPath, sort_order: i + 1, // Start from 1 since video is 0 } }); uploadedAttachments.push({ file_name: attachment.originalname, file_path: attachmentPath, file_size: attachment.size, mime_type: attachment.mimetype, }); } } } else if (type === 'QUIZ' && request.quiz_data) { // Create Quiz with Questions and Choices const { quiz_data } = request; const userId = decodedToken.id; // Create the quiz const quiz = await prisma.quiz.create({ data: { lesson_id: lesson.id, title: quiz_data.title, description: quiz_data.description, passing_score: quiz_data.passing_score ?? 60, time_limit: quiz_data.time_limit, shuffle_questions: quiz_data.shuffle_questions ?? false, shuffle_choices: quiz_data.shuffle_choices ?? false, show_answers_after_completion: quiz_data.show_answers_after_completion ?? true, created_by: userId, } }); // Create questions with choices if (quiz_data.questions && quiz_data.questions.length > 0) { for (let i = 0; i < quiz_data.questions.length; i++) { const questionInput = quiz_data.questions[i]; // Create the question const question = await prisma.question.create({ data: { quiz_id: quiz.id, question: questionInput.question, explanation: questionInput.explanation, question_type: questionInput.question_type, score: questionInput.score ?? 1, sort_order: questionInput.sort_order ?? i, } }); // Create choices for this question if (questionInput.choices && questionInput.choices.length > 0) { for (let j = 0; j < questionInput.choices.length; j++) { const choiceInput = questionInput.choices[j]; await prisma.choice.create({ data: { question_id: question.id, text: choiceInput.text, is_correct: choiceInput.is_correct, sort_order: choiceInput.sort_order ?? j, } }); } } } } } // Fetch the complete lesson with attachments and quiz const completeLesson = await prisma.lesson.findUnique({ where: { id: lesson.id }, include: { attachments: { orderBy: { sort_order: 'asc' } }, quiz: { include: { questions: { orderBy: { sort_order: 'asc' }, include: { choices: { orderBy: { sort_order: 'asc' } } } } } } } }); return { code: 200, message: 'Lesson created successfully', data: completeLesson as LessonData }; } catch (error) { logger.error(`Error creating lesson: ${error}`); throw error; } } }