Initial commit: teaching institution management API.
Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// /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 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' })
|
||||
.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 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
|
||||
);
|
||||
} 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,
|
||||
};
|
||||
Reference in New Issue
Block a user