Add sessionHolding template, unique 8-digit codes, reason slot for sessionCancelled, and disable unused notifications

This commit is contained in:
2026-08-21 10:44:51 +03:30
parent 5328f36f06
commit 091e519280
28 changed files with 2180 additions and 280 deletions
@@ -9,12 +9,13 @@ const bcrypt = require('bcryptjs');
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
const { calculateRegistrationPricing } = require('../../utils/registrationPricing');
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
const { sendAccountCreatedSms, sendClassRequestApprovedSms, sendClassRequestRejectedSms, sendPendingRegistrationSms } = require('../../utils/senders/smsMessages');
const { notifyAction } = require('../../utils/actionNotify');
const { resolveNotifyFlags } = require('../../utils/notifyResolver');
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
const classService = require('../classes/classService');
const paymentService = require('../payments/paymentService');
const logger = require('../../utils/logger');
const { recordAndSend } = require('../../utils/senders/notificationRecorder');
const POPULATE_LIST = [
{ path: 'user', select: 'name phoneNumber nationalIdCode email username' },
@@ -148,7 +149,18 @@ const findOrCreateUser = async (body) => {
});
try {
await sendAccountCreatedSms(normalizedPhone, username, plainPassword, user._id);
const notify = await resolveNotifyFlags({}, 'accountCreated');
if (notify.sms || notify.email || notify.bot) {
await notifyAction({
actionKey: 'accountCreated',
userId: user._id,
phoneNumber: normalizedPhone,
email,
subject: 'ایجاد حساب کاربری',
body: `حساب کاربری شما ایجاد شد. نام کاربری: ${username} — کد کاربری: ${user.uniqueCode || ''}`,
smsHandler: () => sendAccountCreatedSms(normalizedPhone, username, plainPassword, user._id, user.uniqueCode)
});
}
} catch (err) {
logger.error(`[findOrCreateUser] Account SMS failed for ${normalizedPhone}: ${err.message}`);
}
@@ -214,6 +226,27 @@ const createPendingStudentAfterPayment = async (body) => {
paymentReference: paymentReference || undefined
});
try {
const notify = await resolveNotifyFlags({}, 'pendingRegistration');
if (notify.sms || notify.email || notify.bot) {
await notifyAction({
actionKey: 'pendingRegistration',
userId: user._id,
phoneNumber: user.phoneNumber,
email: user.email,
subject: 'دریافت درخواست ثبت‌نام',
body: `درخواست ثبت‌نام شما در «${cls.name}» دریافت شد. کد درخواست: ${pending.uniqueCode || ''}`,
smsHandler: () => sendPendingRegistrationSms(user.phoneNumber, {
fullName: user.name || '',
className: cls.name || '',
registrationCode: pending.uniqueCode || ''
}, user._id)
});
}
} catch (err) {
logger.error(`[createPendingStudentAfterPayment] Notification failed: ${err.message}`);
}
return PendingStudent.findById(pending._id).populate(POPULATE_LIST).lean();
};
@@ -262,26 +295,27 @@ const getPendingStudentById = async (id) => {
return pending;
};
const sendClassRequestApprovedSms = async (user, courseTitle) => {
const sendClassRequestApprovedNotification = async (user, courseTitle, classCode = '') => {
if (!user?.phoneNumber) return;
const body = `درخواست تشکیل کلاس جدید برای «${courseTitle}» تأیید شد. به‌زودی با شما هماهنگ می‌کنیم.`;
await recordAndSend({
await notifyAction({
actionKey: 'classRequestApproved',
userId: user._id,
channel: 'sms',
subject: 'تأیید درخواست کلاس',
body,
relatedEvent: 'class_request.approved',
sendFn: async () => {
logger.info(`[class_request.approved] SMS queued for ${user.phoneNumber}: ${body}`);
return { skipped: true, reason: 'template_pending' };
}
phoneNumber: user.phoneNumber,
email: user.email,
subject: 'تأیید درخواست تشکیل کلاس',
body: `درخواست تشکیل کلاس «${courseTitle}» تأیید شد.`,
smsHandler: () => sendClassRequestApprovedSms(user.phoneNumber, {
fullName: user.name || '',
courseName: courseTitle,
classCode
}, user._id)
});
};
const approvePendingStudent = async (id, actorId, body = {}) => {
const pending = await PendingStudent.findById(id)
.populate({ path: 'user', select: 'name phoneNumber' })
.populate({ path: 'class', select: 'name course', populate: { path: 'course', select: 'title' } })
.populate({ path: 'class', select: 'name course uniqueCode', populate: { path: 'course', select: 'title' } })
.populate({ path: 'course', select: 'title' });
if (!pending) throw new AppError('NOT_FOUND', null, 'درخواست در انتظار یافت نشد.');
@@ -321,7 +355,11 @@ const approvePendingStudent = async (id, actorId, body = {}) => {
}, actorId);
} else {
const courseTitle = pending.course?.title || pending.class?.course?.title || pending.class?.name || 'دوره';
await sendClassRequestApprovedSms(pending.user, courseTitle);
await sendClassRequestApprovedNotification(
pending.user,
courseTitle,
pending.class?.uniqueCode || ''
);
}
await PendingStudent.findByIdAndDelete(id);
@@ -330,7 +368,10 @@ const approvePendingStudent = async (id, actorId, body = {}) => {
};
const rejectPendingStudent = async (id, actorId, body = {}) => {
const pending = await PendingStudent.findById(id);
const pending = await PendingStudent.findById(id)
.populate({ path: 'user', select: 'name phoneNumber email' })
.populate({ path: 'class', select: 'name', populate: { path: 'course', select: 'title' } })
.populate({ path: 'course', select: 'title' });
if (!pending) throw new AppError('NOT_FOUND', null, 'درخواست در انتظار یافت نشد.');
if (!['pending_review', 'pending_payment'].includes(pending.status)) {
throw new AppError('VALIDATION_FAILED', null, 'این درخواست قابل رد کردن نیست.');
@@ -340,6 +381,26 @@ const rejectPendingStudent = async (id, actorId, body = {}) => {
logger.info(`[rejectPendingStudent] ${id} by ${actorId}: ${String(body.adminNotes).trim()}`);
}
try {
const courseTitle = pending.course?.title || pending.class?.course?.title || pending.class?.name || 'دوره';
await notifyAction({
actionKey: 'classRequestRejected',
userId: pending.user?._id,
phoneNumber: pending.user?.phoneNumber,
email: pending.user?.email,
subject: 'رد درخواست ثبت‌نام',
body: `درخواست شما برای «${courseTitle}» رد شد.`,
smsHandler: () => sendClassRequestRejectedSms(pending.user.phoneNumber, {
fullName: pending.user?.name || '',
courseName: courseTitle,
reason: body.adminNotes ? String(body.adminNotes).trim() : '—',
registrationCode: pending.uniqueCode || ''
}, pending.user?._id)
});
} catch (err) {
logger.error(`[rejectPendingStudent] Notification failed: ${err.message}`);
}
await PendingStudent.findByIdAndDelete(id);
return { rejected: true };
};