310 lines
11 KiB
JavaScript
310 lines
11 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,
|
|
normalizePhoneForBypass
|
|
} = 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,
|
|
category: entry.category || def.category || 'اطلاعرسانی',
|
|
text: entry.text || def.defaultText || '',
|
|
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 formatBypassNumbers = (list) => {
|
|
if (!Array.isArray(list)) return [];
|
|
return list.map((item) => {
|
|
const raw = item.toObject ? item.toObject() : item;
|
|
return {
|
|
_id: raw._id,
|
|
phoneNumber: raw.phoneNumber,
|
|
label: raw.label || '',
|
|
isActive: raw.isActive !== false,
|
|
createdAt: raw.createdAt
|
|
};
|
|
});
|
|
};
|
|
|
|
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(),
|
|
smsBypassNumbers: []
|
|
});
|
|
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),
|
|
smsBypassNumbers: formatBypassNumbers(doc.smsBypassNumbers)
|
|
};
|
|
};
|
|
|
|
const getSmsTemplate = async (key) => {
|
|
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key);
|
|
if (!def) {
|
|
return { templateId: '', variables: [], enabled: true, text: '', category: 'اطلاعرسانی' };
|
|
}
|
|
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),
|
|
category: entry.category || def.category || 'اطلاعرسانی',
|
|
text: entry.text || def.defaultText || '',
|
|
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, text: undefined, category: undefined };
|
|
}
|
|
if (typeof raw !== 'object') return null;
|
|
return {
|
|
enabled: raw.enabled !== undefined ? Boolean(raw.enabled) : undefined,
|
|
templateId: raw.templateId,
|
|
category: raw.category,
|
|
text: raw.text,
|
|
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());
|
|
}
|
|
|
|
// Handle smsBypassNumbers
|
|
if (Array.isArray(body.smsBypassNumbers)) {
|
|
const nextBypass = body.smsBypassNumbers
|
|
.map((item) => {
|
|
const rawPhone = item.phoneNumber || item.phone;
|
|
const normalized = normalizePhoneForBypass(rawPhone);
|
|
return {
|
|
_id: item._id || undefined,
|
|
phoneNumber: normalized || rawPhone,
|
|
label: String(item.label || '').trim(),
|
|
isActive: item.isActive !== false,
|
|
createdAt: item.createdAt || new Date()
|
|
};
|
|
})
|
|
.filter((item) => item.phoneNumber && item.phoneNumber.length >= 10);
|
|
|
|
doc.set('smsBypassNumbers', nextBypass);
|
|
doc.markModified('smsBypassNumbers');
|
|
}
|
|
|
|
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),
|
|
smsBypassNumbers: formatBypassNumbers(saved.smsBypassNumbers)
|
|
};
|
|
};
|
|
|
|
module.exports = {
|
|
getSettings,
|
|
saveSettings,
|
|
getSmsTemplate,
|
|
getSmsTemplateId
|
|
};
|