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,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
// /jobs/notificationRetryJob.js
|
||||
|
||||
const cron = require('node-cron');
|
||||
const Notification = require('../components/notifications/notificationModel');
|
||||
const { retryNotification } = require('../components/notifications/notificationService');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const runNotificationRetryJob = async () => {
|
||||
try {
|
||||
const failedNotifications = await Notification.find({
|
||||
status: 'failed',
|
||||
$expr: { $lt: ['$retryCount', '$maxRetries'] }
|
||||
}).limit(50);
|
||||
|
||||
if (failedNotifications.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`[NotificationRetryJob] Found ${failedNotifications.length} failed notifications to retry...`);
|
||||
|
||||
for (const notification of failedNotifications) {
|
||||
try {
|
||||
await retryNotification(notification._id);
|
||||
logger.info(`[NotificationRetryJob] Successfully retried notification ID: ${notification._id}`);
|
||||
} catch (retryError) {
|
||||
logger.warn(`[NotificationRetryJob] Retry failed for notification ID ${notification._id}: ${retryError.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[NotificationRetryJob ERROR]: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const startNotificationRetryJob = () => {
|
||||
// Run every 5 minutes
|
||||
cron.schedule('*/5 * * * *', async () => {
|
||||
await runNotificationRetryJob();
|
||||
});
|
||||
logger.info('[NotificationRetryJob] Scheduled to run every 5 minutes.');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
startNotificationRetryJob,
|
||||
runNotificationRetryJob
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// /jobs/paymentReminderJob.js
|
||||
|
||||
const cron = require('node-cron');
|
||||
const Payment = require('../components/payments/paymentModel');
|
||||
const eventEmitter = require('../events/eventEmitter');
|
||||
const EVENT_NAMES = require('../constants/eventNames');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const runPaymentReminderJob = async () => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const threeDaysFromNow = new Date();
|
||||
threeDaysFromNow.setDate(now.getDate() + 3);
|
||||
|
||||
// Find pending/partiallyPaid payments due in the next 3 days
|
||||
const upcomingPayments = await Payment.find({
|
||||
status: { $in: ['pending', 'partiallyPaid'] },
|
||||
dueDate: { $gte: now, $lte: threeDaysFromNow }
|
||||
}).populate('user');
|
||||
|
||||
for (const payment of upcomingPayments) {
|
||||
if (payment.user) {
|
||||
eventEmitter.emit(EVENT_NAMES.PAYMENT_REMINDER_DUE, {
|
||||
paymentId: payment._id,
|
||||
userId: payment.user._id,
|
||||
amountDue: payment.amount - payment.amountPaid,
|
||||
dueDate: payment.dueDate
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Find overdue payments and update status to overdue
|
||||
const overduePayments = await Payment.find({
|
||||
status: { $in: ['pending', 'partiallyPaid'] },
|
||||
dueDate: { $lt: now }
|
||||
});
|
||||
|
||||
for (const payment of overduePayments) {
|
||||
payment.status = 'overdue';
|
||||
await payment.save();
|
||||
if (payment.user) {
|
||||
eventEmitter.emit(EVENT_NAMES.PAYMENT_STATUS_CHANGED, {
|
||||
paymentId: payment._id,
|
||||
userId: payment.user,
|
||||
oldStatus: 'pending',
|
||||
newStatus: 'overdue'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[PaymentReminderJob ERROR]: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const startPaymentReminderJob = () => {
|
||||
// Run daily at midnight
|
||||
cron.schedule('0 0 * * *', async () => {
|
||||
await runPaymentReminderJob();
|
||||
});
|
||||
logger.info('[PaymentReminderJob] Scheduled to run daily at midnight.');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
startPaymentReminderJob,
|
||||
runPaymentReminderJob
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
// /jobs/tempBucketCleanupJob.js
|
||||
|
||||
const cron = require('node-cron');
|
||||
const { cleanupTempBucket } = require('../utils/s3Client');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const runTempBucketCleanupJob = async () => {
|
||||
try {
|
||||
logger.info('[TempBucketCleanupJob] Starting daily temp bucket cleanup...');
|
||||
const deletedCount = await cleanupTempBucket(10); // Files older than 10 minutes
|
||||
logger.info(`[TempBucketCleanupJob] Daily cleanup finished. Removed ${deletedCount} files.`);
|
||||
} catch (error) {
|
||||
logger.error(`[TempBucketCleanupJob ERROR]: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const startTempBucketCleanupJob = () => {
|
||||
// Schedule daily at 3:00 AM
|
||||
cron.schedule('0 3 * * *', async () => {
|
||||
await runTempBucketCleanupJob();
|
||||
});
|
||||
logger.info('[TempBucketCleanupJob] Scheduled to run daily at 3:00 AM.');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
startTempBucketCleanupJob,
|
||||
runTempBucketCleanupJob
|
||||
};
|
||||
Reference in New Issue
Block a user