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
+9
View File
@@ -0,0 +1,9 @@
// /events/eventEmitter.js
const EventEmitter = require('events');
class DomainEventEmitter extends EventEmitter {}
const eventEmitter = new DomainEventEmitter();
module.exports = eventEmitter;
+154
View File
@@ -0,0 +1,154 @@
// /events/eventListeners.js
const eventEmitter = require('./eventEmitter');
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 logger = require('../utils/logger');
const { sendEmail } = require('../utils/senders/emailSender');
const { sendSMS } = require('../utils/senders/smsSender');
const { sendBaleMessage } = require('../utils/senders/baleBotSender');
const safeEventListener = (handler) => {
return async (payload) => {
try {
await handler(payload);
} catch (error) {
logger.error(`[EventListener Error] Handler failure: ${error.message}`, { stack: error.stack });
}
};
};
const recordEventLog = async (eventName, payload, actor = null) => {
try {
await EventLog.create({
eventName,
payload,
actor: actor || payload?.actorId || payload?.userId || null
});
} catch (err) {
logger.error(`[EventLog Error] Failed to write audit log: ${err.message}`);
}
};
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;
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;
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;
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
});
}
}));
// 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(eventName, safeEventListener(async (payload) => {
await recordEventLog(eventName, payload);
}));
}
});
logger.info('Domain event listeners successfully registered.');
};
module.exports = registerEventListeners;
+25
View File
@@ -0,0 +1,25 @@
// /events/eventLogModel.js
const mongoose = require('mongoose');
const eventLogSchema = new mongoose.Schema({
eventName: {
type: String,
required: true,
index: true
},
payload: {
type: mongoose.Schema.Types.Mixed
},
actor: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('EventLog', eventLogSchema);