File size: 4,353 Bytes
8314cf4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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<number> {
  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();
}