414 lines
15 KiB
JavaScript
414 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
const SMS_TEMPLATE_DEFS = [
|
|
{
|
|
key: 'accountCreated',
|
|
label: 'ایجاد حساب کاربری',
|
|
envKey: 'SMS_TEMPLATE_ACCOUNT_CREATED',
|
|
slots: [
|
|
{ key: 'username', label: 'نام کاربری', defaultName: 'user' },
|
|
{ key: 'password', label: 'رمز عبور', defaultName: 'password' },
|
|
{ key: 'fullName', label: 'نام کاربر', defaultName: 'name' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' },
|
|
{ key: 'userCode', label: 'کد کاربری', defaultName: 'userCode' }
|
|
]
|
|
},
|
|
{
|
|
key: 'classRegistered',
|
|
label: 'ثبتنام در کلاس',
|
|
envKey: 'SMS_TEMPLATE_CLASS_REGISTERED',
|
|
slots: [
|
|
{ key: 'className', label: 'نام کلاس', defaultName: 'className' },
|
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' },
|
|
{ key: 'classStartDate', label: 'تاریخ شروع کلاس', defaultName: 'classStartDate' },
|
|
{ key: 'classDays', label: 'روزهای برگزاری', defaultName: 'classDays' },
|
|
{ key: 'courseTime', label: 'ساعت دوره', defaultName: 'courseTime' },
|
|
{ key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' }
|
|
]
|
|
},
|
|
{
|
|
key: 'classReminder',
|
|
label: 'یادآوری کلاس',
|
|
envKey: 'SMS_TEMPLATE_CLASS_REMINDER',
|
|
slots: [
|
|
{ key: 'className', label: 'نام کلاس', defaultName: 'className' },
|
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
|
{ key: 'topic', label: 'موضوع جلسه', defaultName: 'topic' },
|
|
{ key: 'time', label: 'ساعت', defaultName: 'time' },
|
|
{ key: 'classTime', label: 'ساعت کلاس', defaultName: 'classTime' },
|
|
{ key: 'sessionDate', label: 'تاریخ جلسه', defaultName: 'sessionDate' },
|
|
{ key: 'place', label: 'مکان', defaultName: 'place' },
|
|
{ key: 'fullName', label: 'نام کاربر', defaultName: 'fullName' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' },
|
|
{ key: 'classStartDate', label: 'تاریخ شروع کلاس', defaultName: 'classStartDate' },
|
|
{ key: 'classDays', label: 'روزهای برگزاری', defaultName: 'classDays' },
|
|
{ key: 'courseTime', label: 'ساعت دوره', defaultName: 'courseTime' },
|
|
{ key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' }
|
|
]
|
|
},
|
|
{
|
|
key: 'invoiceCreated',
|
|
label: 'ایجاد صورتحساب',
|
|
envKey: 'SMS_TEMPLATE_INVOICE_CREATED',
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' },
|
|
{ key: 'amount', label: 'مبلغ', defaultName: 'PAYMENT_PRICE' },
|
|
{ key: 'course', label: 'نام دوره', defaultName: 'COURSE' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'MOBILE' },
|
|
{ key: 'classStartDate', label: 'تاریخ شروع کلاس', defaultName: 'classStartDate' },
|
|
{ key: 'classDays', label: 'روزهای برگزاری', defaultName: 'classDays' },
|
|
{ key: 'courseTime', label: 'ساعت دوره', defaultName: 'courseTime' },
|
|
{ key: 'invoiceCode', label: 'کد صورتحساب', defaultName: 'invoiceCode' }
|
|
]
|
|
},
|
|
{
|
|
key: 'passwordReset',
|
|
label: 'بازنشانی رمز عبور',
|
|
envKey: 'SMS_TEMPLATE_PASSWORD_RESET',
|
|
slots: [
|
|
{ key: 'username', label: 'نام کاربری', defaultName: 'user' },
|
|
{ key: 'password', label: 'رمز عبور', defaultName: 'password' },
|
|
{ key: 'fullName', label: 'نام کاربر', defaultName: 'name' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' },
|
|
{ key: 'userCode', label: 'کد کاربری', defaultName: 'userCode' }
|
|
]
|
|
},
|
|
{
|
|
key: 'paymentStatusChanged',
|
|
label: 'تغییر وضعیت پرداخت',
|
|
envKey: 'SMS_TEMPLATE_PAYMENT_STATUS_CHANGED',
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'status', label: 'وضعیت', defaultName: 'status' },
|
|
{ key: 'amount', label: 'مبلغ', defaultName: 'amount' },
|
|
{ key: 'invoiceCode', label: 'کد صورتحساب', defaultName: 'invoiceCode' },
|
|
{ key: 'course', label: 'نام دوره', defaultName: 'course' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
},
|
|
{
|
|
key: 'paymentReminder',
|
|
label: 'یادآوری سررسید پرداخت',
|
|
envKey: 'SMS_TEMPLATE_PAYMENT_REMINDER',
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'amount', label: 'مبلغ باقیمانده', defaultName: 'amount' },
|
|
{ key: 'dueDate', label: 'تاریخ سررسید', defaultName: 'dueDate' },
|
|
{ key: 'invoiceCode', label: 'کد صورتحساب', defaultName: 'invoiceCode' },
|
|
{ key: 'course', label: 'نام دوره', defaultName: 'course' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
},
|
|
{
|
|
key: 'transactionRecorded',
|
|
label: 'ثبت تراکنش',
|
|
envKey: 'SMS_TEMPLATE_TRANSACTION_RECORDED',
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'amount', label: 'مبلغ', defaultName: 'amount' },
|
|
{ key: 'invoiceCode', label: 'کد صورتحساب', defaultName: 'invoiceCode' },
|
|
{ key: 'transactionCode', label: 'کد تراکنش', defaultName: 'transactionCode' },
|
|
{ key: 'receiptNumber', label: 'شماره رسید', defaultName: 'receiptNumber' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
},
|
|
{
|
|
key: 'sessionCancelled',
|
|
label: 'لغو جلسه',
|
|
envKey: 'SMS_TEMPLATE_SESSION_CANCELLED',
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'className', label: 'نام کلاس', defaultName: 'className' },
|
|
{ key: 'topic', label: 'موضوع جلسه', defaultName: 'topic' },
|
|
{ key: 'sessionDate', label: 'تاریخ جلسه', defaultName: 'sessionDate' },
|
|
{ key: 'reason', label: 'دلیل لغو', defaultName: 'reason' },
|
|
{ key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
},
|
|
{
|
|
key: 'sessionHolding',
|
|
label: 'برگزاری جلسه طبق برنامه',
|
|
envKey: 'SMS_TEMPLATE_SESSION_HOLDING',
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' },
|
|
{ key: 'topic', label: 'موضوع جلسه', defaultName: 'TOPIC' },
|
|
{ key: 'className', label: 'نام کلاس', defaultName: 'CLASSNAME' },
|
|
{ key: 'sessionDate', label: 'تاریخ جلسه', defaultName: 'SESSIONDATE' },
|
|
{ key: 'classTime', label: 'ساعت کلاس', defaultName: 'CLASSTIME' },
|
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
|
{ key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' },
|
|
{ key: 'place', label: 'مکان', defaultName: 'place' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
},
|
|
{
|
|
key: 'classRequestApproved',
|
|
label: 'تأیید درخواست تشکیل کلاس',
|
|
envKey: 'SMS_TEMPLATE_CLASS_REQUEST_APPROVED',
|
|
defaultEnabled: false,
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
|
{ key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
},
|
|
{
|
|
key: 'classRequestRejected',
|
|
label: 'رد درخواست ثبتنام',
|
|
envKey: 'SMS_TEMPLATE_CLASS_REQUEST_REJECTED',
|
|
defaultEnabled: false,
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
|
{ key: 'reason', label: 'دلیل', defaultName: 'reason' },
|
|
{ key: 'registrationCode', label: 'کد درخواست', defaultName: 'registrationCode' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
},
|
|
{
|
|
key: 'pendingRegistration',
|
|
label: 'دریافت درخواست ثبتنام',
|
|
envKey: 'SMS_TEMPLATE_PENDING_REGISTRATION',
|
|
defaultEnabled: false,
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'className', label: 'نام کلاس', defaultName: 'className' },
|
|
{ key: 'registrationCode', label: 'کد درخواست', defaultName: 'registrationCode' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
},
|
|
{
|
|
key: 'certificateIssued',
|
|
label: 'صدور گواهینامه',
|
|
envKey: 'SMS_TEMPLATE_CERTIFICATE_ISSUED',
|
|
slots: [
|
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
|
{ key: 'certificateTitle', label: 'عنوان گواهینامه', defaultName: 'certificateTitle' },
|
|
{ key: 'certificateCode', label: 'کد گواهینامه', defaultName: 'certificateCode' },
|
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
|
]
|
|
}
|
|
];
|
|
|
|
const SMS_TEMPLATE_KEYS = SMS_TEMPLATE_DEFS.map((def) => def.key);
|
|
|
|
const envFallbackFor = (def) => {
|
|
if (!def?.envKey) return '';
|
|
return String(process.env[def.envKey] || '').trim();
|
|
};
|
|
|
|
const sanitizeTemplateId = (value) => {
|
|
if (value == null) return '';
|
|
const digits = String(value).trim();
|
|
if (!digits) return '';
|
|
if (!/^\d{1,30}$/.test(digits)) return null;
|
|
return digits;
|
|
};
|
|
|
|
const sanitizeVariableName = (value) => {
|
|
if (value == null) return '';
|
|
const name = String(value).trim().replace(/^#+|#+$/g, '').trim();
|
|
if (!name) return '';
|
|
// Check for dangerous injection or control chars
|
|
if (/[\r\n\t\0<>"'`]/.test(name)) return null;
|
|
if (name.length > 100) return null;
|
|
return name;
|
|
};
|
|
|
|
const defaultVariablesList = (def) => {
|
|
return (def?.slots || []).map((slot) => ({
|
|
slot: slot.key,
|
|
name: slot.defaultName
|
|
}));
|
|
};
|
|
|
|
const emptyTemplateEntry = (def) => ({
|
|
enabled: def ? (def.defaultEnabled !== false) : true,
|
|
templateId: '',
|
|
variables: defaultVariablesList(def)
|
|
});
|
|
|
|
const toVariableItem = (item, fallbackSlot = '') => {
|
|
if (item == null) return null;
|
|
if (typeof item === 'object') {
|
|
const slot = String(item.slot || fallbackSlot || '').trim();
|
|
if (!slot) return null;
|
|
return {
|
|
slot,
|
|
name: String(item.name || '').trim()
|
|
};
|
|
}
|
|
const slot = String(fallbackSlot || '').trim();
|
|
if (!slot) return null;
|
|
return {
|
|
slot,
|
|
name: String(item).trim()
|
|
};
|
|
};
|
|
|
|
const normalizeVariablesInput = (rawVariables, def) => {
|
|
if (rawVariables == null) {
|
|
return def ? defaultVariablesList(def) : [];
|
|
}
|
|
|
|
if (Array.isArray(rawVariables)) {
|
|
return rawVariables.map((item) => toVariableItem(item)).filter(Boolean);
|
|
}
|
|
|
|
if (typeof rawVariables === 'object') {
|
|
return Object.entries(rawVariables)
|
|
.map(([key, value]) => toVariableItem(value, key))
|
|
.filter(Boolean);
|
|
}
|
|
|
|
return def ? defaultVariablesList(def) : [];
|
|
};
|
|
|
|
const normalizeStoredEntry = (raw, def) => {
|
|
const defaultEnabled = def ? (def.defaultEnabled !== false) : true;
|
|
if (raw == null || raw === '') {
|
|
return {
|
|
enabled: defaultEnabled,
|
|
templateId: '',
|
|
variables: def ? defaultVariablesList(def) : []
|
|
};
|
|
}
|
|
if (typeof raw === 'string' || typeof raw === 'number') {
|
|
return {
|
|
enabled: true,
|
|
templateId: String(raw),
|
|
variables: def ? defaultVariablesList(def) : []
|
|
};
|
|
}
|
|
if (typeof raw !== 'object') {
|
|
return {
|
|
enabled: defaultEnabled,
|
|
templateId: '',
|
|
variables: def ? defaultVariablesList(def) : []
|
|
};
|
|
}
|
|
|
|
const variables = Object.prototype.hasOwnProperty.call(raw, 'variables')
|
|
? normalizeVariablesInput(raw.variables, def)
|
|
: (def ? defaultVariablesList(def) : []);
|
|
|
|
const enabled = raw.enabled !== undefined
|
|
? Boolean(raw.enabled)
|
|
: defaultEnabled;
|
|
|
|
return {
|
|
enabled,
|
|
templateId: raw.templateId != null ? String(raw.templateId) : '',
|
|
variables
|
|
};
|
|
};
|
|
|
|
const parseIncomingVariables = (rawVariables, def) => {
|
|
if (rawVariables === undefined) return undefined;
|
|
return normalizeVariablesInput(rawVariables, def);
|
|
};
|
|
|
|
const mergeTemplateEntry = (def, storedRaw, incomingRaw = null) => {
|
|
const storedEntry = normalizeStoredEntry(storedRaw, def);
|
|
let templateId = storedEntry.templateId || envFallbackFor(def);
|
|
let variables = storedEntry.variables;
|
|
let enabled = storedEntry.enabled;
|
|
|
|
if (incomingRaw) {
|
|
if (incomingRaw.enabled !== undefined) {
|
|
enabled = Boolean(incomingRaw.enabled);
|
|
}
|
|
|
|
if (incomingRaw.templateId !== undefined) {
|
|
const sanitized = sanitizeTemplateId(incomingRaw.templateId);
|
|
if (sanitized === null) {
|
|
const error = new Error(`Invalid SMS template ID for ${def?.key || 'template'}`);
|
|
error.code = 'INVALID_TEMPLATE_ID';
|
|
error.field = def?.key;
|
|
throw error;
|
|
}
|
|
templateId = sanitized;
|
|
}
|
|
|
|
if (incomingRaw.variables !== undefined) {
|
|
const parsed = parseIncomingVariables(incomingRaw.variables, def) || [];
|
|
for (const item of parsed) {
|
|
const sanitized = sanitizeVariableName(item.name);
|
|
if (item.name && sanitized === null) {
|
|
const error = new Error(`Invalid SMS variable name "${item.name}"`);
|
|
error.code = 'INVALID_VARIABLE_NAME';
|
|
error.field = `${def?.key}.${item.slot}`;
|
|
error.rawName = item.name;
|
|
throw error;
|
|
}
|
|
}
|
|
variables = parsed;
|
|
}
|
|
} else if (!storedEntry.templateId) {
|
|
templateId = envFallbackFor(def);
|
|
}
|
|
|
|
return {
|
|
enabled,
|
|
templateId,
|
|
variables: resolveVariablesList(def, variables)
|
|
};
|
|
};
|
|
|
|
const resolveVariablesList = (def, storedVariables) => {
|
|
if (!storedVariables) {
|
|
return defaultVariablesList(def);
|
|
}
|
|
|
|
return normalizeVariablesInput(storedVariables, def)
|
|
.map((item) => {
|
|
const slot = String(item.slot || '').trim();
|
|
if (!slot) return null;
|
|
const slotDef = (def?.slots || []).find((s) => s.key === slot);
|
|
const rawName = item.name !== undefined && String(item.name).trim() !== ''
|
|
? String(item.name).trim()
|
|
: slotDef?.defaultName || '';
|
|
const sanitized = sanitizeVariableName(rawName);
|
|
return {
|
|
slot,
|
|
name: sanitized || slotDef?.defaultName || slot
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const buildSmsParameters = (variables, valuesBySlot = {}) => {
|
|
return normalizeVariablesInput(variables, null)
|
|
.map((item) => {
|
|
const name = sanitizeVariableName(item?.name);
|
|
if (!name) return null;
|
|
const slot = item?.slot;
|
|
const value = slot ? valuesBySlot?.[slot] : '';
|
|
return {
|
|
name,
|
|
value: String(value ?? '')
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
};
|
|
|
|
module.exports = {
|
|
SMS_TEMPLATE_DEFS,
|
|
SMS_TEMPLATE_KEYS,
|
|
envFallbackFor,
|
|
sanitizeTemplateId,
|
|
sanitizeVariableName,
|
|
defaultVariablesList,
|
|
emptyTemplateEntry,
|
|
normalizeStoredEntry,
|
|
parseIncomingVariables,
|
|
mergeTemplateEntry,
|
|
resolveVariablesList,
|
|
buildSmsParameters
|
|
};
|