// /components/settings/smsTemplates.js 'use strict'; const SMS_TEMPLATE_DEFS = [ { key: 'sessionHolding', label: 'کلاس طبق برنامه', category: 'اطلاع‌رسانی', defaultTemplateId: '720661', envKey: 'SMS_TEMPLATE_SESSION_HOLDING', defaultText: ` #FULLNAME# عزیز، «#TOPIC#» کلاس #CLASSNAME# در تاریخ #SESSIONDATE# و ساعت #CLASSTIME# طبق برنامه برگزار خواهد شد. آموزشگاه گام نو game-no.ir`, 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: 'certificateIssued', label: 'صدور گواهی', category: 'اطلاع‌رسانی', defaultTemplateId: '854493', envKey: 'SMS_TEMPLATE_CERTIFICATE_ISSUED', defaultText: ` #FULLNAME# عزیز، گواهی «#CERTIFICATETITLE#» شما، برای دوره #COURSENAME# صادر شد. جهت مشاهده و دانلود گواهی به پروفایل خود در وبسایت آموزشگاه مراجعه کنید. آموزشگاه گام نو game-no.ir`, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' }, { key: 'certificateTitle', label: 'عنوان گواهینامه', defaultName: 'CERTIFICATETITLE' }, { key: 'courseName', label: 'نام دوره', defaultName: 'COURSENAME' }, { key: 'certificateCode', label: 'کد گواهینامه', defaultName: 'certificateCode' }, { key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' } ] }, { key: 'sessionCancelled', label: 'لغو کلاس', category: 'اطلاع‌رسانی', defaultTemplateId: '174248', envKey: 'SMS_TEMPLATE_SESSION_CANCELLED', defaultText: ` #FULLNAME# عزیز، جلسه «#TOPIC#» کلاس #CLASSNAME# در تاریخ #SESSIONDATE# برگزار نخواهد شد. آموزشگاه گام نو game-no.ir`, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' }, { key: 'topic', label: 'موضوع جلسه', defaultName: 'TOPIC' }, { key: 'className', label: 'نام کلاس', defaultName: 'CLASSNAME' }, { key: 'sessionDate', label: 'تاریخ جلسه', defaultName: 'SESSIONDATE' }, { key: 'reason', label: 'دلیل لغو', defaultName: 'reason' }, { key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' }, { key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' } ] }, { key: 'transactionRecorded', label: 'ثبت تراکنش', category: 'اطلاع‌رسانی', defaultTemplateId: '580983', envKey: 'SMS_TEMPLATE_TRANSACTION_RECORDED', defaultText: ` #FULLNAME# عزیز، تراکنش #TRANSACTIONCODE# به مبلغ #AMOUNT# تومان ثبت شد. صورتحساب: #INVOICECODE# — رسید: #RECEIPTNUMBER# آموزشگاه گام نو game-no.ir`, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' }, { key: 'transactionCode', label: 'کد تراکنش', defaultName: 'TRANSACTIONCODE' }, { key: 'amount', label: 'مبلغ', defaultName: 'AMOUNT' }, { key: 'invoiceCode', label: 'کد صورتحساب', defaultName: 'INVOICECODE' }, { key: 'receiptNumber', label: 'شماره رسید', defaultName: 'RECEIPTNUMBER' }, { key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' } ] }, { key: 'paymentReminder', label: 'یادآوری پرداخت', category: 'اطلاع‌رسانی', defaultTemplateId: '357550', envKey: 'SMS_TEMPLATE_PAYMENT_REMINDER', defaultText: ` #FULLNAME# عزیز، یادآوری پرداخت: صورتحساب #INVOICECODE# — مبلغ باقیمانده: #AMOUNT# تومان سررسید: #DUEDATE# دوره: #COURSE# آموزشگاه گام نو game-no.ir`, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' }, { key: 'invoiceCode', label: 'کد صورتحساب', defaultName: 'INVOICECODE' }, { key: 'amount', label: 'مبلغ باقیمانده', defaultName: 'AMOUNT' }, { key: 'dueDate', label: 'تاریخ سررسید', defaultName: 'DUEDATE' }, { key: 'course', label: 'نام دوره', defaultName: 'COURSE' }, { key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' } ] }, { key: 'paymentStatusChanged', label: 'وضعیت صورتحساب', category: 'وضعیت سفارش', defaultTemplateId: '270174', envKey: 'SMS_TEMPLATE_PAYMENT_STATUS_CHANGED', defaultText: ` #FULLNAME# عزیز، وضعیت صورتحساب #INVOICECODE# به «#STATUS#» تغییر کرد. مبلغ: #AMOUNT# تومان — دوره: #COURSE# آموزشگاه گام نو game-no.ir`, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' }, { key: 'invoiceCode', label: 'کد صورتحساب', defaultName: 'INVOICECODE' }, { key: 'status', label: 'وضعیت', defaultName: 'STATUS' }, { key: 'amount', label: 'مبلغ', defaultName: 'AMOUNT' }, { key: 'course', label: 'نام دوره', defaultName: 'COURSE' }, { key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' } ] }, { key: 'accountCreated', label: 'اطلاعات اکانت', category: 'اطلاع‌رسانی', defaultTemplateId: '250973', envKey: 'SMS_TEMPLATE_ACCOUNT_CREATED', defaultText: ` کارآموز گرامی، یک حساب کاربری برای شما در وبسایت آموزشگاه گام نو ایجاد شد. نام کاربری: #USER# رمز عبور: #PASSWORD# game-no.ir`, 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: 'invoiceCreated', label: 'پرداخت', category: 'اطلاع‌رسانی', defaultTemplateId: '580892', envKey: 'SMS_TEMPLATE_INVOICE_CREATED', defaultText: ` کارآموز عزیز، #FULLNAME#، یک صورتحساب به مبلغ #PAYMENT_PRICE# تومان، بابت دوره ی #COURSE# برای شما ایجاد شده است. آموزشگاه گام نو game-no.ir`, 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: 'classRegistered', label: 'ثبت نام دوره', category: 'اطلاع‌رسانی', defaultTemplateId: '910210', envKey: 'SMS_TEMPLATE_CLASS_REGISTERED', defaultText: ` کارآموز عزیز، #FULLNAME#، ثبت نام شما در دوره #CLASS# انجام شد. هر هفته #CLASSDAYS#، #CLASSTIME# آموزشگاه گام نو game-no.ir`, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' }, { key: 'className', label: 'نام کلاس', defaultName: 'CLASS' }, { key: 'classDays', label: 'روزهای برگزاری', defaultName: 'CLASSDAYS' }, { key: 'courseTime', label: 'ساعت دوره', defaultName: 'CLASSTIME' }, { key: 'courseName', label: 'نام دوره', defaultName: 'courseName' }, { key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }, { key: 'classStartDate', label: 'تاریخ شروع کلاس', defaultName: 'classStartDate' }, { key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' } ] }, { key: 'classPlanProfessor', label: 'برنامه کلاس به استاد', category: 'اطلاع‌رسانی', defaultTemplateId: '', envKey: 'SMS_TEMPLATE_CLASS_PLAN_PROFESSOR', defaultText: `با سلام و وقت بخیر، استاد #professorName#، برنامه کلاس #className# شما به شرح زیر می باشد: #classDays#، #classTimes# از #classStartDate# الی #classEndDate#`, slots: [ { key: 'professorName', label: 'نام استاد', defaultName: 'professorName' }, { key: 'className', label: 'نام کلاس', defaultName: 'className' }, { key: 'classDays', label: 'روزهای برگزاری', defaultName: 'classDays' }, { key: 'classTimes', label: 'ساعت برگزاری', defaultName: 'classTimes' }, { key: 'classStartDate', label: 'تاریخ شروع', defaultName: 'classStartDate' }, { key: 'classEndDate', label: 'تاریخ پایان', defaultName: 'classEndDate' }, { key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' } ] }, { key: 'passwordReset', label: 'بازنشانی رمز عبور', category: 'اطلاع‌رسانی', defaultTemplateId: '250973', envKey: 'SMS_TEMPLATE_PASSWORD_RESET', defaultText: ` کارآموز گرامی، رمز عبور حساب کاربری شما در وبسایت آموزشگاه گام نو بازنشانی شد. نام کاربری: #USER# رمز عبور: #PASSWORD# game-no.ir`, 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: 'classReminder', label: 'یادآوری کلاس', category: 'اطلاع‌رسانی', defaultTemplateId: '', envKey: 'SMS_TEMPLATE_CLASS_REMINDER', defaultText: ` #FULLNAME# عزیز، یادآوری کلاس «#CLASSNAME#» در تاریخ #SESSIONDATE# ساعت #CLASSTIME#. مکان: #PLACE# آموزشگاه گام نو game-no.ir`, slots: [ { key: 'className', label: 'نام کلاس', defaultName: 'CLASSNAME' }, { key: 'topic', label: 'موضوع جلسه', defaultName: 'topic' }, { 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: 'classRequestApproved', label: 'تأیید درخواست تشکیل کلاس', category: 'ثبت‌نام و درخواست‌ها', defaultTemplateId: '', envKey: 'SMS_TEMPLATE_CLASS_REQUEST_APPROVED', defaultEnabled: false, defaultText: ` #FULLNAME# عزیز، درخواست شما برای تشکیل کلاس #COURSENAME# تأیید شد. آموزشگاه گام نو game-no.ir`, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' }, { key: 'courseName', label: 'نام دوره', defaultName: 'COURSENAME' }, { key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' }, { key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' } ] }, { key: 'classRequestRejected', label: 'رد درخواست ثبت‌نام', category: 'ثبت‌نام و درخواست‌ها', defaultTemplateId: '', envKey: 'SMS_TEMPLATE_CLASS_REQUEST_REJECTED', defaultEnabled: false, defaultText: ` #FULLNAME# عزیز، درخواست ثبت‌نام شما برای دوره #COURSENAME# مورد پذیرش قرار نگرفت. دلیل: #REASON# آموزشگاه گام نو game-no.ir`, 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: 'دریافت درخواست ثبت‌نام', category: 'ثبت‌نام و درخواست‌ها', defaultTemplateId: '', envKey: 'SMS_TEMPLATE_PENDING_REGISTRATION', defaultEnabled: false, defaultText: ` #FULLNAME# عزیز، درخواست ثبت‌نام شما در کلاس #CLASSNAME# با کد پیگیری #REGISTRATIONCODE# دریافت شد. آموزشگاه گام نو game-no.ir`, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' }, { key: 'className', label: 'نام کلاس', defaultName: 'CLASSNAME' }, { key: 'registrationCode', label: 'کد درخواست', defaultName: 'REGISTRATIONCODE' }, { 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 ''; if (/[\r\n\t\0<>"'`]/.test(name)) return null; if (name.length > 100) return null; return name; }; const escapeRegExp = (string) => { return String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; const defaultVariablesList = (def) => { return (def?.slots || []).map((slot) => ({ slot: slot.key, name: slot.defaultName })); }; const emptyTemplateEntry = (def) => ({ enabled: def ? (def.defaultEnabled !== false) : true, templateId: def?.defaultTemplateId || '', category: def?.category || 'اطلاع‌رسانی', text: def?.defaultText || '', 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; const defaultText = def?.defaultText || ''; const defaultTemplateId = def?.defaultTemplateId || ''; const defaultCategory = def?.category || 'اطلاع‌رسانی'; if (raw == null || raw === '') { return { enabled: defaultEnabled, templateId: defaultTemplateId, category: defaultCategory, text: defaultText, variables: def ? defaultVariablesList(def) : [] }; } if (typeof raw === 'string' || typeof raw === 'number') { return { enabled: true, templateId: String(raw), category: defaultCategory, text: defaultText, variables: def ? defaultVariablesList(def) : [] }; } if (typeof raw !== 'object') { return { enabled: defaultEnabled, templateId: defaultTemplateId, category: defaultCategory, text: defaultText, 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; const templateId = raw.templateId != null && String(raw.templateId).trim() !== '' ? String(raw.templateId).trim() : defaultTemplateId; const text = raw.text != null && String(raw.text).trim() !== '' ? String(raw.text) : defaultText; const category = raw.category || defaultCategory; return { enabled, templateId, category, text, 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) || def?.defaultTemplateId || ''; let variables = storedEntry.variables; let enabled = storedEntry.enabled; let text = storedEntry.text || def?.defaultText || ''; let category = storedEntry.category || def?.category || 'اطلاع‌رسانی'; if (incomingRaw) { if (incomingRaw.enabled !== undefined) { enabled = Boolean(incomingRaw.enabled); } if (incomingRaw.text !== undefined) { text = String(incomingRaw.text || ''); } if (incomingRaw.category !== undefined) { category = String(incomingRaw.category || ''); } 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) || def?.defaultTemplateId || ''; } return { enabled, templateId, category, text, 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); }; /** * Renders the exact text of an SMS template by replacing variable placeholders (e.g. #FULLNAME# or #className#) * with the corresponding values provided in slotValues. */ const renderSmsText = (templateText, variables = [], slotValues = {}) => { if (!templateText || typeof templateText !== 'string') return ''; let result = templateText; // 1. Replace defined template variables (mapped by slot and variable name in sms.ir) if (Array.isArray(variables)) { for (const item of variables) { if (!item || !item.name) continue; const varName = String(item.name).trim().replace(/^#+|#+$/g, ''); if (!varName) continue; const slotKey = item.slot; let val = slotKey !== undefined && slotValues[slotKey] !== undefined ? slotValues[slotKey] : ''; if (isPriceSlot(slotKey, varName) && val != null && val !== '') { val = formatPriceValue(val); } const pattern = new RegExp(`#${escapeRegExp(varName)}#`, 'gi'); result = result.replace(pattern, String(val ?? '')); } } // 2. Direct slot replacements (e.g., #professorName#, #className#, #amount#, #fullName#) if (slotValues && typeof slotValues === 'object') { for (const [slotKey, rawVal] of Object.entries(slotValues)) { if (!slotKey) continue; let val = rawVal; if (isPriceSlot(slotKey, slotKey) && val != null && val !== '') { val = formatPriceValue(val); } const pattern = new RegExp(`#${escapeRegExp(slotKey)}#`, 'gi'); result = result.replace(pattern, String(val ?? '')); } } return result; }; module.exports = { SMS_TEMPLATE_DEFS, SMS_TEMPLATE_KEYS, envFallbackFor, sanitizeTemplateId, sanitizeVariableName, defaultVariablesList, emptyTemplateEntry, normalizeStoredEntry, parseIncomingVariables, mergeTemplateEntry, resolveVariablesList, buildSmsParameters, renderSmsText, formatPriceValue };