'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 isPriceSlot = (slot = '', name = '') => { const s = String(slot).toLowerCase(); const n = String(name).toLowerCase(); return s.includes('amount') || s.includes('price') || s.includes('tuition') || s.includes('fee') || n.includes('amount') || n.includes('price') || n.includes('tuition') || n.includes('fee') || n.includes('cost'); }; const formatPriceValue = (val) => { if (val === null || val === undefined || val === '') return ''; const str = String(val).trim(); const rawNum = Number(str.replace(/,/g, '')); if (!Number.isNaN(rawNum) && Number.isFinite(rawNum)) { return rawNum.toLocaleString('en-US'); } return str; }; const buildSmsParameters = (variables, valuesBySlot = {}) => { return normalizeVariablesInput(variables, null) .map((item) => { const name = sanitizeVariableName(item?.name); if (!name) return null; const slot = item?.slot; let value = slot ? valuesBySlot?.[slot] : ''; if (isPriceSlot(slot, name) && value != null && value !== '') { value = formatPriceValue(value); } 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 };