'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: '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: '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' } ] } ]; 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 };