import { prisma } from '../config/database'; import { config } from '../config'; import { logger } from '../config/logger'; import { NotFoundError, ValidationError } from '../middleware/errorHandler'; import jwt from 'jsonwebtoken'; import { getPresignedUrl } from '../config/minio'; import { ListApprovedCoursesResponse, GetCourseByIdResponse, ToggleRecommendedResponse, RecommendedCourseData } from '../types/RecommendedCourses.types'; import { auditService } from './audit.service'; import { AuditAction } from '@prisma/client'; export class RecommendedCoursesService { /** * List all approved courses (for admin to manage recommendations) */ static async listApprovedCourses(token: string): Promise { try { const courses = await prisma.course.findMany({ where: { status: 'APPROVED' }, orderBy: [ { is_recommended: 'desc' }, { updated_at: 'desc' } ], 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: { include: { lessons: true } } } }); const data = await Promise.all(courses.map(async (course) => { 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 { id: course.id, title: course.title as { th: string; en: string }, slug: course.slug, description: course.description as { th: string; en: string }, thumbnail_url: thumbnail_presigned_url, price: Number(course.price), is_free: course.is_free, have_certificate: course.have_certificate, is_recommended: course.is_recommended, status: course.status, created_at: course.created_at, updated_at: course.updated_at, creator: course.creator, category: course.category ? { id: course.category.id, name: course.category.name as { th: string; en: string } } : null, 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) } as RecommendedCourseData; })); return { code: 200, message: 'Approved courses retrieved successfully', data, total: data.length }; } catch (error) { logger.error('Failed to list approved courses', { error }); const decoded = jwt.decode(token) as { id: number } | null; if (decoded?.id) { await auditService.logSync({ userId: decoded.id, action: AuditAction.ERROR, entityType: 'RecommendedCourses', entityId: 0, metadata: { operation: 'list_approved_courses', error: error instanceof Error ? error.message : String(error) } }); } throw error; } } /** * Get course by ID (for admin to view details) */ static async getCourseById(token: string, 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: { include: { lessons: true } } } }); if (!course) { throw new NotFoundError('Course not found'); } if (course.status !== 'APPROVED') { throw new ValidationError('Course is not approved'); } // 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}`); } } const data: RecommendedCourseData = { id: course.id, title: course.title as { th: string; en: string }, slug: course.slug, description: course.description as { th: string; en: string }, thumbnail_url: thumbnail_presigned_url, price: Number(course.price), is_free: course.is_free, have_certificate: course.have_certificate, is_recommended: course.is_recommended, status: course.status, created_at: course.created_at, updated_at: course.updated_at, creator: course.creator, category: course.category ? { id: course.category.id, name: course.category.name as { th: string; en: string } } : null, 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) }; return { code: 200, message: 'Course retrieved successfully', data }; } catch (error) { logger.error('Failed to get course by ID', { error }); const decoded = jwt.decode(token) as { id: number } | null; if (decoded?.id) { await auditService.logSync({ userId: decoded.id, action: AuditAction.ERROR, entityType: 'RecommendedCourses', entityId: 0, metadata: { operation: 'get_course_by_id', error: error instanceof Error ? error.message : String(error) } }); } throw error; } } /** * Toggle course recommendation status */ static async toggleRecommended( token: string, courseId: number, isRecommended: boolean ): 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 !== 'APPROVED') { throw new ValidationError('Only approved courses can be recommended'); } const updatedCourse = await prisma.course.update({ where: { id: courseId }, data: { is_recommended: isRecommended } }); // Audit log await auditService.logSync({ userId: decoded.id, action: AuditAction.UPDATE, entityType: 'Course', entityId: courseId, oldValue: { is_recommended: course.is_recommended }, newValue: { is_recommended: isRecommended }, metadata: { action: 'toggle_recommended' } }); return { code: 200, message: `Course ${isRecommended ? 'marked as recommended' : 'unmarked as recommended'} successfully`, data: { id: updatedCourse.id, is_recommended: updatedCourse.is_recommended } }; } catch (error) { logger.error('Failed to toggle recommended status', { error }); const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'RecommendedCourses', entityId: courseId, metadata: { operation: 'toggle_recommended', error: error instanceof Error ? error.message : String(error) } }); throw error; } } }