Files
starr-philproperties/cron/jobs/user_notifications.cron.js
T
kennethobsequio 941590f51a implement tasks
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-07-17 17:28:11 +08:00

138 lines
6.6 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name : user_task_overdue_notify.cron.js
* Type : Cron Job
* Description : Emits a UserNotification for every user who belongs to a group
* assigned to a task list that contains a task the admin
* taskOverdue cron JUST auto-flipped in the last hour — to
* either 'overdue' or 'completed', depending on that job's
* configured target_status.
*
* Runs 5 minutes after the admin taskOverdue cron (which fires at
* the top of each hour) so the status flips are already committed
* before this job queries them.
*
* "Just auto-flipped" = auto_marked_at is within the last 65
* minutes (1-hour window + 5-min drift buffer). auto_marked_at
* is written ONLY by cron/jobs/task_overdue.cron.js, never by a
* user's own completion flow, so this can't misfire on a task a
* user legitimately just completed themselves.
*
* Rows are grouped by status: 'overdue' tasks get the existing
* "Tasks Overdue" notification, 'completed' tasks get a
* separate "Tasks Auto-Completed" notification. In practice a
* single run is homogeneous (target_status is one job-wide
* setting), but the grouping keeps this correct even if the
* setting changed mid-window.
*
* When a status group contains exactly one task, its taskId/
* taskListId are included in the notification data (plus each
* recipient's own group_id) so the client can deep-link
* straight to that task. A multi-task batch can't pick just
* one task to link to, so it falls back to no link.
*
* Schedule : 5 minutes past every hour ("5 * * * *"). Registered by
* cron/client.cron.js.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 19, 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 { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
const WINDOW_MS = 65 * 60 * 1000; // 65-minute lookback
async function run() {
// Entire job exists to emit this notification — skippable via /admin/notifications/settings.
const settings = await CronNotificationSetting.findOne({ where: { job_name: 'userNotifications' } });
if (settings && !settings.enabled) return;
// ── 1. Find tasks the admin cron JUST auto-flipped in the last 65 minutes ─
let recentlyAutoMarked;
try {
recentlyAutoMarked = await Task.findAll({
attributes: ['task_id', 'task_list_id', 'name', 'status'],
where: {
status: { [Op.in]: ['overdue', 'completed'] },
auto_marked_at: { [Op.gte]: new Date(Date.now() - WINDOW_MS) },
},
raw: true,
});
} catch (err) {
console.error('[CRON][USER NOTIFY] Failed to query recently auto-marked tasks:', err);
return;
}
if (recentlyAutoMarked.length === 0) return;
console.log(`[CRON][USER NOTIFY] ${recentlyAutoMarked.length} recently auto-marked task(s) — resolving affected users.`);
// ── 2. Resolve affected users via task_list_groups → user_group_members ───
try {
const byStatus = {
overdue: recentlyAutoMarked.filter(t => t.status === 'overdue'),
completed: recentlyAutoMarked.filter(t => t.status === 'completed'),
};
const now = new Date();
for (const [status, tasks] of Object.entries(byStatus)) {
if (tasks.length === 0) continue;
const taskListIds = [...new Set(tasks.map(t => t.task_list_id))];
// DISTINCT ON picks one group per user (deterministic — lowest group_id)
// so each affected user gets a single notification even if they belong
// to more than one group assigned to these task lists.
const affectedUsers = await sequelize.query(
`SELECT DISTINCT ON (ugm.user_id) ugm.user_id, ugm.group_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)
ORDER BY ugm.user_id, ugm.group_id`,
{ replacements: { taskListIds }, type: QueryTypes.SELECT }
);
if (affectedUsers.length === 0) continue;
const count = tasks.length;
const registryKey = status === 'completed' ? 'user_task_auto_completed' : 'user_task_overdue';
// A single-task batch can deep-link straight to that task; a multi-task
// batch can't pick just one, so it falls back to the task-list link.
const single = count === 1 ? tasks[0] : null;
const notify = NOTIFICATION_REGISTRY[registryKey].build({
count,
task_list_ids: taskListIds,
taskId: single?.task_id ?? null,
taskListId: single?.task_list_id ?? null,
});
await UserNotification.bulkCreate(
affectedUsers.map(({ user_id, group_id }) => ({
user_id,
...notify,
data: { ...notify.data, groupId: single ? group_id : null },
seen: false,
createdAt: now,
updatedAt: now,
})),
{ validate: false }
);
console.log(`[CRON][USER NOTIFY] Notified ${affectedUsers.length} user(s) about ${count} ${status} task(s).`);
}
} catch (err) {
console.error('[CRON][USER NOTIFY] Failed to emit user notifications:', err);
}
}
module.exports = {
name: 'userNotifications',
schedule: '5 * * * *',
run,
};