mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user