Add sessionHolding template, unique 8-digit codes, reason slot for sessionCancelled, and disable unused notifications
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
const { recordAndSend } = require('./senders/notificationRecorder');
|
||||
const { sendEmail } = require('./senders/emailSender');
|
||||
const { sendBaleMessage } = require('./senders/baleBotSender');
|
||||
const { resolveNotifyFlags } = require('./notifyResolver');
|
||||
const { findActionDef } = require('../components/settings/notificationActions');
|
||||
|
||||
/**
|
||||
* Send SMS (via handler), email, and bot notifications for an action when enabled.
|
||||
* SMS handler should use template-based recordAndSend internally.
|
||||
*/
|
||||
const notifyAction = async ({
|
||||
actionKey,
|
||||
userId = null,
|
||||
phoneNumber = null,
|
||||
email = null,
|
||||
subject = '',
|
||||
body = '',
|
||||
smsHandler = null,
|
||||
requestSource = {},
|
||||
relatedEvent = null
|
||||
}) => {
|
||||
const flags = await resolveNotifyFlags(requestSource, actionKey);
|
||||
const def = findActionDef(actionKey);
|
||||
const eventTag = relatedEvent || def?.relatedEvent || actionKey;
|
||||
const results = {};
|
||||
|
||||
if (flags.sms && phoneNumber && typeof smsHandler === 'function') {
|
||||
results.sms = await smsHandler();
|
||||
}
|
||||
|
||||
if (flags.email && email) {
|
||||
results.email = await recordAndSend({
|
||||
userId,
|
||||
channel: 'email',
|
||||
subject,
|
||||
body,
|
||||
relatedEvent: eventTag,
|
||||
sendFn: () => sendEmail({ to: email, subject, body })
|
||||
});
|
||||
}
|
||||
|
||||
if (flags.bot && phoneNumber) {
|
||||
results.bot = await recordAndSend({
|
||||
userId,
|
||||
channel: 'baleBot',
|
||||
subject,
|
||||
body,
|
||||
relatedEvent: eventTag,
|
||||
sendFn: () => sendBaleMessage({ chatId: phoneNumber, body })
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
notifyAction
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const { generateUniqueCode } = require('./uniqueCode');
|
||||
|
||||
/**
|
||||
* Mongoose plugin: auto-assigns an 8-digit uniqueCode before validation if missing.
|
||||
*/
|
||||
const uniqueCodePlugin = (schema, options = {}) => {
|
||||
const field = options.field || 'uniqueCode';
|
||||
|
||||
schema.add({
|
||||
[field]: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
trim: true,
|
||||
index: true
|
||||
}
|
||||
});
|
||||
|
||||
schema.pre('validate', async function assignUniqueCode(next) {
|
||||
if (this[field]) return next();
|
||||
try {
|
||||
this[field] = await generateUniqueCode(this.constructor, field);
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = uniqueCodePlugin;
|
||||
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('../config/config');
|
||||
const { Setting, SETTINGS_KEY } = require('../components/settings/settingModel');
|
||||
const {
|
||||
NOTIFICATION_ACTION_DEFS,
|
||||
mergeNotificationSettings,
|
||||
toPublicNotificationSettings,
|
||||
normalizeStoredActionEntry,
|
||||
findActionDef
|
||||
} = require('../components/settings/notificationActions');
|
||||
const {
|
||||
isEnvChannelEnabled,
|
||||
isDbChannelEnabled,
|
||||
CHANNELS
|
||||
} = require('./messagingChannels');
|
||||
const { getDbMessagingFlags, invalidateMessagingCache } = require('../components/settings/messagingFlags');
|
||||
|
||||
let settingsCache = null;
|
||||
let settingsCacheAt = 0;
|
||||
const CACHE_TTL_MS = 10000;
|
||||
|
||||
const readStoredMap = (doc) => {
|
||||
const stored = doc?.notificationSettings || {};
|
||||
if (stored instanceof Map) {
|
||||
return Object.fromEntries(stored.entries());
|
||||
}
|
||||
return { ...stored };
|
||||
};
|
||||
|
||||
const getMergedSettingsMap = (doc) => mergeNotificationSettings(readStoredMap(doc));
|
||||
|
||||
const loadNotificationSettingsDoc = async () => {
|
||||
const now = Date.now();
|
||||
if (settingsCache && (now - settingsCacheAt) < CACHE_TTL_MS) {
|
||||
return settingsCache;
|
||||
}
|
||||
|
||||
const doc = await Setting.findOne({ key: SETTINGS_KEY })
|
||||
.select('notificationSettings smsEnabled emailEnabled botEnabled')
|
||||
.lean();
|
||||
|
||||
settingsCache = {
|
||||
map: getMergedSettingsMap(doc),
|
||||
dbMessaging: {
|
||||
smsEnabled: doc?.smsEnabled,
|
||||
emailEnabled: doc?.emailEnabled,
|
||||
botEnabled: doc?.botEnabled
|
||||
}
|
||||
};
|
||||
settingsCacheAt = now;
|
||||
return settingsCache;
|
||||
};
|
||||
|
||||
const invalidateNotificationSettingsCache = () => {
|
||||
settingsCache = null;
|
||||
settingsCacheAt = 0;
|
||||
invalidateMessagingCache();
|
||||
};
|
||||
|
||||
const isGlobalChannelEnabled = async (channel) => {
|
||||
if (!CHANNELS.includes(channel)) return false;
|
||||
if (!isEnvChannelEnabled(channel, config)) return false;
|
||||
const db = await getDbMessagingFlags();
|
||||
return isDbChannelEnabled(channel, db);
|
||||
};
|
||||
|
||||
const isActionChannelEnabled = async (actionKey, channel) => {
|
||||
if (!CHANNELS.includes(channel)) return false;
|
||||
if (!(await isGlobalChannelEnabled(channel))) return false;
|
||||
|
||||
const cached = await loadNotificationSettingsDoc();
|
||||
const def = findActionDef(actionKey);
|
||||
if (!def) return true;
|
||||
|
||||
const entry = normalizeStoredActionEntry(cached.map[actionKey], def);
|
||||
return entry[channel] === true;
|
||||
};
|
||||
|
||||
const getActionChannelFlags = async (actionKey) => {
|
||||
const result = { sms: false, email: false, bot: false };
|
||||
await Promise.all(
|
||||
CHANNELS.map(async (channel) => {
|
||||
result[channel] = await isActionChannelEnabled(actionKey, channel);
|
||||
})
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
const getPublicNotificationSettings = async (doc = null) => {
|
||||
const resolved = doc || await Setting.findOne({ key: SETTINGS_KEY }).lean();
|
||||
return toPublicNotificationSettings(getMergedSettingsMap(resolved));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getMergedSettingsMap,
|
||||
isActionChannelEnabled,
|
||||
getActionChannelFlags,
|
||||
getPublicNotificationSettings,
|
||||
invalidateNotificationSettingsCache,
|
||||
readStoredMap
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const { pickNotifyFlags } = require('./notifyFlags');
|
||||
const { getActionChannelFlags } = require('./notificationSettings');
|
||||
|
||||
/**
|
||||
* Combines per-request notify flags (from admin forms) with dashboard per-action settings.
|
||||
*/
|
||||
const resolveNotifyFlags = async (source = {}, actionKey) => {
|
||||
const request = pickNotifyFlags(source);
|
||||
const dashboard = await getActionChannelFlags(actionKey);
|
||||
|
||||
return {
|
||||
sms: request.sms && dashboard.sms,
|
||||
email: request.email && dashboard.email,
|
||||
bot: request.bot && dashboard.bot
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
resolveNotifyFlags
|
||||
};
|
||||
+278
-77
@@ -13,22 +13,26 @@ const resolveUserIdByPhone = async (phoneNumber) => {
|
||||
return user?._id || null;
|
||||
};
|
||||
|
||||
const sendAccountCredentialsSms = async ({
|
||||
const resolveUserName = async (userId, fallback = '') => {
|
||||
if (!userId) return fallback;
|
||||
const user = await User.findById(userId).select('name uniqueCode').lean();
|
||||
return {
|
||||
fullName: user?.name || fallback,
|
||||
userCode: user?.uniqueCode || ''
|
||||
};
|
||||
};
|
||||
|
||||
const sendTemplateSms = async ({
|
||||
templateKey,
|
||||
receiver,
|
||||
username,
|
||||
password,
|
||||
userId = null,
|
||||
subject,
|
||||
body,
|
||||
relatedEvent
|
||||
relatedEvent,
|
||||
slotValues = {}
|
||||
}) => {
|
||||
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
||||
let fullName = '';
|
||||
if (resolvedUserId) {
|
||||
const user = await User.findById(resolvedUserId).select('name').lean();
|
||||
if (user?.name) fullName = user.name;
|
||||
}
|
||||
const { templateId, variables } = await getSmsTemplate('accountCreated');
|
||||
const { templateId, variables } = await getSmsTemplate(templateKey);
|
||||
return recordAndSend({
|
||||
userId: resolvedUserId,
|
||||
channel: 'sms',
|
||||
@@ -36,37 +40,74 @@ const sendAccountCredentialsSms = async ({
|
||||
body,
|
||||
relatedEvent,
|
||||
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
||||
username,
|
||||
password,
|
||||
fullName: fullName || username,
|
||||
name: fullName || username,
|
||||
phoneNumber: receiver,
|
||||
mobile: receiver
|
||||
mobile: receiver,
|
||||
...slotValues
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
const sendAccountCreatedSms = async (receiver, username, password, userId = null) => {
|
||||
return sendAccountCredentialsSms({
|
||||
const sendAccountCredentialsSms = async ({
|
||||
receiver,
|
||||
username,
|
||||
password,
|
||||
userId = null,
|
||||
subject,
|
||||
body,
|
||||
relatedEvent,
|
||||
templateKey = 'accountCreated',
|
||||
userCode = ''
|
||||
}) => {
|
||||
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
||||
let fullName = '';
|
||||
let code = userCode;
|
||||
if (resolvedUserId) {
|
||||
const profile = await resolveUserName(resolvedUserId, username);
|
||||
fullName = profile.fullName;
|
||||
code = code || profile.userCode;
|
||||
}
|
||||
return sendTemplateSms({
|
||||
templateKey,
|
||||
receiver,
|
||||
username,
|
||||
password,
|
||||
userId,
|
||||
subject: 'ایجاد حساب کاربری',
|
||||
body: `حساب کاربری شما ایجاد شد. نام کاربری: ${username}`,
|
||||
relatedEvent: 'user.created'
|
||||
userId: resolvedUserId,
|
||||
subject,
|
||||
body,
|
||||
relatedEvent,
|
||||
slotValues: {
|
||||
username,
|
||||
password,
|
||||
fullName: fullName || username,
|
||||
name: fullName || username,
|
||||
userCode: code
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendPasswordResetSms = async (receiver, username, password, userId = null) => {
|
||||
const sendAccountCreatedSms = async (receiver, username, password, userId = null, userCode = '') => {
|
||||
return sendAccountCredentialsSms({
|
||||
receiver,
|
||||
username,
|
||||
password,
|
||||
userId,
|
||||
userCode,
|
||||
subject: 'ایجاد حساب کاربری',
|
||||
body: `حساب کاربری شما ایجاد شد. نام کاربری: ${username}`,
|
||||
relatedEvent: 'user.created',
|
||||
templateKey: 'accountCreated'
|
||||
});
|
||||
};
|
||||
|
||||
const sendPasswordResetSms = async (receiver, username, password, userId = null, userCode = '') => {
|
||||
return sendAccountCredentialsSms({
|
||||
receiver,
|
||||
username,
|
||||
password,
|
||||
userId,
|
||||
userCode,
|
||||
subject: 'بازنشانی رمز عبور',
|
||||
body: `رمز عبور شما بازنشانی شد. نام کاربری: ${username}`,
|
||||
relatedEvent: 'user.password_reset'
|
||||
relatedEvent: 'user.password_reset',
|
||||
templateKey: 'passwordReset'
|
||||
});
|
||||
};
|
||||
|
||||
@@ -74,27 +115,26 @@ const sendClassRegisteredSms = async (receiver, className, userId = null, extraC
|
||||
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
||||
let fullName = extraContext.fullName || '';
|
||||
if (!fullName && resolvedUserId) {
|
||||
const user = await User.findById(resolvedUserId).select('name').lean();
|
||||
if (user?.name) fullName = user.name;
|
||||
const profile = await resolveUserName(resolvedUserId);
|
||||
fullName = profile.fullName;
|
||||
}
|
||||
const { templateId, variables } = await getSmsTemplate('classRegistered');
|
||||
return recordAndSend({
|
||||
return sendTemplateSms({
|
||||
templateKey: 'classRegistered',
|
||||
receiver,
|
||||
userId: resolvedUserId,
|
||||
channel: 'sms',
|
||||
subject: 'ثبتنام در کلاس',
|
||||
body: `ثبتنام شما در کلاس «${className}» انجام شد.`,
|
||||
relatedEvent: 'user.enrolled',
|
||||
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
||||
slotValues: {
|
||||
className,
|
||||
courseName: extraContext.courseName || className,
|
||||
fullName: fullName || 'کارآموز',
|
||||
phoneNumber: receiver,
|
||||
mobile: receiver,
|
||||
classStartDate: extraContext.classStartDate || '',
|
||||
classDays: extraContext.classDays || '',
|
||||
courseTime: extraContext.courseTime || '',
|
||||
classCode: extraContext.classCode || '',
|
||||
...extraContext
|
||||
}))
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -102,74 +142,226 @@ const sendClassReminderSms = async (receiver, className, time, place = '', userI
|
||||
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
||||
let fullName = extraContext.fullName || '';
|
||||
if (!fullName && resolvedUserId) {
|
||||
const user = await User.findById(resolvedUserId).select('name').lean();
|
||||
if (user?.name) fullName = user.name;
|
||||
const profile = await resolveUserName(resolvedUserId);
|
||||
fullName = profile.fullName;
|
||||
}
|
||||
const { templateId, variables } = await getSmsTemplate('classReminder');
|
||||
return recordAndSend({
|
||||
return sendTemplateSms({
|
||||
templateKey: 'classReminder',
|
||||
receiver,
|
||||
userId: resolvedUserId,
|
||||
channel: 'sms',
|
||||
subject: 'یادآوری کلاس',
|
||||
body: `یادآوری کلاس «${className}» ساعت ${time} — مکان: ${place || '-'}`,
|
||||
relatedEvent: 'session.reminder',
|
||||
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
||||
slotValues: {
|
||||
className,
|
||||
topic: extraContext.topic || className,
|
||||
courseName: extraContext.courseName || className,
|
||||
time,
|
||||
classTime: extraContext.classTime || extraContext.courseTime || extraContext.time || time || '',
|
||||
sessionDate: extraContext.sessionDate || '',
|
||||
place: place || '-',
|
||||
fullName: fullName || 'کارآموز',
|
||||
phoneNumber: receiver,
|
||||
mobile: receiver,
|
||||
classStartDate: extraContext.classStartDate || '',
|
||||
classDays: extraContext.classDays || '',
|
||||
courseTime: extraContext.courseTime || extraContext.time || time || '',
|
||||
courseTime: extraContext.courseTime || extraContext.classTime || extraContext.time || time || '',
|
||||
classCode: extraContext.classCode || '',
|
||||
...extraContext
|
||||
}))
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendInvoiceCreatedSms = async (receiver, payload, userId = null) => {
|
||||
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
||||
const { templateId, variables } = await getSmsTemplate('invoiceCreated');
|
||||
const data = typeof payload === 'object' && payload !== null ? payload : { amount: payload };
|
||||
const fullNameLabel = data.fullName || 'کارآموز';
|
||||
const amountLabel = data.amount != null ? String(data.amount) : '';
|
||||
const courseLabel = data.course || '-';
|
||||
|
||||
let fullName = '';
|
||||
let amount = '';
|
||||
let course = '';
|
||||
let classStartDate = '';
|
||||
let classDays = '';
|
||||
let courseTime = '';
|
||||
|
||||
if (typeof payload === 'object' && payload !== null) {
|
||||
fullName = payload.fullName || '';
|
||||
amount = payload.amount != null ? String(payload.amount) : '';
|
||||
course = payload.course || '';
|
||||
classStartDate = payload.classStartDate || '';
|
||||
classDays = payload.classDays || '';
|
||||
courseTime = payload.courseTime || '';
|
||||
} else {
|
||||
amount = String(payload || '');
|
||||
}
|
||||
|
||||
const amountLabel = String(amount);
|
||||
const courseLabel = course || '-';
|
||||
const fullNameLabel = fullName || 'کارآموز';
|
||||
|
||||
return recordAndSend({
|
||||
return sendTemplateSms({
|
||||
templateKey: 'invoiceCreated',
|
||||
receiver,
|
||||
userId: resolvedUserId,
|
||||
channel: 'sms',
|
||||
subject: 'ایجاد صورتحساب',
|
||||
body: `کارآموز عزیز، ${fullNameLabel}، یک صورتحساب به مبلغ ${amountLabel} تومان بابت دوره «${courseLabel}» برای شما ایجاد شد.`,
|
||||
relatedEvent: 'payment.created',
|
||||
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
||||
slotValues: {
|
||||
fullName: fullNameLabel,
|
||||
amount: amountLabel,
|
||||
course: courseLabel,
|
||||
phoneNumber: receiver,
|
||||
mobile: receiver,
|
||||
classStartDate,
|
||||
classDays,
|
||||
courseTime
|
||||
}))
|
||||
classStartDate: data.classStartDate || '',
|
||||
classDays: data.classDays || '',
|
||||
courseTime: data.courseTime || '',
|
||||
invoiceCode: data.invoiceCode || ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendPaymentStatusChangedSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'paymentStatusChanged',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'تغییر وضعیت پرداخت',
|
||||
body: `وضعیت صورتحساب شما به «${data.statusLabel || data.status || '-'}» تغییر کرد.`,
|
||||
relatedEvent: 'payment.status_changed',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
status: data.statusLabel || data.status || '-',
|
||||
amount: data.amount != null ? String(data.amount) : '',
|
||||
invoiceCode: data.invoiceCode || '',
|
||||
course: data.course || '-'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendPaymentReminderSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'paymentReminder',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'یادآوری سررسید پرداخت',
|
||||
body: `یادآوری: مبلغ ${data.amount || '-'} تومان تا ${data.dueDate || '-'} سررسید دارد.`,
|
||||
relatedEvent: 'payment.reminder_due',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
amount: data.amount != null ? String(data.amount) : '',
|
||||
dueDate: data.dueDate || '',
|
||||
invoiceCode: data.invoiceCode || '',
|
||||
course: data.course || '-'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendTransactionRecordedSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'transactionRecorded',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'ثبت تراکنش',
|
||||
body: `تراکنش به مبلغ ${data.amount || '-'} تومان ثبت شد.`,
|
||||
relatedEvent: 'payment.transaction_added',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
amount: data.amount != null ? String(data.amount) : '',
|
||||
invoiceCode: data.invoiceCode || '',
|
||||
transactionCode: data.transactionCode || '',
|
||||
receiptNumber: data.receiptNumber || ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendSessionCancelledSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'sessionCancelled',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'لغو جلسه',
|
||||
body: `جلسه «${data.topic || data.className || 'کلاس'}» لغو شد.`,
|
||||
relatedEvent: 'session.cancelled',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
className: data.className || '-',
|
||||
topic: data.topic || '-',
|
||||
sessionDate: data.sessionDate || '',
|
||||
reason: data.reason || '-',
|
||||
classCode: data.classCode || ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendSessionHoldingSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'sessionHolding',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'برگزاری جلسه طبق برنامه',
|
||||
body: `جلسه «${data.topic || data.className || 'کلاس'}» کلاس ${data.className || ''} در تاریخ ${data.sessionDate || '-'} و ساعت ${data.classTime || data.time || '-'} طبق برنامه برگزار خواهد شد.`,
|
||||
relatedEvent: 'session.holding',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
topic: data.topic || '-',
|
||||
className: data.className || '-',
|
||||
sessionDate: data.sessionDate || '',
|
||||
classTime: data.classTime || data.time || '',
|
||||
courseName: data.courseName || data.className || '-',
|
||||
classCode: data.classCode || '',
|
||||
place: data.place || '-'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendClassRequestApprovedSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'classRequestApproved',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'تأیید درخواست تشکیل کلاس',
|
||||
body: `درخواست تشکیل کلاس «${data.courseName || '-'}» تأیید شد.`,
|
||||
relatedEvent: 'class_request.approved',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
courseName: data.courseName || '-',
|
||||
classCode: data.classCode || ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendClassRequestRejectedSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'classRequestRejected',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'رد درخواست ثبتنام',
|
||||
body: `درخواست شما برای «${data.courseName || '-'}» رد شد.`,
|
||||
relatedEvent: 'class_request.rejected',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
courseName: data.courseName || '-',
|
||||
reason: data.reason || '-',
|
||||
registrationCode: data.registrationCode || ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendPendingRegistrationSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'pendingRegistration',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'دریافت درخواست ثبتنام',
|
||||
body: `درخواست ثبتنام شما در «${data.className || '-'}» دریافت شد.`,
|
||||
relatedEvent: 'pending.registration_received',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
className: data.className || '-',
|
||||
registrationCode: data.registrationCode || ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendCertificateIssuedSms = async (receiver, payload, userId = null) => {
|
||||
const data = payload || {};
|
||||
return sendTemplateSms({
|
||||
templateKey: 'certificateIssued',
|
||||
receiver,
|
||||
userId,
|
||||
subject: 'صدور گواهینامه',
|
||||
body: `گواهینامه «${data.certificateTitle || '-'}» برای شما صادر شد.`,
|
||||
relatedEvent: 'certificate.issued',
|
||||
slotValues: {
|
||||
fullName: data.fullName || 'کارآموز',
|
||||
certificateTitle: data.certificateTitle || '-',
|
||||
certificateCode: data.certificateCode || '',
|
||||
courseName: data.courseName || '-'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -178,5 +370,14 @@ module.exports = {
|
||||
sendPasswordResetSms,
|
||||
sendClassRegisteredSms,
|
||||
sendClassReminderSms,
|
||||
sendInvoiceCreatedSms
|
||||
sendInvoiceCreatedSms,
|
||||
sendPaymentStatusChangedSms,
|
||||
sendPaymentReminderSms,
|
||||
sendTransactionRecordedSms,
|
||||
sendSessionCancelledSms,
|
||||
sendSessionHoldingSms,
|
||||
sendClassRequestApprovedSms,
|
||||
sendClassRequestRejectedSms,
|
||||
sendPendingRegistrationSms,
|
||||
sendCertificateIssuedSms
|
||||
};
|
||||
|
||||
@@ -8,6 +8,15 @@ const {
|
||||
sendClassRegisteredSms,
|
||||
sendClassReminderSms,
|
||||
sendInvoiceCreatedSms,
|
||||
sendPaymentStatusChangedSms,
|
||||
sendPaymentReminderSms,
|
||||
sendTransactionRecordedSms,
|
||||
sendSessionCancelledSms,
|
||||
sendSessionHoldingSms,
|
||||
sendClassRequestApprovedSms,
|
||||
sendClassRequestRejectedSms,
|
||||
sendPendingRegistrationSms,
|
||||
sendCertificateIssuedSms,
|
||||
} = require('./smsMessages');
|
||||
|
||||
/**
|
||||
@@ -36,4 +45,13 @@ module.exports = {
|
||||
sendClassRegisteredSms,
|
||||
sendClassReminderSms,
|
||||
sendInvoiceCreatedSms,
|
||||
sendPaymentStatusChangedSms,
|
||||
sendPaymentReminderSms,
|
||||
sendTransactionRecordedSms,
|
||||
sendSessionCancelledSms,
|
||||
sendSessionHoldingSms,
|
||||
sendClassRequestApprovedSms,
|
||||
sendClassRequestRejectedSms,
|
||||
sendPendingRegistrationSms,
|
||||
sendCertificateIssuedSms,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const MIN_CODE = 10000000;
|
||||
const MAX_CODE = 100000000;
|
||||
|
||||
/** Generate a random 8-digit numeric string (10000000–99999999). */
|
||||
const generateCode = () => String(crypto.randomInt(MIN_CODE, MAX_CODE));
|
||||
|
||||
/**
|
||||
* Allocate a unique code for a Mongoose model field.
|
||||
* Retries on collision up to maxAttempts times.
|
||||
*/
|
||||
const generateUniqueCode = async (Model, field = 'uniqueCode', maxAttempts = 24) => {
|
||||
if (!Model) throw new Error('Model is required to generate a unique code');
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
const code = generateCode();
|
||||
const exists = await Model.exists({ [field]: code });
|
||||
if (!exists) return code;
|
||||
}
|
||||
|
||||
throw new Error(`Could not generate a unique ${field} after ${maxAttempts} attempts`);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateCode,
|
||||
generateUniqueCode
|
||||
};
|
||||
Reference in New Issue
Block a user