Expose schedule fields on class and invoice templates so SMS can include Jalali start date, weekdays, and hour range.
105 lines
3.5 KiB
JavaScript
105 lines
3.5 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' })
|
|
.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,
|
|
};
|