Add sessionHolding template, unique 8-digit codes, reason slot for sessionCancelled, and disable unused notifications
This commit is contained in:
+40
-22
@@ -6,6 +6,8 @@ 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) => {
|
||||
@@ -37,7 +39,7 @@ const runClassReminderJob = async () => {
|
||||
reminderSentAt: null,
|
||||
day: { $gte: dayFrom, $lte: dayTo },
|
||||
})
|
||||
.populate({ path: 'class', select: 'name students startDate days startTime endTime' })
|
||||
.populate({ path: 'class', select: 'name students startDate days startTime endTime uniqueCode' })
|
||||
.populate({ path: 'course', select: 'title' })
|
||||
.lean();
|
||||
|
||||
@@ -60,27 +62,43 @@ const runClassReminderJob = async () => {
|
||||
: [];
|
||||
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}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
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}`);
|
||||
|
||||
@@ -2,9 +2,51 @@
|
||||
|
||||
const cron = require('node-cron');
|
||||
const Payment = require('../components/payments/paymentModel');
|
||||
const User = require('../components/users/userModel');
|
||||
const Course = require('../components/courses/courseModel');
|
||||
const eventEmitter = require('../events/eventEmitter');
|
||||
const EVENT_NAMES = require('../constants/eventNames');
|
||||
const logger = require('../utils/logger');
|
||||
const { getPayableAmount } = require('../utils/paymentAmount');
|
||||
const { notifyAction } = require('../utils/actionNotify');
|
||||
const { sendPaymentReminderSms } = require('../utils/senders/smsMessages');
|
||||
const { resolveNotifyFlags } = require('../utils/notifyResolver');
|
||||
|
||||
const formatDueDate = (date) => {
|
||||
if (!date) return '';
|
||||
const parsed = new Date(date);
|
||||
if (Number.isNaN(parsed.getTime())) return '';
|
||||
return parsed.toLocaleDateString('fa-IR');
|
||||
};
|
||||
|
||||
const notifyPaymentReminder = async (payment, user) => {
|
||||
const notify = await resolveNotifyFlags({}, 'paymentReminder');
|
||||
if (!notify.sms && !notify.email && !notify.bot) return;
|
||||
|
||||
let courseName = '-';
|
||||
if (payment.course) {
|
||||
const course = await Course.findById(payment.course).select('title').lean();
|
||||
if (course?.title) courseName = course.title;
|
||||
}
|
||||
|
||||
const amountDue = getPayableAmount(payment) - (payment.paidAmount || 0);
|
||||
|
||||
await notifyAction({
|
||||
actionKey: 'paymentReminder',
|
||||
userId: user._id,
|
||||
phoneNumber: user.phoneNumber,
|
||||
email: user.email,
|
||||
subject: 'یادآوری سررسید پرداخت',
|
||||
body: `یادآوری: مبلغ ${amountDue} تومان تا ${formatDueDate(payment.dueDate)} سررسید دارد. کد صورتحساب: ${payment.uniqueCode || ''}`,
|
||||
smsHandler: () => sendPaymentReminderSms(user.phoneNumber, {
|
||||
fullName: user.name || '',
|
||||
amount: amountDue,
|
||||
dueDate: formatDueDate(payment.dueDate),
|
||||
invoiceCode: payment.uniqueCode || '',
|
||||
course: courseName
|
||||
}, user._id)
|
||||
});
|
||||
};
|
||||
|
||||
const runPaymentReminderJob = async () => {
|
||||
try {
|
||||
@@ -12,37 +54,41 @@ const runPaymentReminderJob = async () => {
|
||||
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'] },
|
||||
status: { $in: ['pending', 'partial'] },
|
||||
dueDate: { $gte: now, $lte: threeDaysFromNow }
|
||||
}).populate('user');
|
||||
}).populate({ path: 'user', select: 'name phoneNumber email' });
|
||||
|
||||
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,
|
||||
amountDue: getPayableAmount(payment) - (payment.paidAmount || 0),
|
||||
dueDate: payment.dueDate
|
||||
});
|
||||
try {
|
||||
await notifyPaymentReminder(payment, payment.user);
|
||||
} catch (err) {
|
||||
logger.error(`[PaymentReminderJob] Notification failed for payment ${payment._id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find overdue payments and update status to overdue
|
||||
const overduePayments = await Payment.find({
|
||||
status: { $in: ['pending', 'partiallyPaid'] },
|
||||
status: { $in: ['pending', 'partial'] },
|
||||
dueDate: { $lt: now }
|
||||
});
|
||||
|
||||
for (const payment of overduePayments) {
|
||||
const previousStatus = payment.status;
|
||||
payment.status = 'overdue';
|
||||
await payment.save();
|
||||
if (payment.user) {
|
||||
eventEmitter.emit(EVENT_NAMES.PAYMENT_STATUS_CHANGED, {
|
||||
paymentId: payment._id,
|
||||
userId: payment.user,
|
||||
oldStatus: 'pending',
|
||||
oldStatus: previousStatus,
|
||||
newStatus: 'overdue'
|
||||
});
|
||||
}
|
||||
@@ -53,7 +99,6 @@ const runPaymentReminderJob = async () => {
|
||||
};
|
||||
|
||||
const startPaymentReminderJob = () => {
|
||||
// Run daily at midnight
|
||||
cron.schedule('0 0 * * *', async () => {
|
||||
await runPaymentReminderJob();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user