// /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 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' }) .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').lean(); await Promise.all( users.map(async (user) => { if (!user.phoneNumber) return; try { await sendClassReminderSms( user.phoneNumber, classLabel, timeLabel, placeLabel, user._id, { courseName: session.course?.title || classLabel, ...schedule } ); } catch (err) { logger.error(`[ClassReminderJob] SMS 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, };