feat: add employee timing system, notification templates, and professor class plan sms
This commit is contained in:
@@ -65,6 +65,15 @@ const NOTIFICATION_ACTION_DEFS = [
|
||||
smsTemplateKey: 'sessionHolding',
|
||||
defaultChannels: { sms: true, email: false, bot: false }
|
||||
},
|
||||
{
|
||||
key: 'classPlanProfessor',
|
||||
label: 'ارسال برنامه کلاس به استاد',
|
||||
description: 'ارسال جزئیات و زمانبندی برگزاری کلاس به استاد',
|
||||
category: 'classes',
|
||||
relatedEvent: 'class.plan_to_professor',
|
||||
smsTemplateKey: 'classPlanProfessor',
|
||||
defaultChannels: { sms: true, email: false, bot: false }
|
||||
},
|
||||
{
|
||||
key: 'invoiceCreated',
|
||||
label: 'ایجاد صورتحساب',
|
||||
|
||||
@@ -61,6 +61,8 @@ const toPublicTemplates = (storedMap, notificationMap = {}) => {
|
||||
return {
|
||||
key: def.key,
|
||||
label: def.label,
|
||||
category: entry.category || def.category || 'اطلاعرسانی',
|
||||
text: entry.text || def.defaultText || '',
|
||||
enabled: isEnabled,
|
||||
templateId: resolveTemplateId(entry, def),
|
||||
availableSlots: (def.slots || []).map((slot) => ({
|
||||
@@ -115,7 +117,7 @@ const getSettings = async () => {
|
||||
const getSmsTemplate = async (key) => {
|
||||
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key);
|
||||
if (!def) {
|
||||
return { templateId: '', variables: [], enabled: true };
|
||||
return { templateId: '', variables: [], enabled: true, text: '', category: 'اطلاعرسانی' };
|
||||
}
|
||||
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
|
||||
const entry = normalizeStoredEntry(readStoredMap(doc)[key], def);
|
||||
@@ -128,6 +130,8 @@ const getSmsTemplate = async (key) => {
|
||||
return {
|
||||
enabled: isEnabled,
|
||||
templateId: resolveTemplateId(entry, def),
|
||||
category: entry.category || def.category || 'اطلاعرسانی',
|
||||
text: entry.text || def.defaultText || '',
|
||||
variables: resolveVariablesList(def, entry.variables)
|
||||
};
|
||||
};
|
||||
@@ -140,12 +144,14 @@ const getSmsTemplateId = async (key) => {
|
||||
const parseIncomingEntry = (raw) => {
|
||||
if (raw == null) return null;
|
||||
if (typeof raw === 'string' || typeof raw === 'number') {
|
||||
return { templateId: raw, variables: undefined, enabled: undefined };
|
||||
return { templateId: raw, variables: undefined, enabled: undefined, text: undefined, category: undefined };
|
||||
}
|
||||
if (typeof raw !== 'object') return null;
|
||||
return {
|
||||
enabled: raw.enabled !== undefined ? Boolean(raw.enabled) : undefined,
|
||||
templateId: raw.templateId,
|
||||
category: raw.category,
|
||||
text: raw.text,
|
||||
variables: raw.variables
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// /components/settings/smsTemplateRender.test.js
|
||||
'use strict';
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { renderSmsText, SMS_TEMPLATE_DEFS } = require('./smsTemplates');
|
||||
const { formatJalaliDate } = require('../../utils/jalaliDate');
|
||||
|
||||
describe('SMS Template Text Rendering', () => {
|
||||
it('renders class plan to professor template correctly with variables', () => {
|
||||
const def = SMS_TEMPLATE_DEFS.find((d) => d.key === 'classPlanProfessor');
|
||||
assert.ok(def);
|
||||
|
||||
const slotValues = {
|
||||
professorName: 'علی رضایی',
|
||||
className: 'برنامهنویسی وب',
|
||||
classDays: 'شنبه و دوشنبه',
|
||||
classTimes: '۱۶:۰۰ الی ۱۸:۰۰',
|
||||
classStartDate: '1405/06/01',
|
||||
classEndDate: '1405/08/15'
|
||||
};
|
||||
|
||||
const rendered = renderSmsText(def.defaultText, def.slots.map(s => ({ slot: s.key, name: s.defaultName })), slotValues);
|
||||
assert.match(rendered, /استاد علی رضایی/);
|
||||
assert.match(rendered, /برنامهنویسی وب/);
|
||||
assert.match(rendered, /شنبه و دوشنبه/);
|
||||
assert.match(rendered, /۱۶:۰۰ الی ۱۸:۰۰/);
|
||||
assert.match(rendered, /1405\/06\/01/);
|
||||
assert.match(rendered, /1405\/08\/15/);
|
||||
});
|
||||
|
||||
it('renders sessionHolding template correctly', () => {
|
||||
const def = SMS_TEMPLATE_DEFS.find((d) => d.key === 'sessionHolding');
|
||||
assert.ok(def);
|
||||
|
||||
const slotValues = {
|
||||
fullName: 'سارا محمدی',
|
||||
topic: 'مقدمهای بر پایگاه داده',
|
||||
className: 'کلاس بکاند',
|
||||
sessionDate: '1405/06/10',
|
||||
classTime: '17:00'
|
||||
};
|
||||
|
||||
const rendered = renderSmsText(def.defaultText, [
|
||||
{ slot: 'fullName', name: 'FULLNAME' },
|
||||
{ slot: 'topic', name: 'TOPIC' },
|
||||
{ slot: 'className', name: 'CLASSNAME' },
|
||||
{ slot: 'sessionDate', name: 'SESSIONDATE' },
|
||||
{ slot: 'classTime', name: 'CLASSTIME' }
|
||||
], slotValues);
|
||||
|
||||
assert.match(rendered, /سارا محمدی عزیز/);
|
||||
assert.match(rendered, /مقدمهای بر پایگاه داده/);
|
||||
assert.match(rendered, /کلاس بکاند/);
|
||||
assert.match(rendered, /1405\/06\/10/);
|
||||
assert.match(rendered, /17:00/);
|
||||
});
|
||||
|
||||
it('formats dates into Jalali correctly', () => {
|
||||
const formatted = formatJalaliDate('2026-08-24T00:00:00.000Z');
|
||||
assert.equal(formatted, '1405/06/02');
|
||||
});
|
||||
});
|
||||
+347
-158
@@ -1,137 +1,17 @@
|
||||
// /components/settings/smsTemplates.js
|
||||
'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: 'برگزاری جلسه طبق برنامه',
|
||||
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' },
|
||||
@@ -144,14 +24,245 @@ const SMS_TEMPLATE_DEFS = [
|
||||
{ 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: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' },
|
||||
{ key: 'courseName', label: 'نام دوره', defaultName: 'COURSENAME' },
|
||||
{ key: 'classCode', label: 'کد کلاس', defaultName: 'classCode' },
|
||||
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
||||
]
|
||||
@@ -159,12 +270,19 @@ const SMS_TEMPLATE_DEFS = [
|
||||
{
|
||||
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: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' },
|
||||
{ key: 'courseName', label: 'نام دوره', defaultName: 'COURSENAME' },
|
||||
{ key: 'reason', label: 'دلیل', defaultName: 'REASON' },
|
||||
{ key: 'registrationCode', label: 'کد درخواست', defaultName: 'registrationCode' },
|
||||
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
||||
]
|
||||
@@ -172,24 +290,18 @@ const SMS_TEMPLATE_DEFS = [
|
||||
{
|
||||
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' }
|
||||
]
|
||||
},
|
||||
{
|
||||
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: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' },
|
||||
{ key: 'className', label: 'نام کلاس', defaultName: 'CLASSNAME' },
|
||||
{ key: 'registrationCode', label: 'کد درخواست', defaultName: 'REGISTRATIONCODE' },
|
||||
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
||||
]
|
||||
}
|
||||
@@ -214,12 +326,15 @@ 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 escapeRegExp = (string) => {
|
||||
return String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
const defaultVariablesList = (def) => {
|
||||
return (def?.slots || []).map((slot) => ({
|
||||
slot: slot.key,
|
||||
@@ -229,7 +344,9 @@ const defaultVariablesList = (def) => {
|
||||
|
||||
const emptyTemplateEntry = (def) => ({
|
||||
enabled: def ? (def.defaultEnabled !== false) : true,
|
||||
templateId: '',
|
||||
templateId: def?.defaultTemplateId || '',
|
||||
category: def?.category || 'اطلاعرسانی',
|
||||
text: def?.defaultText || '',
|
||||
variables: defaultVariablesList(def)
|
||||
});
|
||||
|
||||
@@ -271,10 +388,16 @@ const normalizeVariablesInput = (rawVariables, 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: '',
|
||||
templateId: defaultTemplateId,
|
||||
category: defaultCategory,
|
||||
text: defaultText,
|
||||
variables: def ? defaultVariablesList(def) : []
|
||||
};
|
||||
}
|
||||
@@ -282,13 +405,17 @@ const normalizeStoredEntry = (raw, def) => {
|
||||
return {
|
||||
enabled: true,
|
||||
templateId: String(raw),
|
||||
category: defaultCategory,
|
||||
text: defaultText,
|
||||
variables: def ? defaultVariablesList(def) : []
|
||||
};
|
||||
}
|
||||
if (typeof raw !== 'object') {
|
||||
return {
|
||||
enabled: defaultEnabled,
|
||||
templateId: '',
|
||||
templateId: defaultTemplateId,
|
||||
category: defaultCategory,
|
||||
text: defaultText,
|
||||
variables: def ? defaultVariablesList(def) : []
|
||||
};
|
||||
}
|
||||
@@ -301,9 +428,21 @@ const normalizeStoredEntry = (raw, def) => {
|
||||
? 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: raw.templateId != null ? String(raw.templateId) : '',
|
||||
templateId,
|
||||
category,
|
||||
text,
|
||||
variables
|
||||
};
|
||||
};
|
||||
@@ -315,15 +454,22 @@ const parseIncomingVariables = (rawVariables, def) => {
|
||||
|
||||
const mergeTemplateEntry = (def, storedRaw, incomingRaw = null) => {
|
||||
const storedEntry = normalizeStoredEntry(storedRaw, def);
|
||||
let templateId = storedEntry.templateId || envFallbackFor(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) {
|
||||
@@ -334,7 +480,6 @@ const mergeTemplateEntry = (def, storedRaw, incomingRaw = null) => {
|
||||
}
|
||||
templateId = sanitized;
|
||||
}
|
||||
|
||||
if (incomingRaw.variables !== undefined) {
|
||||
const parsed = parseIncomingVariables(incomingRaw.variables, def) || [];
|
||||
for (const item of parsed) {
|
||||
@@ -350,12 +495,14 @@ const mergeTemplateEntry = (def, storedRaw, incomingRaw = null) => {
|
||||
variables = parsed;
|
||||
}
|
||||
} else if (!storedEntry.templateId) {
|
||||
templateId = envFallbackFor(def);
|
||||
templateId = envFallbackFor(def) || def?.defaultTemplateId || '';
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
templateId,
|
||||
category,
|
||||
text,
|
||||
variables: resolveVariablesList(def, variables)
|
||||
};
|
||||
};
|
||||
@@ -417,6 +564,46 @@ const buildSmsParameters = (variables, valuesBySlot = {}) => {
|
||||
.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,
|
||||
@@ -429,5 +616,7 @@ module.exports = {
|
||||
parseIncomingVariables,
|
||||
mergeTemplateEntry,
|
||||
resolveVariablesList,
|
||||
buildSmsParameters
|
||||
buildSmsParameters,
|
||||
renderSmsText,
|
||||
formatPriceValue
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user