Keep the class schedule on the class itself so SMS and listings do not have to infer days and hours from sessions.
105 lines
3.6 KiB
JavaScript
105 lines
3.6 KiB
JavaScript
// /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 days startTime endTime' })
|
|
.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,
|
|
};
|