Files
starr-philproperties/apps/api/controllers/client/certificate.controller.js
T

114 lines
5.5 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name: certificate.controller.js (client)
* Type of Program: Controller
* Description: Issues a PDF certificate for a completed course.
* A certificate is available only when the user has passed the course assessment.
* Certificate records are persisted (findOrCreate) so the same cert_no/ref_no is
* returned on every subsequent download.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 18, 2026
***********************************************************************************************************************************************************************/
'use strict';
const R = require('../../utils/response.util');
const mdl_Users = require('../../models/users/users.mdl');
const { generateCertificate } = require('../../services/certificate.service');
const { ensureCertificateRecord, formatInstructors } = require('../../services/certificate-record.service');
const { fmtDate } = require('../../utils/datetime.util');
const {
Course,
CourseAssessment,
CourseInstructor,
} = require('../../models/courses/courses.associations');
const notDeleted = { deletedAt: null };
// ─── GET /api/client/certificates/:courseUuid ──────────────────────────────────
exports.getCertificate = async (req, res) => {
try {
const { courseUuid } = req.params;
const user_id = req.user.user_id;
// ── 1. Resolve course ──────────────────────────────────────────────────────
const course = await Course.findOne({
where: { uuid: courseUuid, ...notDeleted },
attributes: ['course_id', 'title', 'course_code', 'duration_seconds'],
include: [
{
model: CourseAssessment,
as: 'assessment',
attributes: ['assessment_id'],
required: false,
},
{
model: CourseInstructor,
as: 'instructors',
attributes: ['display_name', 'order_index'],
required: false,
order: [['order_index', 'ASC']],
},
],
});
if (!course) return R.error(res, 'Course not found.', 404);
if (!course.assessment) {
return R.error(res, 'This course does not have an assessment — no certificate available.', 404);
}
// ── 2. Get user's name ──────────────────────────────────────────────────────
const user = await mdl_Users.findByPk(user_id, { attributes: ['personal_info'] });
const fullName = user?.personal_info?.name?.full_name?.trim() || 'Participant';
// ── 3. Resolve or create the certificate record ─────────────────────────────
// Shared with the hourly issuance cron (cron/jobs/issue_certificates.cron.js)
// so both write through the same cert_no/ref_no sequence.
const cert = await ensureCertificateRecord({ userId: user_id, courseId: course.course_id });
if (!cert) {
return R.error(res, 'Certificate not available — course assessment not passed yet.', 403);
}
// Always use live instructors from course_instructors table for the PDF,
// in case they changed since the certificate row was created.
const liveInstructors = formatInstructors(course.instructors ?? []);
// ── 4. Format issued date as MM/DD/YY HH:MM AM/PM ───────────────────────────
const issuedDate = new Date(cert.issued_at);
const dateStr = fmtDate(issuedDate);
// ── 5. Generate PDF ────────────────────────────────────────────────────────
const pdf = await generateCertificate({
name: fullName,
course: course.title,
date: dateStr,
cert_no: cert.cert_no,
ref_no: cert.ref_no,
instructors: liveInstructors,
length: cert.length_str ?? '',
});
// ── 6. Stream response ─────────────────────────────────────────────────────
const nameParts = fullName.trim().split(/\s+/);
const lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : nameParts[0];
const firstName = nameParts.length > 1 ? nameParts.slice(0, -1).join(' ') : '';
const safeTitle = course.title.replace(/[/\\:*?"<>|]/g, '').trim();
const filename = `${lastName},${firstName}_${safeTitle}_${cert.cert_no}.pdf`;
const asciiName = filename.replace(/[^\x20-\x7E]/g, '_').replace(/"/g, '_');
const encodedName = encodeURIComponent(filename);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${asciiName}"; filename*=UTF-8''${encodedName}`,
'Content-Length': pdf.length,
});
return res.send(pdf);
} catch (err) {
console.error('[CLIENT][CERTIFICATE]', err);
return R.error(res, 'Could not generate certificate.', 500);
}
};