Files
gameno-api/jobs/classReminderJob.js
T

125 lines
4.7 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 { 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 },
isDeleted: { $ne: true }
})
.populate({ path: 'class', select: 'name students startDate days startTime endTime uniqueCode isDeleted' })
.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;
if (!classDoc || classDoc.isDeleted) continue;
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, isDeleted: { $ne: true } }).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,
};