mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
138 lines
6.6 KiB
JavaScript
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,
|
|
};
|