mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
108 lines
4.1 KiB
JavaScript
108 lines
4.1 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name : certificate-record.service.js
|
|
* Type : Service
|
|
* Description : Resolves or lazily creates the persisted `certificates` row for a
|
|
* user/course pair (cert_no/ref_no assignment, instructor snapshot).
|
|
* Shared by the PDF download endpoint (certificate.controller.js) and
|
|
* the hourly issuance cron (cron/jobs/issue_certificates.cron.js) so
|
|
* both write through the same cert_no/ref_no sequence.
|
|
*
|
|
* Author: Kenneth Obsequio (@lash0000)
|
|
* Date Created: Jul. 2, 2026
|
|
***********************************************************************************************************************************************************************/
|
|
'use strict';
|
|
|
|
const { formatDuration } = require('../utils/duration.util');
|
|
|
|
const {
|
|
Course,
|
|
CourseAssessment,
|
|
QuizAttempt,
|
|
Certificate,
|
|
CourseInstructor,
|
|
} = require('../models/courses/courses.associations');
|
|
|
|
function formatInstructors(rows) {
|
|
const names = rows.map(r => r.display_name);
|
|
if (names.length === 0) return '';
|
|
if (names.length === 1) return names[0];
|
|
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
return `${names[0]}, ${names[1]} and et. al`;
|
|
}
|
|
|
|
// cert_no format: YYYYMM-{userId:6}-{userCertSeq:5}
|
|
async function buildCertNo(userId) {
|
|
const count = await Certificate.count({ where: { user_id: userId } });
|
|
const seq = String(count + 1).padStart(5, '0');
|
|
const uid = String(userId).padStart(6, '0');
|
|
const now = new Date();
|
|
const YYYYMM = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
return `${YYYYMM}-${uid}-${seq}`;
|
|
}
|
|
|
|
// ref_no format: PP-YYYYMM-{globalSeq:5} (unique across all certs)
|
|
async function buildRefNo() {
|
|
const count = await Certificate.count();
|
|
const seq = String(count + 1).padStart(5, '0');
|
|
const now = new Date();
|
|
const YYYYMM = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
return `PP-${YYYYMM}-${seq}`;
|
|
}
|
|
|
|
/**
|
|
* Resolves (or creates) the Certificate row for a user/course. Idempotent —
|
|
* safe to call from both the cron job and the on-demand download endpoint.
|
|
* Returns null if the course has no assessment or the user hasn't passed it.
|
|
*/
|
|
async function ensureCertificateRecord({ userId, courseId }) {
|
|
const course = await Course.findOne({
|
|
where: { course_id: courseId },
|
|
attributes: ['course_id', 'title', '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 || !course.assessment) return null;
|
|
|
|
const passedAttempt = await QuizAttempt.findOne({
|
|
where: {
|
|
user_id: userId,
|
|
assessment_id: course.assessment.assessment_id,
|
|
passed: true,
|
|
},
|
|
order: [['createdAt', 'DESC']],
|
|
attributes: ['score', 'createdAt'],
|
|
});
|
|
if (!passedAttempt) return null;
|
|
|
|
const liveInstructors = formatInstructors(course.instructors ?? []);
|
|
|
|
// CockroachDB does not support findOrCreate (uses temp PL/pgSQL functions).
|
|
let cert = await Certificate.findOne({ where: { user_id: userId, course_id: courseId } });
|
|
if (!cert) {
|
|
cert = await Certificate.create({
|
|
user_id: userId,
|
|
course_id: courseId,
|
|
cert_no: await buildCertNo(userId),
|
|
ref_no: await buildRefNo(),
|
|
instructors: liveInstructors,
|
|
score: passedAttempt.score ?? null,
|
|
length_str: formatDuration(course.duration_seconds),
|
|
issued_at: passedAttempt.createdAt,
|
|
});
|
|
} else if (liveInstructors !== (cert.instructors ?? '')) {
|
|
await cert.update({ instructors: liveInstructors });
|
|
}
|
|
|
|
return cert;
|
|
}
|
|
|
|
module.exports = { ensureCertificateRecord, formatInstructors, buildCertNo, buildRefNo };
|