/*********************************************************************************************************************************************************************** * 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); } };