elearning/Backend/src/services/RecommendedCourses.service.ts

272 lines
10 KiB
TypeScript

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<ListApprovedCoursesResponse> {
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<GetCourseByIdResponse> {
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<ToggleRecommendedResponse> {
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;
}
}
}