/*********************************************************************************************************************************************************************** * File Name : task_due_soon.cron.js * Type : Cron Job * Description : Emits a learner-facing "task_reminder" UserNotification for * each user who has NOT yet completed a task whose deadline * falls ~24h from now. Unlike task_overdue.cron.js (a single * admin-facing status flip), completion here is per-user, so * each candidate task's assigned-group members are checked * individually via checkTaskCompletion before notifying. * * "Falls ~24h from now" = deadline between (now + 23h) and * (now + 24h), a 1-hour sliding window — since this runs * hourly, each task's deadline crosses that window exactly * once, giving a single reminder ~24h before it's due * without needing a separate "already notified" table. * * Schedule : Every hour, 10 minutes past ("10 * * * *"). Registered by * cron/client.cron.js. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jul. 9, 2026 ***********************************************************************************************************************************************************************/ const { Op, QueryTypes } = require('sequelize'); const sequelize = require('../../config/db.config'); const { Task } = require('../../models/task/task.mdl'); const UserNotification = require('../../models/notifications/user_notification.mdl'); const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl'); const { renderNotification } = require('../../services/notificationTemplate.service'); const { checkTaskCompletion } = require('../../controllers/client/task.controller'); const WINDOW_START_MS = 23 * 60 * 60 * 1000; const WINDOW_END_MS = 24 * 60 * 60 * 1000; async function run() { const settings = await CronNotificationSetting.findOne({ where: { job_name: 'taskDueSoon' } }); if (settings && !settings.enabled) return; const now = Date.now(); let dueSoonTasks; try { dueSoonTasks = await Task.findAll({ attributes: ['task_id', 'task_list_id', 'name', 'deadline'], where: { deadline: { [Op.gte]: new Date(now + WINDOW_START_MS), [Op.lt]: new Date(now + WINDOW_END_MS), }, status: { [Op.notIn]: ['completed', 'overdue'] }, }, raw: true, }); } catch (err) { console.error('[CRON][TASK DUE SOON] Failed to query upcoming deadlines:', err); return; } if (!dueSoonTasks.length) return; console.log(`[CRON][TASK DUE SOON] ${dueSoonTasks.length} task(s) due in ~24h — resolving affected users.`); try { const taskListIds = [...new Set(dueSoonTasks.map((t) => t.task_list_id))]; const memberRows = await sequelize.query( `SELECT DISTINCT tlg.task_list_id, ugm.user_id FROM task_list_groups tlg JOIN user_group_members ugm ON ugm.group_id = tlg.group_id AND ugm."deletedAt" IS NULL WHERE tlg.task_list_id IN (:taskListIds)`, { replacements: { taskListIds }, type: QueryTypes.SELECT } ); const usersByTaskList = new Map(); for (const row of memberRows) { const list = usersByTaskList.get(row.task_list_id) ?? []; list.push(row.user_id); usersByTaskList.set(row.task_list_id, list); } const now2 = new Date(); let notifiedCount = 0; for (const task of dueSoonTasks) { const candidateUserIds = usersByTaskList.get(task.task_list_id) ?? []; if (!candidateUserIds.length) continue; const incompleteUserIds = []; for (const userId of candidateUserIds) { const done = await checkTaskCompletion(userId, task.task_id); if (!done) incompleteUserIds.push(userId); } if (!incompleteUserIds.length) continue; const notify = await renderNotification({ type: 'task_reminder', data: { taskName: task.name, deadline: task.deadline, } }); await UserNotification.bulkCreate( incompleteUserIds.map((user_id) => ({ user_id, ...notify, seen: false, createdAt: now2, updatedAt: now2 })), { validate: false } ); notifiedCount += incompleteUserIds.length; } console.log(`[CRON][TASK DUE SOON] Sent ${notifiedCount} reminder(s) across ${dueSoonTasks.length} task(s).`); } catch (err) { console.error('[CRON][TASK DUE SOON] Failed to emit reminders:', err); } } module.exports = { name: 'taskDueSoon', schedule: '10 * * * *', run, };