Expose schedule fields on class and invoice templates so SMS can include Jalali start date, weekdays, and hour range.
261 lines
8.2 KiB
JavaScript
261 lines
8.2 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: '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: 'classReminder',
|
|
label: 'یادآوری کلاس',
|
|
envKey: 'SMS_TEMPLATE_CLASS_REMINDER',
|
|
slots: [
|
|
{ key: 'className', label: 'نام کلاس', defaultName: 'className' },
|
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
|
{ key: 'time', label: 'ساعت', defaultName: 'time' },
|
|
{ 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: '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' }
|
|
]
|
|
}
|
|
];
|
|
|
|
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) => ({
|
|
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) => {
|
|
if (raw == null || raw === '') {
|
|
return {
|
|
templateId: '',
|
|
variables: def ? defaultVariablesList(def) : []
|
|
};
|
|
}
|
|
if (typeof raw === 'string' || typeof raw === 'number') {
|
|
return {
|
|
templateId: String(raw),
|
|
variables: def ? defaultVariablesList(def) : []
|
|
};
|
|
}
|
|
if (typeof raw !== 'object') {
|
|
return {
|
|
templateId: '',
|
|
variables: def ? defaultVariablesList(def) : []
|
|
};
|
|
}
|
|
|
|
const variables = Object.prototype.hasOwnProperty.call(raw, 'variables')
|
|
? normalizeVariablesInput(raw.variables, def)
|
|
: (def ? defaultVariablesList(def) : []);
|
|
|
|
return {
|
|
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;
|
|
|
|
if (incomingRaw) {
|
|
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 {
|
|
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
|
|
};
|