import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; import { config } from '../config'; import { logger } from '../config/logger'; import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; import jwt from 'jsonwebtoken'; import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio'; import { CreateCourseInput, UpdateCourseInput, createCourseResponse, GetMyCourseResponse, ListMyCourseResponse, addinstructorCourse, addinstructorCourseResponse, removeinstructorCourse, removeinstructorCourseResponse, setprimaryCourseInstructor, setprimaryCourseInstructorResponse, submitCourseResponse, listinstructorCourseResponse, sendCourseForReview, getmyCourse, listinstructorCourse, } from "../types/CoursesInstructor.types"; export class CoursesInstructorService { static async createCourse(courseData: CreateCourseInput, userId: number, thumbnailFile?: Express.Multer.File): Promise { try { let thumbnailUrl: string | undefined; // Upload thumbnail to MinIO if provided if (thumbnailFile) { const timestamp = Date.now(); const uniqueId = Math.random().toString(36).substring(2, 15); const extension = thumbnailFile.originalname.split('.').pop() || 'jpg'; const safeFilename = `${timestamp}-${uniqueId}.${extension}`; const filePath = `courses/thumbnails/${safeFilename}`; await uploadFile(filePath, thumbnailFile.buffer, thumbnailFile.mimetype || 'image/jpeg'); thumbnailUrl = filePath; } // Use transaction to create course and instructor together const result = await prisma.$transaction(async (tx) => { // Create the course const courseCreated = await tx.course.create({ data: { category_id: courseData.category_id, title: courseData.title, slug: courseData.slug, description: courseData.description, thumbnail_url: thumbnailUrl, price: courseData.price || 0, is_free: courseData.is_free ?? false, have_certificate: courseData.have_certificate ?? false, created_by: userId, status: 'DRAFT' } }); // Add creator as primary instructor await tx.courseInstructor.create({ data: { course_id: courseCreated.id, user_id: userId, is_primary: true, } }); return courseCreated; }); return { code: 201, message: 'Course created successfully', data: result }; } catch (error) { logger.error('Failed to create course', { error }); throw error; } } static async listMyCourses(token: string): Promise { try { const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; const courseInstructors = await prisma.courseInstructor.findMany({ where: { user_id: decoded.id }, include: { course: true } }); const courses = await Promise.all( courseInstructors.map(async (ci) => { let thumbnail_presigned_url: string | null = null; if (ci.course.thumbnail_url) { try { thumbnail_presigned_url = await getPresignedUrl(ci.course.thumbnail_url, 3600); } catch (err) { logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`); } } return { ...ci.course, thumbnail_url: thumbnail_presigned_url, }; }) ); return { code: 200, message: 'Courses retrieved successfully', data: courses, total: courses.length }; } catch (error) { logger.error('Failed to retrieve courses', { error }); throw error; } } static async getmyCourse(getmyCourse: getmyCourse): Promise { try { const decoded = jwt.verify(getmyCourse.token, config.jwt.secret) as { id: number; type: string }; // Check if user is instructor of this course const courseInstructor = await prisma.courseInstructor.findFirst({ where: { user_id: decoded.id, course_id: getmyCourse.course_id }, include: { course: { include: { chapters: { include: { lessons: { include: { attachments: true, progress: true, quiz: true } } } } } } } }); if (!courseInstructor) { throw new ForbiddenError('You are not an instructor of this course'); } // Generate presigned URL for thumbnail let thumbnail_presigned_url: string | null = null; if (courseInstructor.course.thumbnail_url) { try { thumbnail_presigned_url = await getPresignedUrl(courseInstructor.course.thumbnail_url, 3600); } catch (err) { logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`); } } return { code: 200, message: 'Course retrieved successfully', data: { ...courseInstructor.course, thumbnail_url: thumbnail_presigned_url, } }; } catch (error) { logger.error('Failed to retrieve course', { error }); throw error; } } static async updateCourse(token: string, courseId: number, courseData: UpdateCourseInput): Promise { try { const courseInstructorId = await this.validateCourseInstructor(token, courseId); const course = await prisma.course.update({ where: { id: courseId }, data: courseData }); // Generate presigned URL for thumbnail let thumbnail_presigned_url: string | null = null; if (course.thumbnail_url) { try { thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600); } catch (err) { logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`); } } return { code: 200, message: 'Course updated successfully', data: { ...course, thumbnail_url: thumbnail_presigned_url, } }; } catch (error) { logger.error('Failed to update course', { error }); throw error; } } static async deleteCourse(token: string, courseId: number): Promise { try { const courseInstructorId = await this.validateCourseInstructor(token, courseId); if (!courseInstructorId.is_primary) { throw new ForbiddenError('You have no permission to delete this course'); } const course = await prisma.course.delete({ where: { id: courseId } }); return { code: 200, message: 'Course deleted successfully', data: course }; } catch (error) { logger.error('Failed to delete course', { error }); throw error; } } static async sendCourseForReview(sendCourseForReview: sendCourseForReview): Promise { try { const decoded = jwt.verify(sendCourseForReview.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseApproval.create({ data: { course_id: sendCourseForReview.course_id, submitted_by: decoded.id, } }); await prisma.course.update({ where: { id: sendCourseForReview.course_id }, data: { status: 'PENDING' } }); return { code: 200, message: 'Course sent for review successfully', }; } catch (error) { logger.error('Failed to send course for review', { error }); throw error; } } static async getCourseApprovals(token: string, courseId: number): Promise<{ code: number; message: string; data: any[]; total: number; }> { try { const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; // Validate instructor access await this.validateCourseInstructor(token, courseId); const approvals = await prisma.courseApproval.findMany({ where: { course_id: courseId }, orderBy: { created_at: 'desc' }, include: { submitter: { select: { id: true, username: true, email: true } }, reviewer: { select: { id: true, username: true, email: true } } } }); return { code: 200, message: 'Course approvals retrieved successfully', data: approvals, total: approvals.length, }; } catch (error) { logger.error('Failed to retrieve course approvals', { error }); throw error; } } static async addInstructorToCourse(addinstructorCourse: addinstructorCourse): Promise { try { const decoded = jwt.verify(addinstructorCourse.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseInstructor.create({ data: { course_id: addinstructorCourse.course_id, user_id: decoded.id, } }); return { code: 200, message: 'Instructor added to course successfully', }; } catch (error) { logger.error('Failed to add instructor to course', { error }); throw error; } } static async removeInstructorFromCourse(removeinstructorCourse: removeinstructorCourse): Promise { try { const decoded = jwt.verify(removeinstructorCourse.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseInstructor.delete({ where: { course_id_user_id: { course_id: removeinstructorCourse.course_id, user_id: removeinstructorCourse.user_id, }, } }); return { code: 200, message: 'Instructor removed from course successfully', }; } catch (error) { logger.error('Failed to remove instructor from course', { error }); throw error; } } static async listInstructorsOfCourse(listinstructorCourse: listinstructorCourse): Promise { try { const decoded = jwt.verify(listinstructorCourse.token, config.jwt.secret) as { id: number; type: string }; const courseInstructors = await prisma.courseInstructor.findMany({ where: { course_id: listinstructorCourse.course_id, }, include: { user: true, } }); return { code: 200, message: 'Instructors retrieved successfully', data: courseInstructors, }; } catch (error) { logger.error('Failed to retrieve instructors of course', { error }); throw error; } } static async setPrimaryInstructor(setprimaryCourseInstructor: setprimaryCourseInstructor): Promise { try { const decoded = jwt.verify(setprimaryCourseInstructor.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseInstructor.update({ where: { course_id_user_id: { course_id: setprimaryCourseInstructor.course_id, user_id: setprimaryCourseInstructor.user_id, }, }, data: { is_primary: true, } }); return { code: 200, message: 'Primary instructor set successfully', }; } catch (error) { logger.error('Failed to set primary instructor', { error }); throw error; } } static async validateCourseInstructor(token: string, courseId: number): Promise<{ user_id: number; is_primary: boolean }> { const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; const courseInstructor = await prisma.courseInstructor.findFirst({ where: { user_id: decoded.id, course_id: courseId } }); if (!courseInstructor) { throw new ForbiddenError('You are not an instructor of this course'); } else return { user_id: courseInstructor.user_id, is_primary: courseInstructor.is_primary }; } static async validateCourseStatus(courseId: number): Promise { const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { throw new NotFoundError('Course not found'); } if (course.status === 'APPROVED' || course.status === 'PENDING') { throw new ForbiddenError('Course is already approved Cannot Edit'); } } }