mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
124 lines
5.9 KiB
JavaScript
124 lines
5.9 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name : issue_certificates.cron.js
|
|
* Type : Cron Job
|
|
* Description : Issues certificates for users who passed a course assessment.
|
|
* Runs every hour on the hour and processes any
|
|
* pending_certificates row where issue_at <= NOW() and
|
|
* processed_at IS NULL.
|
|
*
|
|
* For each ready row it:
|
|
* 1. Grants the course_completed_<uuid> achievement (the key
|
|
* MyCertificates / Profile use to display certificate cards).
|
|
* 2. Persists the certificate record (cert_no/ref_no) via
|
|
* services/certificate-record.service.js, so course.certificate
|
|
* is populated immediately instead of only on first PDF download.
|
|
* 3. Sends a 'certificate_issued' UserNotification.
|
|
* 4. Marks the row processed_at = NOW() so it never fires again.
|
|
*
|
|
* Safety pattern: processed_at is set only after both step 1 and
|
|
* step 2 succeed. If the process restarts mid-run the row will be
|
|
* picked up again on the next tick — both DB writes are idempotent.
|
|
*
|
|
* Schedule : Every hour on the hour ("0 * * * *"). Registered by
|
|
* cron/client.cron.js.
|
|
*
|
|
* Author: Kenneth Obsequio (@lash0000)
|
|
* Date Created: Jun. 24, 2026
|
|
***********************************************************************************************************************************************************************/
|
|
'use strict';
|
|
|
|
const { Op } = require('sequelize');
|
|
const PendingCertificate = require('../../models/courses/pending_certificate.mdl');
|
|
const mdl_Achievements = require('../../models/users/achievements.mdl');
|
|
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
|
const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl');
|
|
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
|
const { ensureCertificateRecord } = require('../../services/certificate-record.service');
|
|
|
|
async function run() {
|
|
// Certificate/achievement issuance always happens — only the notification step is skippable.
|
|
const settings = await CronNotificationSetting.findOne({ where: { job_name: 'issueCertificates' } });
|
|
const notificationsEnabled = !settings || settings.enabled;
|
|
|
|
// ── 1. Fetch all rows ready to process ────────────────────────────────────
|
|
let rows;
|
|
try {
|
|
rows = await PendingCertificate.findAll({
|
|
where: {
|
|
issue_at: { [Op.lte]: new Date() },
|
|
processed_at: null,
|
|
},
|
|
raw: true,
|
|
});
|
|
} catch (err) {
|
|
console.error('[CRON][ISSUE CERTS] Failed to query pending_certificates:', err);
|
|
return;
|
|
}
|
|
|
|
if (rows.length === 0) return;
|
|
|
|
console.log(`[CRON][ISSUE CERTS] Processing ${rows.length} pending certificate(s).`);
|
|
|
|
for (const row of rows) {
|
|
const { pending_id, user_id, course_uuid, course_title } = row;
|
|
const achKey = `course_completed_${course_uuid}`;
|
|
|
|
try {
|
|
// ── 2. Grant course_completed_<uuid> achievement (idempotent) ──────
|
|
const existing = await mdl_Achievements.findOne({ where: { user_id, key: achKey } });
|
|
if (!existing) {
|
|
await mdl_Achievements.create({
|
|
user_id,
|
|
type: 'milestone',
|
|
key: achKey,
|
|
label: 'Certificate of Completion',
|
|
description: course_title ?? '',
|
|
granted_at: new Date(),
|
|
metadata: { courseTitle: course_title, courseUuid: course_uuid },
|
|
});
|
|
}
|
|
|
|
// ── 3. Persist the actual certificate record (cert_no/ref_no) so
|
|
// course.certificate is populated immediately, instead of only
|
|
// lazily on first PDF download ────────────────────────────────
|
|
await ensureCertificateRecord({ userId: user_id, courseId: row.course_id });
|
|
|
|
// ── 4. Send certificate_issued notification ─────────────────────────
|
|
if (notificationsEnabled) {
|
|
await UserNotification.create({
|
|
user_id,
|
|
...NOTIFICATION_REGISTRY.certificate_issued.build({
|
|
courseTitle: course_title ?? '',
|
|
courseUuid: course_uuid,
|
|
}),
|
|
});
|
|
}
|
|
|
|
// ── 5. Mark row processed ─────────────────────────────────────────
|
|
await PendingCertificate.update(
|
|
{ processed_at: new Date() },
|
|
{ where: { pending_id } }
|
|
);
|
|
|
|
console.log(`[CRON][ISSUE CERTS] Issued certificate for user ${user_id} / course ${course_uuid}.`);
|
|
} catch (err) {
|
|
// Log and continue — next tick will retry this row
|
|
if (err?.parent?.code !== '23505') {
|
|
console.error(`[CRON][ISSUE CERTS] Failed for pending_id ${pending_id}:`, err);
|
|
} else {
|
|
// Unique constraint: achievement already exists — still mark processed
|
|
await PendingCertificate.update(
|
|
{ processed_at: new Date() },
|
|
{ where: { pending_id } }
|
|
).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
name: 'issueCertificates',
|
|
schedule: '0 * * * *',
|
|
run,
|
|
};
|