diff --git a/components/settings/settingService.js b/components/settings/settingService.js index 382489e..a47af4c 100644 --- a/components/settings/settingService.js +++ b/components/settings/settingService.js @@ -49,13 +49,19 @@ const resolveTemplateId = (entry, def) => { return envFallbackFor(def); }; -const toPublicTemplates = (storedMap) => { +const toPublicTemplates = (storedMap, notificationMap = {}) => { return SMS_TEMPLATE_DEFS.map((def) => { const entry = normalizeStoredEntry(storedMap[def.key], def); const variables = resolveVariablesList(def, entry.variables); + const notifyAction = notificationMap[def.key]; + const isEnabled = notifyAction && notifyAction.sms !== undefined + ? Boolean(notifyAction.sms) + : entry.enabled; + return { key: def.key, label: def.label, + enabled: isEnabled, templateId: resolveTemplateId(entry, def), availableSlots: (def.slots || []).map((slot) => ({ slot: slot.key, @@ -98,8 +104,9 @@ const getSettings = async () => { } const storedMap = readStoredMap(doc); + const notificationMap = readNotificationStoredMap(doc); return { - smsTemplates: toPublicTemplates(storedMap), + smsTemplates: toPublicTemplates(storedMap, notificationMap), messaging: getPublicMessaging(doc), notificationSettings: await getPublicNotificationSettings(doc) }; @@ -108,11 +115,18 @@ const getSettings = async () => { const getSmsTemplate = async (key) => { const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key); if (!def) { - return { templateId: '', variables: [] }; + return { templateId: '', variables: [], enabled: true }; } const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean(); const entry = normalizeStoredEntry(readStoredMap(doc)[key], def); + const notificationStored = readNotificationStoredMap(doc); + const notifyAction = notificationStored[key]; + const isEnabled = notifyAction && notifyAction.sms !== undefined + ? Boolean(notifyAction.sms) + : entry.enabled; + return { + enabled: isEnabled, templateId: resolveTemplateId(entry, def), variables: resolveVariablesList(def, entry.variables) }; @@ -126,10 +140,11 @@ const getSmsTemplateId = async (key) => { const parseIncomingEntry = (raw) => { if (raw == null) return null; if (typeof raw === 'string' || typeof raw === 'number') { - return { templateId: raw, variables: undefined }; + return { templateId: raw, variables: undefined, enabled: undefined }; } if (typeof raw !== 'object') return null; return { + enabled: raw.enabled !== undefined ? Boolean(raw.enabled) : undefined, templateId: raw.templateId, variables: raw.variables }; @@ -172,6 +187,23 @@ const saveSettings = async (body = {}) => { doc.set('smsTemplates', JSON.parse(JSON.stringify(nextMap))); doc.markModified('smsTemplates'); + + // Synchronize smsEnabled to notificationSettings + const storedNotificationMap = existing + ? readNotificationStoredMap(existing.toObject ? existing.toObject() : existing) + : emptyNotificationSettingsMap(); + const updatedNotificationMap = { ...storedNotificationMap }; + for (const def of SMS_TEMPLATE_DEFS) { + if (nextMap[def.key] && nextMap[def.key].enabled !== undefined) { + const currentAction = updatedNotificationMap[def.key] || { sms: true, email: false, bot: false }; + updatedNotificationMap[def.key] = { + ...currentAction, + sms: Boolean(nextMap[def.key].enabled) + }; + } + } + doc.set('notificationSettings', JSON.parse(JSON.stringify(mergeNotificationSettings(updatedNotificationMap)))); + doc.markModified('notificationSettings'); } else if (!existing) { doc.set('smsTemplates', emptyTemplateMap()); } @@ -191,7 +223,24 @@ const saveSettings = async (body = {}) => { const nextNotificationMap = mergeNotificationSettings(storedNotificationMap, incomingNotificationSettings); doc.set('notificationSettings', JSON.parse(JSON.stringify(nextNotificationMap))); doc.markModified('notificationSettings'); - } else if (!existing) { + + // Synchronize enabled flag in smsTemplates + const storedSmsMap = existing + ? readStoredMap(existing.toObject ? existing.toObject() : existing) + : emptyTemplateMap(); + const updatedSmsMap = { ...storedSmsMap }; + for (const def of SMS_TEMPLATE_DEFS) { + if (nextNotificationMap[def.key] && nextNotificationMap[def.key].sms !== undefined) { + const currentTemplate = normalizeStoredEntry(updatedSmsMap[def.key], def); + updatedSmsMap[def.key] = { + ...currentTemplate, + enabled: Boolean(nextNotificationMap[def.key].sms) + }; + } + } + doc.set('smsTemplates', JSON.parse(JSON.stringify(updatedSmsMap))); + doc.markModified('smsTemplates'); + } else if (!existing && !hasTemplatePayload) { doc.set('notificationSettings', emptyNotificationSettingsMap()); } @@ -200,8 +249,9 @@ const saveSettings = async (body = {}) => { invalidateNotificationSettingsCache(); const saved = await Setting.findOne({ key: SETTINGS_KEY }).lean(); + const finalNotificationMap = readNotificationStoredMap(saved); return { - smsTemplates: toPublicTemplates(readStoredMap(saved)), + smsTemplates: toPublicTemplates(readStoredMap(saved), finalNotificationMap), messaging: getPublicMessaging(saved), notificationSettings: await getPublicNotificationSettings(saved) }; diff --git a/components/settings/smsTemplates.js b/components/settings/smsTemplates.js index 5236dfc..dec1a5a 100644 --- a/components/settings/smsTemplates.js +++ b/components/settings/smsTemplates.js @@ -148,6 +148,7 @@ const SMS_TEMPLATE_DEFS = [ key: 'classRequestApproved', label: 'تأیید درخواست تشکیل کلاس', envKey: 'SMS_TEMPLATE_CLASS_REQUEST_APPROVED', + defaultEnabled: false, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' }, { key: 'courseName', label: 'نام دوره', defaultName: 'courseName' }, @@ -159,6 +160,7 @@ const SMS_TEMPLATE_DEFS = [ key: 'classRequestRejected', label: 'رد درخواست ثبت‌نام', envKey: 'SMS_TEMPLATE_CLASS_REQUEST_REJECTED', + defaultEnabled: false, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' }, { key: 'courseName', label: 'نام دوره', defaultName: 'courseName' }, @@ -171,6 +173,7 @@ const SMS_TEMPLATE_DEFS = [ key: 'pendingRegistration', label: 'دریافت درخواست ثبت‌نام', envKey: 'SMS_TEMPLATE_PENDING_REGISTRATION', + defaultEnabled: false, slots: [ { key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' }, { key: 'className', label: 'نام کلاس', defaultName: 'className' }, @@ -225,6 +228,7 @@ const defaultVariablesList = (def) => { }; const emptyTemplateEntry = (def) => ({ + enabled: def ? (def.defaultEnabled !== false) : true, templateId: '', variables: defaultVariablesList(def) }); @@ -266,20 +270,24 @@ const normalizeVariablesInput = (rawVariables, 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) : [] }; @@ -289,7 +297,12 @@ const normalizeStoredEntry = (raw, def) => { ? 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 }; @@ -304,8 +317,13 @@ 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) { @@ -336,6 +354,7 @@ const mergeTemplateEntry = (def, storedRaw, incomingRaw = null) => { } return { + enabled, templateId, variables: resolveVariablesList(def, variables) }; diff --git a/components/settings/smsTemplates.test.js b/components/settings/smsTemplates.test.js index 1db975d..f6c201d 100644 --- a/components/settings/smsTemplates.test.js +++ b/components/settings/smsTemplates.test.js @@ -92,4 +92,27 @@ describe('SMS template variables', () => { assert.ok(slots.includes(slot), `sessionHolding is missing slot ${slot}`); } }); + + it('normalizes and merges enabled property for SMS templates', () => { + const stored = normalizeStoredEntry({ + enabled: false, + templateId: '123', + variables: threeInvoiceVars + }, invoiceDef); + assert.equal(stored.enabled, false); + + const mergedToTrue = mergeTemplateEntry( + invoiceDef, + stored, + { enabled: true } + ); + assert.equal(mergedToTrue.enabled, true); + + const mergedToFalse = mergeTemplateEntry( + invoiceDef, + mergedToTrue, + { enabled: false } + ); + assert.equal(mergedToFalse.enabled, false); + }); }); diff --git a/utils/senders/smsMessages.js b/utils/senders/smsMessages.js index 8f63788..06ad894 100644 --- a/utils/senders/smsMessages.js +++ b/utils/senders/smsMessages.js @@ -6,6 +6,7 @@ const { recordAndSend } = require('./notificationRecorder'); const { getSmsTemplate } = require('../../components/settings/settingService'); const { buildSmsParameters } = require('../../components/settings/smsTemplates'); const User = require('../../components/users/userModel'); +const logger = require('../logger'); const resolveUserIdByPhone = async (phoneNumber) => { if (!phoneNumber) return null; @@ -32,7 +33,11 @@ const sendTemplateSms = async ({ slotValues = {} }) => { const resolvedUserId = userId || await resolveUserIdByPhone(receiver); - const { templateId, variables } = await getSmsTemplate(templateKey); + const { templateId, variables, enabled } = await getSmsTemplate(templateKey); + if (enabled === false) { + logger.info(`[SMS] Template ${templateKey} is disabled. Skipping SMS send to ${receiver}.`); + return { success: true, skipped: true, reason: 'template_disabled' }; + } return recordAndSend({ userId: resolvedUserId, channel: 'sms',