Add sessionHolding template, unique 8-digit codes, reason slot for sessionCancelled, and disable unused notifications
This commit is contained in:
+110
-92
@@ -5,11 +5,11 @@ const EVENT_NAMES = require('../constants/eventNames');
|
||||
const EventLog = require('./eventLogModel');
|
||||
const Notification = require('../components/notifications/notificationModel');
|
||||
const User = require('../components/users/userModel');
|
||||
const Course = require('../components/courses/courseModel');
|
||||
const Session = require('../components/sessions/sessionModel');
|
||||
const logger = require('../utils/logger');
|
||||
const { sendEmail } = require('../utils/senders/emailSender');
|
||||
const { sendSMS } = require('../utils/senders/smsSender');
|
||||
const { sendBaleMessage } = require('../utils/senders/baleBotSender');
|
||||
const { notifyAction } = require('../utils/actionNotify');
|
||||
const { sendSessionCancelledSms, sendSessionHoldingSms } = require('../utils/senders/smsMessages');
|
||||
const { resolveNotifyFlags } = require('../utils/notifyResolver');
|
||||
|
||||
const safeEventListener = (handler) => {
|
||||
return async (payload) => {
|
||||
@@ -33,115 +33,133 @@ const recordEventLog = async (eventName, payload, actor = null) => {
|
||||
}
|
||||
};
|
||||
|
||||
const dispatchNotification = async ({ userId, preferredChannel, subject, body, relatedEvent }) => {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) return;
|
||||
|
||||
const channelMap = {
|
||||
Email: 'email',
|
||||
SMS: 'sms',
|
||||
Bale: 'baleBot',
|
||||
WhatsApp: 'sms',
|
||||
Telegram: 'baleBot'
|
||||
};
|
||||
|
||||
const preferred = Array.isArray(user.preferredMessenger)
|
||||
? user.preferredMessenger[0]
|
||||
: user.preferredMessenger;
|
||||
const channel = channelMap[preferred || preferredChannel] || 'sms';
|
||||
|
||||
const notification = await Notification.create({
|
||||
user: userId,
|
||||
channel,
|
||||
subject: subject || 'Institution Notification',
|
||||
body,
|
||||
status: 'pending',
|
||||
relatedEvent
|
||||
});
|
||||
|
||||
let sendResult;
|
||||
try {
|
||||
if (channel === 'email' && user.email) {
|
||||
sendResult = await sendEmail({ to: user.email, subject, body });
|
||||
} else if (channel === 'baleBot') {
|
||||
sendResult = await sendBaleMessage({ chatId: user.phoneNumber, body });
|
||||
} else {
|
||||
sendResult = await sendSMS({ phoneNumber: user.phoneNumber, body });
|
||||
}
|
||||
|
||||
notification.status = 'sent';
|
||||
notification.sentAt = new Date();
|
||||
await notification.save();
|
||||
eventEmitter.emit(EVENT_NAMES.NOTIFICATION_SENT, { notificationId: notification._id });
|
||||
} catch (sendErr) {
|
||||
notification.status = 'failed';
|
||||
notification.lastError = sendErr.message;
|
||||
notification.retryCount = 1;
|
||||
await notification.save();
|
||||
eventEmitter.emit(EVENT_NAMES.NOTIFICATION_FAILED, { notificationId: notification._id, error: sendErr.message });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[DispatchNotification Error]: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const registerEventListeners = () => {
|
||||
// Listener for USER_ENROLLED
|
||||
eventEmitter.on(EVENT_NAMES.USER_ENROLLED, safeEventListener(async (payload) => {
|
||||
const { userId, courseId, actorId } = payload;
|
||||
const { actorId } = payload;
|
||||
await recordEventLog(EVENT_NAMES.USER_ENROLLED, payload, actorId);
|
||||
|
||||
const course = await Course.findById(courseId);
|
||||
const courseTitle = course ? course.title : 'Course';
|
||||
|
||||
await dispatchNotification({
|
||||
userId,
|
||||
subject: 'Course Enrollment Confirmation',
|
||||
body: `You have been successfully enrolled in ${courseTitle}.`,
|
||||
relatedEvent: EVENT_NAMES.USER_ENROLLED
|
||||
});
|
||||
}));
|
||||
|
||||
// Listener for PAYMENT_STATUS_CHANGED
|
||||
eventEmitter.on(EVENT_NAMES.PAYMENT_STATUS_CHANGED, safeEventListener(async (payload) => {
|
||||
const { paymentId, userId, newStatus, actorId } = payload;
|
||||
const { actorId } = payload;
|
||||
await recordEventLog(EVENT_NAMES.PAYMENT_STATUS_CHANGED, payload, actorId);
|
||||
|
||||
await dispatchNotification({
|
||||
userId,
|
||||
subject: 'Payment Status Update',
|
||||
body: `Your payment status has been updated to: ${newStatus}.`,
|
||||
relatedEvent: EVENT_NAMES.PAYMENT_STATUS_CHANGED
|
||||
});
|
||||
}));
|
||||
|
||||
// Listener for COURSE_PRICE_CHANGED
|
||||
eventEmitter.on(EVENT_NAMES.COURSE_PRICE_CHANGED, safeEventListener(async (payload) => {
|
||||
const { courseId, oldPrice, newPrice, actorId } = payload;
|
||||
await recordEventLog(EVENT_NAMES.COURSE_PRICE_CHANGED, payload, actorId);
|
||||
logger.info(`[Event] Course ${courseId} price changed from ${oldPrice} to ${newPrice}`);
|
||||
}));
|
||||
|
||||
// Listener for SESSION_CANCELLED
|
||||
eventEmitter.on(EVENT_NAMES.SESSION_CANCELLED, safeEventListener(async (payload) => {
|
||||
const { sessionId, courseId, topic, actorId } = payload;
|
||||
const { sessionId, courseId, topic, reason, actorId } = payload;
|
||||
await recordEventLog(EVENT_NAMES.SESSION_CANCELLED, payload, actorId);
|
||||
|
||||
const enrolledUsers = await User.find({ courses: courseId });
|
||||
for (const student of enrolledUsers) {
|
||||
await dispatchNotification({
|
||||
userId: student._id,
|
||||
subject: 'Session Cancellation Notice',
|
||||
body: `The session for topic "${topic || 'Upcoming Class'}" has been cancelled.`,
|
||||
relatedEvent: EVENT_NAMES.SESSION_CANCELLED
|
||||
});
|
||||
const notify = await resolveNotifyFlags({}, 'sessionCancelled');
|
||||
if (!notify.sms && !notify.email && !notify.bot) return;
|
||||
|
||||
const session = sessionId
|
||||
? await Session.findById(sessionId).populate({ path: 'class', select: 'name uniqueCode students' }).lean()
|
||||
: null;
|
||||
const classDoc = session?.class;
|
||||
const studentIds = classDoc?.students?.length
|
||||
? classDoc.students
|
||||
: (await User.find({ courses: courseId }).select('_id')).map((u) => u._id);
|
||||
|
||||
if (!studentIds.length) return;
|
||||
|
||||
const sessionDate = session?.day
|
||||
? new Date(session.day).toLocaleDateString('fa-IR')
|
||||
: '';
|
||||
const className = classDoc?.name || 'کلاس';
|
||||
const classCode = classDoc?.uniqueCode || '';
|
||||
|
||||
const users = await User.find({ _id: { $in: studentIds } }).select('name phoneNumber email').lean();
|
||||
for (const student of users) {
|
||||
if (!student.phoneNumber) continue;
|
||||
try {
|
||||
await notifyAction({
|
||||
actionKey: 'sessionCancelled',
|
||||
userId: student._id,
|
||||
phoneNumber: student.phoneNumber,
|
||||
email: student.email,
|
||||
subject: 'لغو جلسه',
|
||||
body: `جلسه «${topic || className}» لغو شد.`,
|
||||
smsHandler: () => sendSessionCancelledSms(student.phoneNumber, {
|
||||
fullName: student.name || '',
|
||||
className,
|
||||
topic: topic || className,
|
||||
sessionDate,
|
||||
reason: reason || 'اعلام آموزشگاه',
|
||||
classCode
|
||||
}, student._id)
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`[SESSION_CANCELLED] Notification failed for user ${student._id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Generic audit logger for all other events
|
||||
Object.values(EVENT_NAMES).forEach(eventName => {
|
||||
if (![EVENT_NAMES.USER_ENROLLED, EVENT_NAMES.PAYMENT_STATUS_CHANGED, EVENT_NAMES.COURSE_PRICE_CHANGED, EVENT_NAMES.SESSION_CANCELLED].includes(eventName)) {
|
||||
eventEmitter.on(EVENT_NAMES.SESSION_HOLDING, safeEventListener(async (payload) => {
|
||||
const { sessionId, courseId, topic, classTime, actorId } = payload;
|
||||
await recordEventLog(EVENT_NAMES.SESSION_HOLDING, payload, actorId);
|
||||
|
||||
const notify = await resolveNotifyFlags({}, 'sessionHolding');
|
||||
if (!notify.sms && !notify.email && !notify.bot) return;
|
||||
|
||||
const session = sessionId
|
||||
? await Session.findById(sessionId).populate({ path: 'class', select: 'name uniqueCode students startTime place' }).populate('course', 'title').lean()
|
||||
: null;
|
||||
const classDoc = session?.class;
|
||||
const studentIds = classDoc?.students?.length
|
||||
? classDoc.students
|
||||
: (await User.find({ courses: courseId }).select('_id')).map((u) => u._id);
|
||||
|
||||
if (!studentIds.length) return;
|
||||
|
||||
const sessionDate = session?.day
|
||||
? new Date(session.day).toLocaleDateString('fa-IR')
|
||||
: '';
|
||||
const className = classDoc?.name || session?.course?.title || 'کلاس';
|
||||
const classCode = classDoc?.uniqueCode || '';
|
||||
const timeLabel = classTime || session?.startTime || '';
|
||||
const topicLabel = topic || session?.topic || className;
|
||||
|
||||
const users = await User.find({ _id: { $in: studentIds } }).select('name phoneNumber email').lean();
|
||||
for (const student of users) {
|
||||
if (!student.phoneNumber) continue;
|
||||
try {
|
||||
await notifyAction({
|
||||
actionKey: 'sessionHolding',
|
||||
userId: student._id,
|
||||
phoneNumber: student.phoneNumber,
|
||||
email: student.email,
|
||||
subject: 'برگزاری جلسه طبق برنامه',
|
||||
body: `جلسه «${topicLabel}» کلاس ${className} در تاریخ ${sessionDate} و ساعت ${timeLabel} طبق برنامه برگزار خواهد شد.`,
|
||||
smsHandler: () => sendSessionHoldingSms(student.phoneNumber, {
|
||||
fullName: student.name || '',
|
||||
className,
|
||||
topic: topicLabel,
|
||||
sessionDate,
|
||||
classTime: timeLabel,
|
||||
courseName: session?.course?.title || className,
|
||||
classCode,
|
||||
place: session?.place || '-'
|
||||
}, student._id)
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`[SESSION_HOLDING] Notification failed for user ${student._id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Object.values(EVENT_NAMES).forEach((eventName) => {
|
||||
if (![
|
||||
EVENT_NAMES.USER_ENROLLED,
|
||||
EVENT_NAMES.PAYMENT_STATUS_CHANGED,
|
||||
EVENT_NAMES.COURSE_PRICE_CHANGED,
|
||||
EVENT_NAMES.SESSION_CANCELLED,
|
||||
EVENT_NAMES.SESSION_HOLDING
|
||||
].includes(eventName)) {
|
||||
eventEmitter.on(eventName, safeEventListener(async (payload) => {
|
||||
await recordEventLog(eventName, payload);
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user