// /jobs/classReminderJob.js 'use strict'; const cron = require('node-cron'); const Session = require('../components/sessions/sessionModel'); const User = require('../components/users/userModel'); const { sendClassReminderSms } = require('../utils/senders/smsMessages'); const { buildClassScheduleContext } = require('../utils/classSchedule'); const { notifyAction } = require('../utils/actionNotify'); const { resolveNotifyFlags } = require('../utils/notifyResolver'); const logger = require('../utils/logger'); const parseStartDateTime = (session) => { const day = new Date(session.day); if (Number.isNaN(day.getTime())) return null; const [hoursRaw, minutesRaw = '0'] = String(session.startTime || '00:00').split(':'); const hours = Number(hoursRaw); const minutes = Number(minutesRaw); if (Number.isNaN(hours) || Number.isNaN(minutes)) return null; const start = new Date(day); start.setHours(hours, minutes, 0, 0); return start; }; const runClassReminderJob = async () => { try { const now = Date.now(); const windowStart = new Date(now + 29 * 60 * 1000); const windowEnd = new Date(now + 31 * 60 * 1000); // Load scheduled sessions for today ± 1 day, then filter by exact start window const dayFrom = new Date(now - 24 * 60 * 60 * 1000); const dayTo = new Date(now + 24 * 60 * 60 * 1000); const sessions = await Session.find({ status: 'scheduled', reminderSentAt: null, day: { $gte: dayFrom, $lte: dayTo }, }) .populate({ path: 'class', select: 'name students startDate days startTime endTime uniqueCode' }) .populate({ path: 'course', select: 'title' }) .lean(); for (const session of sessions) { const startAt = parseStartDateTime(session); if (!startAt || startAt < windowStart || startAt > windowEnd) continue; const classDoc = session.class; const studentIds = classDoc?.students || []; if (studentIds.length === 0) { await Session.updateOne({ _id: session._id }, { reminderSentAt: new Date() }); continue; } const classLabel = classDoc?.name || session.course?.title || 'کلاس'; const timeLabel = session.startTime; const placeLabel = session.place || '-'; const classSessions = classDoc?._id ? await Session.find({ class: classDoc._id }).select('day startTime endTime').sort({ day: 1 }).lean() : []; const schedule = buildClassScheduleContext(classDoc, classSessions, session); const users = await User.find({ _id: { $in: studentIds } }).select('phoneNumber email name').lean(); const notify = await resolveNotifyFlags({}, 'classReminder'); if (notify.sms || notify.email || notify.bot) { await Promise.all( users.map(async (user) => { if (!user.phoneNumber) return; try { await notifyAction({ actionKey: 'classReminder', userId: user._id, phoneNumber: user.phoneNumber, email: user.email, subject: 'یادآوری کلاس', body: `یادآوری کلاس «${classLabel}» ساعت ${timeLabel} — مکان: ${placeLabel || '-'}`, smsHandler: () => sendClassReminderSms( user.phoneNumber, classLabel, timeLabel, placeLabel, user._id, { fullName: user.name, topic: session.topic || classLabel, sessionDate: session.day ? new Date(session.day).toLocaleDateString('fa-IR') : '', classTime: timeLabel || session.startTime || '', courseName: session.course?.title || classLabel, classCode: classDoc?.uniqueCode || '', ...schedule } ) }); } catch (err) { logger.error(`[ClassReminderJob] Notification failed for ${user.phoneNumber}: ${err.message}`); } }) ); } await Session.updateOne({ _id: session._id }, { reminderSentAt: new Date() }); logger.info(`[ClassReminderJob] Reminders sent for session ${session._id}`); } } catch (error) { logger.error(`[ClassReminderJob ERROR]: ${error.message}`); } }; const startClassReminderJob = () => { // Every minute — catches sessions ~30 minutes before start cron.schedule('* * * * *', async () => { await runClassReminderJob(); }); logger.info('[ClassReminderJob] Scheduled to run every minute.'); }; module.exports = { startClassReminderJob, runClassReminderJob, };