Files
gameno-api/components/settings/settingService.js
T
kavehhn 72be01fee3 feat: add messaging toggles, password reset SMS, and payment discounts
Allow SuperAdmin to disable SMS, email, and bot from dashboard settings on top of env flags. Add admin password reset with credentials SMS, plus payment discounts, notes, and payable amount handling.
2026-08-16 06:58:08 +03:30

169 lines
5.1 KiB
JavaScript

'use strict';
const AppError = require('../../utils/AppError');
const { Setting, SETTINGS_KEY } = require('./settingModel');
const {
SMS_TEMPLATE_DEFS,
envFallbackFor,
emptyTemplateEntry,
normalizeStoredEntry,
resolveVariablesList,
mergeTemplateEntry
} = require('./smsTemplates');
const { parseIncomingMessaging } = require('../../utils/messagingChannels');
const {
getPublicMessaging,
invalidateMessagingCache
} = require('./messagingFlags');
const emptyTemplateMap = () => {
const map = {};
SMS_TEMPLATE_DEFS.forEach((def) => {
map[def.key] = emptyTemplateEntry(def);
});
return map;
};
const readStoredMap = (doc) => {
const stored = doc?.smsTemplates || {};
if (stored instanceof Map) {
return Object.fromEntries(stored.entries());
}
return { ...stored };
};
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) => {
const entry = normalizeStoredEntry(storedMap[def.key], def);
const variables = resolveVariablesList(def, entry.variables);
return {
key: def.key,
label: def.label,
templateId: resolveTemplateId(entry, def),
availableSlots: (def.slots || []).map((slot) => ({
slot: slot.key,
label: slot.label,
defaultName: slot.defaultName
})),
variables: variables.map((v) => {
const slotDef = (def.slots || []).find((s) => s.key === v.slot);
return {
slot: v.slot,
label: slotDef?.label || v.slot,
name: v.name
};
})
};
});
};
const getSettings = async () => {
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
const storedMap = readStoredMap(doc);
return {
smsTemplates: toPublicTemplates(storedMap),
messaging: getPublicMessaging(doc)
};
};
const getSmsTemplate = async (key) => {
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key);
if (!def) {
return { templateId: '', variables: [] };
}
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
const entry = normalizeStoredEntry(readStoredMap(doc)[key], def);
return {
templateId: resolveTemplateId(entry, def),
variables: resolveVariablesList(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 existing = await Setting.findOne({ key: SETTINGS_KEY });
const doc = existing || new Setting({ key: SETTINGS_KEY });
const hasTemplatePayload = body.smsTemplates !== undefined;
if (hasTemplatePayload) {
const incoming = body.smsTemplates || {};
const incomingMap = Array.isArray(incoming)
? Object.fromEntries(incoming.map((item) => [item.key, item]))
: incoming;
const storedMap = existing ? readStoredMap(existing.toObject ? existing.toObject() : existing) : emptyTemplateMap();
const nextMap = {};
for (const def of SMS_TEMPLATE_DEFS) {
const hasIncoming = Object.prototype.hasOwnProperty.call(incomingMap, def.key);
const incomingEntry = hasIncoming ? parseIncomingEntry(incomingMap[def.key]) : null;
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 (err.code === 'INVALID_VARIABLE_NAME') {
throw new AppError(
'VALIDATION_FAILED',
{ field: err.field || def.key },
`نام متغیر «${err.rawName}» برای ${def.label} نامعتبر است`
);
}
throw err;
}
}
doc.set('smsTemplates', JSON.parse(JSON.stringify(nextMap)));
doc.markModified('smsTemplates');
} else if (!existing) {
doc.set('smsTemplates', emptyTemplateMap());
}
const incomingMessaging = parseIncomingMessaging(body);
if (incomingMessaging) {
if (incomingMessaging.smsEnabled !== undefined) doc.smsEnabled = incomingMessaging.smsEnabled;
if (incomingMessaging.emailEnabled !== undefined) doc.emailEnabled = incomingMessaging.emailEnabled;
if (incomingMessaging.botEnabled !== undefined) doc.botEnabled = incomingMessaging.botEnabled;
}
await doc.save();
invalidateMessagingCache();
const saved = await Setting.findOne({ key: SETTINGS_KEY }).lean();
return {
smsTemplates: toPublicTemplates(readStoredMap(saved)),
messaging: getPublicMessaging(saved)
};
};
module.exports = {
getSettings,
saveSettings,
getSmsTemplate,
getSmsTemplateId
};