/*********************************************************************************************************************************************************************** * File Name : task_overdue.cron.js * Type : Cron Job * Description : Flips Task.status to a configurable target status — 'overdue' * (default before Jul 2026) or 'completed' (current default) — * once its deadline has passed, provided it isn't already * 'completed' or 'overdue'. The target status is an admin- * configurable setting (cron_notification_settings.target_status * for job_name 'taskOverdue'; NULL is treated as 'completed'). * 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. * * Every task this job touches also gets auto_marked_at set to * the current time — this is the ONLY writer of that column, * so downstream consumers (e.g. cron/jobs/user_notifications.cron.js) * can distinguish "the system just did this" from a user's own * legitimate completion. A task already sitting in 'overdue' or * 'completed' is never reclaimed by this sweep even if the * target status changes later — this job only ever moves tasks * OUT of 'pending'/'in_progress', never between the two terminal * states. * * 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 { NOTIFICATION_REGISTRY } = require('../../data/notifications.data'); // ─── The actual sweep ──────────────────────────────────────────────────────── async function run() { // ── 0. Load configured target status (defaults to 'completed') ─────────── let settings = null; let targetStatus = 'completed'; try { settings = await CronNotificationSetting.findOne({ where: { job_name: 'taskOverdue' } }); if (settings?.target_status === 'overdue' || settings?.target_status === 'completed') { targetStatus = settings.target_status; } } catch (err) { console.error('[CRON][TASK OVERDUE] Failed to load target_status setting, defaulting to "completed":', err); } // ── 1. Primary: flip task statuses ─────────────────────────────────────── let affectedCount = 0; try { [affectedCount] = await Task.update( { status: targetStatus, auto_marked_at: new Date() }, { 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 ${targetStatus}.`); // ── 2. Secondary: admin notification — isolated, never blocks step 1 ───── // Skippable via /admin/notifications/settings — the status flip above always happens either way. try { if (settings && !settings.enabled) return; await AdminNotification.create( NOTIFICATION_REGISTRY.task_overdue.build({ count: affectedCount, targetStatus }) ); } catch (err) { console.error('[CRON][TASK OVERDUE] Failed to insert admin notification:', err); } } module.exports = { name: 'taskOverdue', schedule: '0 * * * *', run, };