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.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('../../config/config');
|
||||
const logger = require('../../utils/logger');
|
||||
const { Setting, SETTINGS_KEY } = require('./settingModel');
|
||||
const {
|
||||
isChannelEnabled,
|
||||
isEnvChannelEnabled,
|
||||
toPublicMessaging
|
||||
} = require('../../utils/messagingChannels');
|
||||
|
||||
let cache = null;
|
||||
let cacheAt = 0;
|
||||
const CACHE_TTL_MS = 10000;
|
||||
|
||||
const readDbFlags = (doc) => ({
|
||||
smsEnabled: doc?.smsEnabled,
|
||||
emailEnabled: doc?.emailEnabled,
|
||||
botEnabled: doc?.botEnabled
|
||||
});
|
||||
|
||||
const getDbMessagingFlags = async () => {
|
||||
const now = Date.now();
|
||||
if (cache && (now - cacheAt) < CACHE_TTL_MS) return cache;
|
||||
const doc = await Setting.findOne({ key: SETTINGS_KEY })
|
||||
.select('smsEnabled emailEnabled botEnabled')
|
||||
.lean();
|
||||
cache = readDbFlags(doc);
|
||||
cacheAt = now;
|
||||
return cache;
|
||||
};
|
||||
|
||||
const invalidateMessagingCache = () => {
|
||||
cache = null;
|
||||
cacheAt = 0;
|
||||
};
|
||||
|
||||
const isMessagingChannelEnabled = async (channel) => {
|
||||
if (!isEnvChannelEnabled(channel, config)) return false;
|
||||
try {
|
||||
const db = await getDbMessagingFlags();
|
||||
return isChannelEnabled(channel, { env: config, db });
|
||||
} catch (err) {
|
||||
logger.warn(`[Messaging] Failed to read settings for ${channel}, using env only: ${err.message}`);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const getPublicMessaging = (doc) => toPublicMessaging(readDbFlags(doc), config);
|
||||
|
||||
module.exports = {
|
||||
isMessagingChannelEnabled,
|
||||
invalidateMessagingCache,
|
||||
getPublicMessaging,
|
||||
getDbMessagingFlags
|
||||
};
|
||||
@@ -15,6 +15,18 @@ const settingSchema = new mongoose.Schema({
|
||||
smsTemplates: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: {}
|
||||
},
|
||||
smsEnabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
emailEnabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
botEnabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
|
||||
@@ -10,6 +10,11 @@ const {
|
||||
resolveVariablesList,
|
||||
mergeTemplateEntry
|
||||
} = require('./smsTemplates');
|
||||
const { parseIncomingMessaging } = require('../../utils/messagingChannels');
|
||||
const {
|
||||
getPublicMessaging,
|
||||
invalidateMessagingCache
|
||||
} = require('./messagingFlags');
|
||||
|
||||
const emptyTemplateMap = () => {
|
||||
const map = {};
|
||||
@@ -62,7 +67,8 @@ const getSettings = async () => {
|
||||
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
|
||||
const storedMap = readStoredMap(doc);
|
||||
return {
|
||||
smsTemplates: toPublicTemplates(storedMap)
|
||||
smsTemplates: toPublicTemplates(storedMap),
|
||||
messaging: getPublicMessaging(doc)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -97,44 +103,60 @@ const parseIncomingEntry = (raw) => {
|
||||
};
|
||||
|
||||
const saveSettings = async (body = {}) => {
|
||||
const incoming = body.smsTemplates || {};
|
||||
const incomingMap = Array.isArray(incoming)
|
||||
? Object.fromEntries(incoming.map((item) => [item.key, item]))
|
||||
: incoming;
|
||||
|
||||
const existing = await Setting.findOne({ key: SETTINGS_KEY });
|
||||
const storedMap = existing ? readStoredMap(existing.toObject ? existing.toObject() : existing) : emptyTemplateMap();
|
||||
const nextMap = {};
|
||||
const doc = existing || new Setting({ key: SETTINGS_KEY });
|
||||
const hasTemplatePayload = body.smsTemplates !== undefined;
|
||||
|
||||
for (const def of SMS_TEMPLATE_DEFS) {
|
||||
const hasIncoming = Object.prototype.hasOwnProperty.call(incomingMap, def.key);
|
||||
const incomingEntry = hasIncoming ? parseIncomingEntry(incomingMap[def.key]) : null;
|
||||
if (hasTemplatePayload) {
|
||||
const incoming = body.smsTemplates || {};
|
||||
const incomingMap = Array.isArray(incoming)
|
||||
? Object.fromEntries(incoming.map((item) => [item.key, item]))
|
||||
: incoming;
|
||||
|
||||
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} نامعتبر است`);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
const doc = existing || new Setting({ key: SETTINGS_KEY });
|
||||
doc.set('smsTemplates', JSON.parse(JSON.stringify(nextMap)));
|
||||
doc.markModified('smsTemplates');
|
||||
await doc.save();
|
||||
invalidateMessagingCache();
|
||||
|
||||
const saved = await Setting.findOne({ key: SETTINGS_KEY }).lean();
|
||||
return {
|
||||
smsTemplates: toPublicTemplates(readStoredMap(saved))
|
||||
smsTemplates: toPublicTemplates(readStoredMap(saved)),
|
||||
messaging: getPublicMessaging(saved)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -69,4 +69,11 @@ describe('SMS template variables', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps username and password slots on the accountCreated template', () => {
|
||||
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === 'accountCreated');
|
||||
const slots = (def?.slots || []).map((slot) => slot.key);
|
||||
assert.ok(slots.includes('username'));
|
||||
assert.ok(slots.includes('password'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user