Files
gameno-api/components/settings/settingService.js
T

266 lines
9.2 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 {
emptyNotificationSettingsMap,
mergeNotificationSettings,
parseIncomingNotificationSettings,
NOTIFICATION_ACTION_KEYS
} = require('./notificationActions');
const {
getPublicNotificationSettings,
invalidateNotificationSettingsCache,
readStoredMap: readNotificationStoredMap
} = require('../../utils/notificationSettings');
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, 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,
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 () => {
let doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
if (!doc) {
const created = await Setting.create({
key: SETTINGS_KEY,
smsTemplates: emptyTemplateMap(),
notificationSettings: emptyNotificationSettingsMap()
});
doc = created.toObject();
} else {
const mergedNotifications = mergeNotificationSettings(readNotificationStoredMap(doc));
const storedKeys = Object.keys(readNotificationStoredMap(doc));
const missingAction = NOTIFICATION_ACTION_KEYS.some((key) => !storedKeys.includes(key));
if (missingAction || storedKeys.length === 0) {
await Setting.updateOne(
{ key: SETTINGS_KEY },
{ $set: { notificationSettings: mergedNotifications } }
);
doc = { ...doc, notificationSettings: mergedNotifications };
invalidateNotificationSettingsCache();
}
}
const storedMap = readStoredMap(doc);
const notificationMap = readNotificationStoredMap(doc);
return {
smsTemplates: toPublicTemplates(storedMap, notificationMap),
messaging: getPublicMessaging(doc),
notificationSettings: await getPublicNotificationSettings(doc)
};
};
const getSmsTemplate = async (key) => {
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key);
if (!def) {
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)
};
};
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, enabled: undefined };
}
if (typeof raw !== 'object') return null;
return {
enabled: raw.enabled !== undefined ? Boolean(raw.enabled) : undefined,
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');
// 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());
}
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;
}
const incomingNotificationSettings = parseIncomingNotificationSettings(body.notificationSettings);
if (incomingNotificationSettings !== undefined) {
const storedNotificationMap = existing
? readNotificationStoredMap(existing.toObject ? existing.toObject() : existing)
: emptyNotificationSettingsMap();
const nextNotificationMap = mergeNotificationSettings(storedNotificationMap, incomingNotificationSettings);
doc.set('notificationSettings', JSON.parse(JSON.stringify(nextNotificationMap)));
doc.markModified('notificationSettings');
// 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());
}
await doc.save();
invalidateMessagingCache();
invalidateNotificationSettingsCache();
const saved = await Setting.findOne({ key: SETTINGS_KEY }).lean();
const finalNotificationMap = readNotificationStoredMap(saved);
return {
smsTemplates: toPublicTemplates(readStoredMap(saved), finalNotificationMap),
messaging: getPublicMessaging(saved),
notificationSettings: await getPublicNotificationSettings(saved)
};
};
module.exports = {
getSettings,
saveSettings,
getSmsTemplate,
getSmsTemplateId
};