198 lines
6.8 KiB
JavaScript
198 lines
6.8 KiB
JavaScript
// /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 { notifyAction } = require('../../utils/actionNotify');
|
|
const { sendCertificateIssuedSms } = require('../../utils/senders/smsMessages');
|
|
const Course = require('../courses/courseModel');
|
|
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 });
|
|
|
|
try {
|
|
let courseName = '';
|
|
if (certificate.course) {
|
|
const course = await Course.findById(certificate.course).select('title').lean();
|
|
courseName = course?.title || '';
|
|
}
|
|
await notifyAction({
|
|
actionKey: 'certificateIssued',
|
|
userId: user._id,
|
|
phoneNumber: user.phoneNumber,
|
|
email: user.email,
|
|
subject: 'صدور گواهینامه',
|
|
body: `گواهینامه «${certificate.title}» با کد ${certificate.uniqueCode || ''} صادر شد.`,
|
|
smsHandler: () => sendCertificateIssuedSms(user.phoneNumber, {
|
|
fullName: user.name || '',
|
|
certificateTitle: certificate.title,
|
|
certificateCode: certificate.uniqueCode || '',
|
|
courseName
|
|
}, user._id)
|
|
});
|
|
} catch {
|
|
// Notification failure should not block certificate creation
|
|
}
|
|
|
|
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
|
|
};
|