// /components/certificates/certificateService.js const path = require('path'); const Certificate = require('./certificateModel'); const User = require('../users/userModel'); const AppError = require('../../utils/AppError'); const config = require('../../config/config'); const { commitTempFile, generatePresignedUrl, deleteFromBucket } = require('../../utils/s3Client'); const eventEmitter = require('../../events/eventEmitter'); const EVENT_NAMES = require('../../constants/eventNames'); const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination'); const BUCKET = 'certificates'; const withAccessUrl = async (cert) => { const item = typeof cert.toObject === 'function' ? cert.toObject() : { ...cert }; item.fileUrl = item.fileUrl || null; try { item.signedUrl = await generatePresignedUrl(item.fileKey, item.bucket || config.S3_CERTIFICATES_BUCKET); } catch { item.signedUrl = null; } item.presignedUrl = item.signedUrl; item.url = item.fileUrl || item.signedUrl; return item; }; const createCertificate = async (data) => { const user = await User.findById(data.user); if (!user) throw new AppError('USER_NOT_FOUND'); if (!data.tempFileName) throw new AppError('FILE_REQUIRED'); const safeName = path.basename(data.tempFileName); const targetKey = `cert-${Date.now()}-${safeName}`; const { fileKey, bucket, fileUrl } = await commitTempFile(data.tempFileName, targetKey, BUCKET); const fileName = data.originalName || data.fileName || safeName; const certificate = await Certificate.create({ user: data.user, course: data.course || null, title: data.title || fileName, issuer: data.issuer || '', fileKey, fileName, originalName: fileName, fileUrl: fileUrl || null, bucket, mimeType: data.mimeType || '', isOfficial: data.isOfficial || false }); user.certificates.push(certificate._id); await user.save(); eventEmitter.emit(EVENT_NAMES.CERTIFICATE_ISSUED, { certificateId: certificate._id, userId: user._id }); return withAccessUrl(certificate); }; const getCertificateById = async (id) => { const certificate = await Certificate.findById(id) .populate('user', 'name username nationalIdCode') .populate('course', 'title type'); if (!certificate) throw new AppError('CERTIFICATE_NOT_FOUND'); return withAccessUrl(certificate); }; const getCertificatesByUser = async (userId) => { const certificates = await Certificate.find({ user: userId }) .populate('course', 'title type') .sort({ createdAt: -1 }); return Promise.all(certificates.map(withAccessUrl)); }; const getAllCertificates = async (queryParams) => { const { page, limit, skip, sort } = parsePaginationAndSort(queryParams); const filter = buildFilterQuery(queryParams, ['title', 'issuer']); if (queryParams.userId) filter.user = queryParams.userId; const [certificates, totalCount] = await Promise.all([ Certificate.find(filter) .populate('user', 'name username') .populate('course', 'title') .sort(sort) .skip(skip) .limit(limit), Certificate.countDocuments(filter) ]); const data = await Promise.all(certificates.map(withAccessUrl)); return { data, meta: calculateMeta(totalCount, page, limit) }; }; const updateCertificate = async (id, updateData) => { const certificate = await Certificate.findById(id); if (!certificate) throw new AppError('CERTIFICATE_NOT_FOUND'); if (updateData.tempFileName) { const safeName = path.basename(updateData.tempFileName); const targetKey = `cert-${Date.now()}-${safeName}`; const { fileKey, bucket, fileUrl } = await commitTempFile(updateData.tempFileName, targetKey, BUCKET); await deleteFromBucket(certificate.fileKey, certificate.bucket || config.S3_CERTIFICATES_BUCKET); certificate.fileKey = fileKey; certificate.bucket = bucket; certificate.fileUrl = fileUrl || null; const fileName = updateData.originalName || updateData.fileName; if (fileName) { certificate.fileName = fileName; certificate.originalName = fileName; } if (updateData.mimeType) certificate.mimeType = updateData.mimeType; } if (updateData.title) certificate.title = updateData.title; if (updateData.issuer !== undefined) certificate.issuer = updateData.issuer; if (updateData.isOfficial !== undefined) certificate.isOfficial = updateData.isOfficial; await certificate.save(); return withAccessUrl(certificate); }; const deleteCertificate = async (id) => { const certificate = await Certificate.findById(id); if (!certificate) throw new AppError('CERTIFICATE_NOT_FOUND'); await deleteFromBucket(certificate.fileKey, certificate.bucket || config.S3_CERTIFICATES_BUCKET); await User.findByIdAndUpdate(certificate.user, { $pull: { certificates: certificate._id } }); await Certificate.findByIdAndDelete(id); return null; }; const searchCertificates = async (queryParams) => getAllCertificates(queryParams); const uploadUserCertificate = async (userId, data) => { return createCertificate({ ...data, user: userId, issuer: data.issuer || 'Self-Uploaded', isOfficial: false }); }; const getMyCertificates = async (userId, queryParams) => { const filter = { user: userId, ...buildFilterQuery(queryParams) }; const { page, limit, skip, sort } = parsePaginationAndSort(queryParams); const [certificates, totalCount] = await Promise.all([ Certificate.find(filter) .populate('course', 'title type') .sort(sort) .skip(skip) .limit(limit), Certificate.countDocuments(filter) ]); const data = await Promise.all(certificates.map(withAccessUrl)); return { data, meta: calculateMeta(totalCount, page, limit) }; }; module.exports = { createCertificate, getCertificateById, getCertificatesByUser, getAllCertificates, updateCertificate, deleteCertificate, searchCertificates, uploadUserCertificate, getMyCertificates };