fix(settings): persist every SMS template variable on save

Mongoose Mixed updates were dropping extra invoiceCreated variables after a successful save.
This commit is contained in:
2026-08-15 22:55:54 +03:30
parent d3190a8729
commit 366cd41bb7
5 changed files with 176 additions and 93 deletions
+22 -59
View File
@@ -5,11 +5,10 @@ const { Setting, SETTINGS_KEY } = require('./settingModel');
const {
SMS_TEMPLATE_DEFS,
envFallbackFor,
sanitizeTemplateId,
sanitizeVariableName,
emptyTemplateEntry,
normalizeStoredEntry,
resolveVariablesList
resolveVariablesList,
mergeTemplateEntry
} = require('./smsTemplates');
const emptyTemplateMap = () => {
@@ -103,75 +102,39 @@ const saveSettings = async (body = {}) => {
? Object.fromEntries(incoming.map((item) => [item.key, item]))
: incoming;
const existing = await Setting.findOne({ key: SETTINGS_KEY }).lean();
const storedMap = existing ? readStoredMap(existing) : emptyTemplateMap();
const existing = await Setting.findOne({ key: SETTINGS_KEY });
const storedMap = existing ? readStoredMap(existing.toObject ? existing.toObject() : existing) : emptyTemplateMap();
const nextMap = {};
for (const def of SMS_TEMPLATE_DEFS) {
const storedEntry = normalizeStoredEntry(storedMap[def.key], def);
const hasIncoming = Object.prototype.hasOwnProperty.call(incomingMap, 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 }, `شناسه قالب پیامک برای ${def.label} نامعتبر است`);
}
templateId = sanitized;
try {
nextMap[def.key] = mergeTemplateEntry(def, storedMap[def.key], incomingEntry);
} catch (err) {
if (err.code === 'INVALID_TEMPLATE_ID') {
throw new AppError('VALIDATION_FAILED', { field: def.key }, `شناسه قالب پیامک برای ${def.label} نامعتبر است`);
}
if (incomingEntry.variables !== undefined) {
let rawList = [];
if (Array.isArray(incomingEntry.variables)) {
rawList = incomingEntry.variables;
} else if (incomingEntry.variables && typeof incomingEntry.variables === 'object') {
rawList = Object.entries(incomingEntry.variables).map(([slot, name]) => ({ slot, name }));
}
const validVars = [];
for (const item of rawList) {
if (!item || typeof item !== 'object') continue;
const slot = String(item.slot || '').trim();
if (!slot) continue;
const rawName = String(item.name || '').trim();
const sanitized = sanitizeVariableName(rawName);
if (sanitized === null) {
throw new AppError(
'VALIDATION_FAILED',
{ field: `${def.key}.${slot}` },
`نام متغیر «${rawName}» برای ${def.label} نامعتبر است`
);
}
const slotDef = (def?.slots || []).find((s) => s.key === slot);
validVars.push({
slot,
name: sanitized || slotDef?.defaultName || slot
});
}
variables = validVars;
if (err.code === 'INVALID_VARIABLE_NAME') {
throw new AppError(
'VALIDATION_FAILED',
{ field: err.field || def.key },
`نام متغیر «${err.rawName}» برای ${def.label} نامعتبر است`
);
}
} else if (!storedEntry.templateId) {
templateId = envFallbackFor(def);
throw err;
}
nextMap[def.key] = {
templateId,
variables: resolveVariablesList(def, variables)
};
}
const doc = await Setting.findOneAndUpdate(
{ key: SETTINGS_KEY },
{ $set: { smsTemplates: nextMap } },
{ upsert: true, new: true, setDefaultsOnInsert: true }
).lean();
const doc = existing || new Setting({ key: SETTINGS_KEY });
doc.set('smsTemplates', JSON.parse(JSON.stringify(nextMap)));
doc.markModified('smsTemplates');
await doc.save();
const saved = await Setting.findOne({ key: SETTINGS_KEY }).lean();
return {
smsTemplates: toPublicTemplates(readStoredMap(doc))
smsTemplates: toPublicTemplates(readStoredMap(saved))
};
};