import { prisma } from '../config/database'; import { config } from '../config'; import { logger } from '../config/logger'; import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; import jwt from 'jsonwebtoken'; import { ListPendingCoursesResponse, GetCourseDetailForAdminResponse, ApproveCourseResponse, RejectCourseResponse, } from '../types/AdminCourseApproval.types'; export class AdminCourseApprovalService { /** * Get all pending courses for admin review */ static async listPendingCourses(): Promise { try { const courses = await prisma.course.findMany({ where: { status: 'PENDING' }, orderBy: { updated_at: 'desc' }, include: { creator: { select: { id: true, username: true, email: true } }, instructors: { include: { user: { select: { id: true, username: true, email: true } } } }, chapters: { include: { lessons: true } }, courseApprovals: { where: { action: 'SUBMITTED' }, orderBy: { created_at: 'desc' }, take: 1, include: { submitter: { select: { id: true, username: true, email: true } } } } } }); const data = courses.map(course => ({ id: course.id, title: course.title as { th: string; en: string }, slug: course.slug, description: course.description as { th: string; en: string }, thumbnail_url: course.thumbnail_url, status: course.status, created_at: course.created_at, updated_at: course.updated_at, created_by: course.created_by, creator: course.creator, instructors: course.instructors.map(i => ({ user_id: i.user_id, is_primary: i.is_primary, user: i.user })), chapters_count: course.chapters.length, lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0), latest_submission: course.courseApprovals[0] ? { id: course.courseApprovals[0].id, submitted_by: course.courseApprovals[0].submitted_by, created_at: course.courseApprovals[0].created_at, submitter: course.courseApprovals[0].submitter } : null })); return { code: 200, message: 'Pending courses retrieved successfully', data, total: data.length }; } catch (error) { logger.error('Failed to list pending courses', { error }); throw error; } } /** * Get course details for admin review */ static async getCourseDetail(courseId: number): Promise { try { const course = await prisma.course.findUnique({ where: { id: courseId }, include: { category: { select: { id: true, name: true } }, creator: { select: { id: true, username: true, email: true } }, instructors: { include: { user: { select: { id: true, username: true, email: true } } } }, chapters: { orderBy: { sort_order: 'asc' }, include: { lessons: { orderBy: { sort_order: 'asc' }, select: { id: true, title: true, type: true, sort_order: true, is_published: true } } } }, courseApprovals: { orderBy: { created_at: 'desc' }, include: { submitter: { select: { id: true, username: true, email: true } }, reviewer: { select: { id: true, username: true, email: true } } } } } }); if (!course) { throw new NotFoundError('Course not found'); } return { code: 200, message: 'Course details retrieved successfully', data: { id: course.id, title: course.title as { th: string; en: string }, slug: course.slug, description: course.description as { th: string; en: string }, thumbnail_url: course.thumbnail_url, price: Number(course.price), is_free: course.is_free, have_certificate: course.have_certificate, status: course.status, created_at: course.created_at, updated_at: course.updated_at, category: course.category ? { id: course.category.id, name: course.category.name as { th: string; en: string } } : null, creator: course.creator, instructors: course.instructors.map(i => ({ user_id: i.user_id, is_primary: i.is_primary, user: i.user })), chapters: course.chapters.map(ch => ({ id: ch.id, title: ch.title as { th: string; en: string }, sort_order: ch.sort_order, is_published: ch.is_published, lessons: ch.lessons.map(l => ({ id: l.id, title: l.title as { th: string; en: string }, type: l.type, sort_order: l.sort_order, is_published: l.is_published })) })), approval_history: course.courseApprovals.map(a => ({ id: a.id, action: a.action, comment: a.comment, created_at: a.created_at, submitter: a.submitter, reviewer: a.reviewer })) } }; } catch (error) { logger.error('Failed to get course detail', { error }); throw error; } } /** * Approve a course */ static async approveCourse(token: string, courseId: number, comment?: string): Promise { try { const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { throw new NotFoundError('Course not found'); } if (course.status !== 'PENDING') { throw new ValidationError('Course is not pending for approval'); } await prisma.$transaction([ // Update course status prisma.course.update({ where: { id: courseId }, data: { status: 'APPROVED' } }), // Create approval record prisma.courseApproval.create({ data: { course_id: courseId, submitted_by: course.created_by, reviewed_by: decoded.id, action: 'APPROVED', previous_status: course.status, new_status: 'APPROVED', comment: comment || null } }) ]); return { code: 200, message: 'Course approved successfully' }; } catch (error) { logger.error('Failed to approve course', { error }); throw error; } } /** * Reject a course */ static async rejectCourse(token: string, courseId: number, comment: string): Promise { try { const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { throw new NotFoundError('Course not found'); } if (course.status !== 'PENDING') { throw new ValidationError('Course is not pending for approval'); } if (!comment || comment.trim() === '') { throw new ValidationError('Comment is required when rejecting a course'); } await prisma.$transaction([ // Update course status back to DRAFT prisma.course.update({ where: { id: courseId }, data: { status: 'DRAFT' } }), // Create rejection record prisma.courseApproval.create({ data: { course_id: courseId, submitted_by: course.created_by, reviewed_by: decoded.id, action: 'REJECTED', previous_status: course.status, new_status: 'DRAFT', comment: comment } }) ]); return { code: 200, message: 'Course rejected successfully' }; } catch (error) { logger.error('Failed to reject course', { error }); throw error; } } }