/*********************************************************************************************************************************************************************** * File Name : task_overdue.cron.js * Type : Cron Job * Description : Flips Task.status to 'overdue' once its deadline has passed, * provided it isn't already 'completed' or 'overdue'. Purely * an admin-facing lifecycle label — does NOT touch * TaskCompletion/TaskLinkVisit/TaskProgress, does NOT affect * per-user completion signals or client-side Ongoing/Done/ * Overdue bucketing, and does NOT block late submissions. * * Safety pattern: * - Task.update is the primary operation and must always succeed. * - AdminNotification.create is secondary — wrapped in its own * try/catch so a notification failure never rolls back or * suppresses the status flip. If it fails once, the next * hourly run will insert its own summary for that batch. * * Schedule : Every hour, on the hour ("0 * * * *"). Registered by * cron/admin.cron.js, not scheduled here directly. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 17, 2026 * Modified : Jun. 19, 2026 ***********************************************************************************************************************************************************************/ const { Op } = require('sequelize'); const { Task } = require('../../models/task/task.mdl'); const AdminNotification = require('../../models/notifications/admin_notification.mdl'); const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl'); const { renderNotification } = require('../../services/notificationTemplate.service'); // ─── The actual sweep ──────────────────────────────────────────────────────── async function run() { // ── 1. Primary: flip task statuses ─────────────────────────────────────── let affectedCount = 0; try { [affectedCount] = await Task.update( { status: 'overdue' }, { where: { deadline: { [Op.lt]: new Date() }, status: { [Op.notIn]: ['completed', 'overdue'] }, }, } ); } catch (err) { console.error('[CRON][TASK OVERDUE] Failed to update task statuses:', err); return; } if (affectedCount === 0) return; console.log(`[CRON][TASK OVERDUE] Marked ${affectedCount} task(s) as overdue.`); // ── 2. Secondary: admin notification — isolated, never blocks step 1 ───── // Skippable via /admin/notifications/settings — the status flip above always happens either way. try { const settings = await CronNotificationSetting.findOne({ where: { job_name: 'taskOverdue' } }); if (settings && !settings.enabled) return; await AdminNotification.create( await renderNotification({ type: 'task_overdue', data: { count: affectedCount } }) ); } catch (err) { console.error('[CRON][TASK OVERDUE] Failed to insert admin notification:', err); } } module.exports = { name: 'taskOverdue', schedule: '0 * * * *', run, };