import { db } from "@workspace/db"; import { tasksTable, notificationsTable } from "@workspace/db"; import { and, notInArray, gt, lte, eq, gte } from "drizzle-orm"; import { logger } from "./logger.js"; let isScanning = false; /** * Scans tasks to identify those due within 24 hours, sending idempotent notifications * to assigned users and creators. * * Returns the count of notifications successfully sent. */ export async function checkDeadlines(): Promise { if (isScanning) { logger.info("A deadline check is already in progress, skipping concurrent run."); return 0; } isScanning = true; let sentCount = 0; try { const now = new Date(); const twentyFourHoursFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000); const startOfToday = new Date(); startOfToday.setHours(0, 0, 0, 0); logger.info({ now: now.toISOString(), twentyFourHoursFromNow: twentyFourHoursFromNow.toISOString() }, "Scanning database for upcoming deadlines..."); // Query active tasks due within 24 hours const matchingTasks = await db.select().from(tasksTable) .where(and( notInArray(tasksTable.status, ["completed", "cancelled"]), gt(tasksTable.dueDate, now), lte(tasksTable.dueDate, twentyFourHoursFromNow) )); logger.info({ matchCount: matchingTasks.length }, `Found ${matchingTasks.length} tasks due soon.`); for (const task of matchingTasks) { // Users to notify const userIdsToNotify: number[] = []; if (task.assignedUserId) userIdsToNotify.push(task.assignedUserId); if (task.createdByUserId && task.createdByUserId !== task.assignedUserId) { userIdsToNotify.push(task.createdByUserId); } for (const targetUserId of userIdsToNotify) { try { // Idempotency check: did we already notify this user for this task today? const existing = await db.select().from(notificationsTable) .where(and( eq(notificationsTable.userId, targetUserId), eq(notificationsTable.taskId, task.id), eq(notificationsTable.type, "deadline_soon"), gte(notificationsTable.createdAt, startOfToday) )); if (existing.length > 0) { logger.debug({ taskId: task.id, targetUserId }, "Idempotency trigger: deadline notification already sent today."); continue; } // Create notification const msg = `تنبيه: مهمة «${task.title}» موعد تسليمها غداً`; await db.insert(notificationsTable).values({ userId: targetUserId, taskId: task.id, type: "deadline_soon", message: msg, isRead: false }); logger.info({ taskId: task.id, targetUserId }, `Sent deadline_soon notification to user #${targetUserId} for task #${task.id}`); sentCount++; } catch (innerErr) { logger.error({ innerErr, taskId: task.id, targetUserId }, "Failed to send individual deadline notification"); } } } logger.info({ sentCount }, "Deadline scanning complete."); } catch (err) { logger.error({ err }, "Deadline checking operation encountered an error"); } finally { isScanning = false; } return sentCount; } /** * Sets up a self-scheduling setTimeout loop to run checkDeadlines() at 9:00 AM daily. */ export function scheduleDailyCheck() { const scheduleNext = () => { const now = new Date(); const nextRun = new Date(); nextRun.setHours(9, 0, 0, 0); // If 9:00 AM today has already passed, schedule for tomorrow if (now >= nextRun) { nextRun.setDate(nextRun.getDate() + 1); } const delay = nextRun.getTime() - now.getTime(); logger.info({ nextRun: nextRun.toISOString(), delayMs: delay }, "Deadline checker scheduled daily run"); setTimeout(async () => { try { logger.info("Running scheduled daily deadline check..."); const sent = await checkDeadlines(); logger.info({ sent }, "Scheduled daily deadline check execution completed."); } catch (err) { logger.error({ err }, "Scheduled daily deadline check execution failed"); } // Re-schedule for the next execution scheduleNext(); }, delay); }; scheduleNext(); }