feat: send account and invoice SMS from stored template IDs

Move sms.ir template IDs out of env into settings so SuperAdmin can manage them, and record every outbound SMS status in notifications.
This commit is contained in:
2026-08-15 18:37:44 +03:30
parent 23edffba6a
commit 3160f07873
13 changed files with 261 additions and 22 deletions
+97
View File
@@ -0,0 +1,97 @@
'use strict';
const AppError = require('../../utils/AppError');
const { Setting, SETTINGS_KEY } = require('./settingModel');
const {
SMS_TEMPLATE_DEFS,
envFallbackFor,
sanitizeTemplateId
} = require('./smsTemplates');
const emptyTemplateMap = () => {
const map = {};
SMS_TEMPLATE_DEFS.forEach((def) => {
map[def.key] = '';
});
return map;
};
const readStoredMap = (doc) => {
const stored = doc?.smsTemplates || {};
if (stored instanceof Map) {
return Object.fromEntries(stored.entries());
}
return { ...stored };
};
const resolveTemplateId = (storedMap, def) => {
const fromDb = String(storedMap[def.key] || '').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)
}));
};
const getSettings = async () => {
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
const storedMap = readStoredMap(doc);
return {
smsTemplates: toPublicTemplates(storedMap)
};
};
const getSmsTemplateId = async (key) => {
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key);
if (!def) return '';
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
return resolveTemplateId(readStoredMap(doc), def);
};
const saveSettings = async (body = {}) => {
const incoming = body.smsTemplates || {};
const incomingMap = Array.isArray(incoming)
? Object.fromEntries(incoming.map((item) => [item.key, item.templateId]))
: incoming;
const existing = await Setting.findOne({ key: SETTINGS_KEY });
const storedMap = existing ? readStoredMap(existing) : emptyTemplateMap();
const nextMap = emptyTemplateMap();
for (const def of SMS_TEMPLATE_DEFS) {
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}`);
}
nextMap[def.key] = sanitized;
} else if (storedMap[def.key]) {
nextMap[def.key] = String(storedMap[def.key]).trim();
} else {
nextMap[def.key] = envFallbackFor(def);
}
}
const doc = await Setting.findOneAndUpdate(
{ key: SETTINGS_KEY },
{ $set: { smsTemplates: nextMap } },
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
return {
smsTemplates: toPublicTemplates(readStoredMap(doc))
};
};
module.exports = {
getSettings,
saveSettings,
getSmsTemplateId
};