feat: let SuperAdmin rename SMS template variable names

Store custom sms.ir parameter names per template slot so outbound messages match the panel placeholders.
This commit is contained in:
2026-08-15 18:43:24 +03:30
parent 3160f07873
commit 7ebf9409f0
3 changed files with 203 additions and 52 deletions
+90 -23
View File
@@ -5,13 +5,17 @@ const { Setting, SETTINGS_KEY } = require('./settingModel');
const {
SMS_TEMPLATE_DEFS,
envFallbackFor,
sanitizeTemplateId
sanitizeTemplateId,
sanitizeVariableName,
emptyTemplateEntry,
normalizeStoredEntry,
resolveVariableMap
} = require('./smsTemplates');
const emptyTemplateMap = () => {
const map = {};
SMS_TEMPLATE_DEFS.forEach((def) => {
map[def.key] = '';
map[def.key] = emptyTemplateEntry(def);
});
return map;
};
@@ -24,19 +28,28 @@ const readStoredMap = (doc) => {
return { ...stored };
};
const resolveTemplateId = (storedMap, def) => {
const fromDb = String(storedMap[def.key] || '').trim();
const resolveTemplateId = (entry, def) => {
const fromDb = String(entry?.templateId || '').trim();
if (fromDb) return fromDb;
return envFallbackFor(def);
};
const toPublicTemplates = (storedMap) => {
return SMS_TEMPLATE_DEFS.map((def) => ({
key: def.key,
label: def.label,
variables: def.variables,
templateId: resolveTemplateId(storedMap, def)
}));
return SMS_TEMPLATE_DEFS.map((def) => {
const entry = normalizeStoredEntry(storedMap[def.key]);
const variables = resolveVariableMap(def, entry.variables);
return {
key: def.key,
label: def.label,
templateId: resolveTemplateId(entry, def),
variables: (def.slots || []).map((slot) => ({
slot: slot.key,
label: slot.label,
defaultName: slot.defaultName,
name: variables[slot.key]
}))
};
});
};
const getSettings = async () => {
@@ -47,17 +60,40 @@ const getSettings = async () => {
};
};
const getSmsTemplateId = async (key) => {
const getSmsTemplate = async (key) => {
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key);
if (!def) return '';
if (!def) {
return { templateId: '', variables: {} };
}
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
return resolveTemplateId(readStoredMap(doc), def);
const entry = normalizeStoredEntry(readStoredMap(doc)[key]);
return {
templateId: resolveTemplateId(entry, def),
variables: resolveVariableMap(def, entry.variables)
};
};
const getSmsTemplateId = async (key) => {
const template = await getSmsTemplate(key);
return template.templateId;
};
const parseIncomingEntry = (raw) => {
if (raw == null) return null;
if (typeof raw === 'string' || typeof raw === 'number') {
return { templateId: raw, variables: undefined };
}
if (typeof raw !== 'object') return null;
return {
templateId: raw.templateId,
variables: raw.variables
};
};
const saveSettings = async (body = {}) => {
const incoming = body.smsTemplates || {};
const incomingMap = Array.isArray(incoming)
? Object.fromEntries(incoming.map((item) => [item.key, item.templateId]))
? Object.fromEntries(incoming.map((item) => [item.key, item]))
: incoming;
const existing = await Setting.findOne({ key: SETTINGS_KEY });
@@ -65,18 +101,48 @@ const saveSettings = async (body = {}) => {
const nextMap = emptyTemplateMap();
for (const def of SMS_TEMPLATE_DEFS) {
const storedEntry = normalizeStoredEntry(storedMap[def.key]);
const hasIncoming = Object.prototype.hasOwnProperty.call(incomingMap, def.key);
if (hasIncoming) {
const sanitized = sanitizeTemplateId(incomingMap[def.key]);
if (sanitized === null) {
throw new AppError('VALIDATION_FAILED', { field: def.key }, `Invalid SMS template ID for ${def.key}`);
const incomingEntry = hasIncoming ? parseIncomingEntry(incomingMap[def.key]) : null;
let templateId = storedEntry.templateId || envFallbackFor(def);
let variables = { ...storedEntry.variables };
if (incomingEntry) {
if (incomingEntry.templateId !== undefined) {
const sanitized = sanitizeTemplateId(incomingEntry.templateId);
if (sanitized === null) {
throw new AppError('VALIDATION_FAILED', { field: def.key }, `Invalid SMS template ID for ${def.key}`);
}
templateId = sanitized;
}
nextMap[def.key] = sanitized;
} else if (storedMap[def.key]) {
nextMap[def.key] = String(storedMap[def.key]).trim();
} else {
nextMap[def.key] = envFallbackFor(def);
if (incomingEntry.variables && typeof incomingEntry.variables === 'object') {
const incomingVars = Array.isArray(incomingEntry.variables)
? Object.fromEntries(incomingEntry.variables.map((item) => [item.slot, item.name]))
: incomingEntry.variables;
(def.slots || []).forEach((slot) => {
if (!Object.prototype.hasOwnProperty.call(incomingVars, slot.key)) return;
const sanitized = sanitizeVariableName(incomingVars[slot.key]);
if (sanitized === null) {
throw new AppError(
'VALIDATION_FAILED',
{ field: `${def.key}.${slot.key}` },
`Invalid SMS variable name for ${def.key}.${slot.key}`
);
}
variables[slot.key] = sanitized || slot.defaultName;
});
}
} else if (!storedEntry.templateId) {
templateId = envFallbackFor(def);
}
nextMap[def.key] = {
templateId,
variables: resolveVariableMap(def, variables)
};
}
const doc = await Setting.findOneAndUpdate(
@@ -93,5 +159,6 @@ const saveSettings = async (body = {}) => {
module.exports = {
getSettings,
saveSettings,
getSmsTemplate,
getSmsTemplateId
};