// /components/courses/courseService.js const Course = require('./courseModel'); const Professor = require('../professors/professorModel'); const AppError = require('../../utils/AppError'); const eventEmitter = require('../../events/eventEmitter'); const EVENT_NAMES = require('../../constants/eventNames'); const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination'); const normalizeHighlights = (value) => { if (!Array.isArray(value)) return undefined; return value.map((item) => String(item).trim()).filter(Boolean); }; const createCourse = async (data, actorId = null) => { if (data.professor) { const professor = await Professor.findById(data.professor); if (!professor) { throw new AppError('PROFESSOR_NOT_FOUND'); } } const payload = { ...data, highlights: normalizeHighlights(data.highlights) ?? data.highlights }; const course = await Course.create(payload); if (data.professor) { await Professor.findByIdAndUpdate(data.professor, { $addToSet: { courses: course._id } }); } eventEmitter.emit(EVENT_NAMES.COURSE_CREATED, { courseId: course._id, title: course.title, actorId }); return course; }; const getCourseById = async (id, { publicOnly = false } = {}) => { const filter = { _id: id }; if (publicOnly) filter.showOnFrontend = { $ne: false }; const course = await Course.findOne(filter).populate('professor', 'name surname title expertise email phoneNumber'); if (!course) { throw new AppError('COURSE_NOT_FOUND'); } return course; }; const getAllCourses = async (queryParams, { publicOnly = false } = {}) => { const { page, limit, skip, sort } = parsePaginationAndSort(queryParams); const filter = buildFilterQuery(queryParams, ['title', 'description'], [ 'page', 'limit', 'sortBy', 'sortOrder', 'q', 'lang', 'sort' ]); if (publicOnly) { // Include legacy docs that predate the field (treat missing as visible) filter.showOnFrontend = { $ne: false }; } // Support legacy ?sort=-createdAt style from frontend let finalSort = sort; if (queryParams.sort && typeof queryParams.sort === 'string') { const raw = queryParams.sort.trim(); if (raw.startsWith('-')) { finalSort = { [raw.slice(1)]: -1 }; } else { finalSort = { [raw]: 1 }; } } const [courses, totalCount] = await Promise.all([ Course.find(filter).populate('professor', 'name surname').sort(finalSort).skip(skip).limit(limit), Course.countDocuments(filter) ]); const meta = calculateMeta(totalCount, page, limit); return { data: courses, meta }; }; const updateCourse = async (id, updateData, actorId = null) => { const course = await Course.findById(id); if (!course) { throw new AppError('COURSE_NOT_FOUND'); } if (updateData.professor && updateData.professor !== String(course.professor)) { const professor = await Professor.findById(updateData.professor); if (!professor) throw new AppError('PROFESSOR_NOT_FOUND'); if (course.professor) { await Professor.findByIdAndUpdate(course.professor, { $pull: { courses: course._id } }); } await Professor.findByIdAndUpdate(updateData.professor, { $addToSet: { courses: course._id } }); } if (updateData.price !== undefined && updateData.price !== course.price) { eventEmitter.emit(EVENT_NAMES.COURSE_PRICE_CHANGED, { courseId: course._id, oldPrice: course.price, newPrice: updateData.price, actorId }); } if (updateData.highlights !== undefined) { updateData.highlights = normalizeHighlights(updateData.highlights) || []; } Object.assign(course, updateData); await course.save(); return course; }; const deleteCourse = async (id) => { const course = await Course.findById(id); if (!course) { throw new AppError('COURSE_NOT_FOUND'); } if (course.professor) { await Professor.findByIdAndUpdate(course.professor, { $pull: { courses: course._id } }); } await Course.findByIdAndDelete(id); return null; }; const searchCourses = async (queryParams) => { return getAllCourses(queryParams); }; module.exports = { createCourse, getCourseById, getAllCourses, updateCourse, deleteCourse, searchCourses };