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,107 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user