Initial commit: teaching institution management API.

Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
This commit is contained in:
2026-08-09 04:18:08 +02:00
commit f04c797be6
107 changed files with 9190 additions and 0 deletions
+95
View File
@@ -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,
};
+45
View File
@@ -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
};
+66
View File
@@ -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
};
+28
View File
@@ -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
};