feat(settings): support dynamic number of SMS template variables
This commit is contained in:
@@ -9,7 +9,7 @@ const {
|
|||||||
sanitizeVariableName,
|
sanitizeVariableName,
|
||||||
emptyTemplateEntry,
|
emptyTemplateEntry,
|
||||||
normalizeStoredEntry,
|
normalizeStoredEntry,
|
||||||
resolveVariableMap
|
resolveVariablesList
|
||||||
} = require('./smsTemplates');
|
} = require('./smsTemplates');
|
||||||
|
|
||||||
const emptyTemplateMap = () => {
|
const emptyTemplateMap = () => {
|
||||||
@@ -36,18 +36,25 @@ const resolveTemplateId = (entry, def) => {
|
|||||||
|
|
||||||
const toPublicTemplates = (storedMap) => {
|
const toPublicTemplates = (storedMap) => {
|
||||||
return SMS_TEMPLATE_DEFS.map((def) => {
|
return SMS_TEMPLATE_DEFS.map((def) => {
|
||||||
const entry = normalizeStoredEntry(storedMap[def.key]);
|
const entry = normalizeStoredEntry(storedMap[def.key], def);
|
||||||
const variables = resolveVariableMap(def, entry.variables);
|
const variables = resolveVariablesList(def, entry.variables);
|
||||||
return {
|
return {
|
||||||
key: def.key,
|
key: def.key,
|
||||||
label: def.label,
|
label: def.label,
|
||||||
templateId: resolveTemplateId(entry, def),
|
templateId: resolveTemplateId(entry, def),
|
||||||
variables: (def.slots || []).map((slot) => ({
|
availableSlots: (def.slots || []).map((slot) => ({
|
||||||
slot: slot.key,
|
slot: slot.key,
|
||||||
label: slot.label,
|
label: slot.label,
|
||||||
defaultName: slot.defaultName,
|
defaultName: slot.defaultName
|
||||||
name: variables[slot.key]
|
})),
|
||||||
}))
|
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
|
||||||
|
};
|
||||||
|
})
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -63,13 +70,13 @@ const getSettings = async () => {
|
|||||||
const getSmsTemplate = async (key) => {
|
const getSmsTemplate = async (key) => {
|
||||||
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key);
|
const def = SMS_TEMPLATE_DEFS.find((item) => item.key === key);
|
||||||
if (!def) {
|
if (!def) {
|
||||||
return { templateId: '', variables: {} };
|
return { templateId: '', variables: [] };
|
||||||
}
|
}
|
||||||
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
|
const doc = await Setting.findOne({ key: SETTINGS_KEY }).lean();
|
||||||
const entry = normalizeStoredEntry(readStoredMap(doc)[key]);
|
const entry = normalizeStoredEntry(readStoredMap(doc)[key], def);
|
||||||
return {
|
return {
|
||||||
templateId: resolveTemplateId(entry, def),
|
templateId: resolveTemplateId(entry, def),
|
||||||
variables: resolveVariableMap(def, entry.variables)
|
variables: resolveVariablesList(def, entry.variables)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,12 +108,12 @@ const saveSettings = async (body = {}) => {
|
|||||||
const nextMap = emptyTemplateMap();
|
const nextMap = emptyTemplateMap();
|
||||||
|
|
||||||
for (const def of SMS_TEMPLATE_DEFS) {
|
for (const def of SMS_TEMPLATE_DEFS) {
|
||||||
const storedEntry = normalizeStoredEntry(storedMap[def.key]);
|
const storedEntry = normalizeStoredEntry(storedMap[def.key], def);
|
||||||
const hasIncoming = Object.prototype.hasOwnProperty.call(incomingMap, def.key);
|
const hasIncoming = Object.prototype.hasOwnProperty.call(incomingMap, def.key);
|
||||||
const incomingEntry = hasIncoming ? parseIncomingEntry(incomingMap[def.key]) : null;
|
const incomingEntry = hasIncoming ? parseIncomingEntry(incomingMap[def.key]) : null;
|
||||||
|
|
||||||
let templateId = storedEntry.templateId || envFallbackFor(def);
|
let templateId = storedEntry.templateId || envFallbackFor(def);
|
||||||
let variables = { ...storedEntry.variables };
|
let variables = storedEntry.variables;
|
||||||
|
|
||||||
if (incomingEntry) {
|
if (incomingEntry) {
|
||||||
if (incomingEntry.templateId !== undefined) {
|
if (incomingEntry.templateId !== undefined) {
|
||||||
@@ -117,23 +124,32 @@ const saveSettings = async (body = {}) => {
|
|||||||
templateId = sanitized;
|
templateId = sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (incomingEntry.variables && typeof incomingEntry.variables === 'object') {
|
if (incomingEntry.variables !== undefined) {
|
||||||
const incomingVars = Array.isArray(incomingEntry.variables)
|
let rawList = [];
|
||||||
? Object.fromEntries(incomingEntry.variables.map((item) => [item.slot, item.name]))
|
if (Array.isArray(incomingEntry.variables)) {
|
||||||
: incomingEntry.variables;
|
rawList = incomingEntry.variables;
|
||||||
|
} else if (incomingEntry.variables && typeof incomingEntry.variables === 'object') {
|
||||||
|
rawList = Object.entries(incomingEntry.variables).map(([slot, name]) => ({ slot, name }));
|
||||||
|
}
|
||||||
|
|
||||||
(def.slots || []).forEach((slot) => {
|
const validVars = [];
|
||||||
if (!Object.prototype.hasOwnProperty.call(incomingVars, slot.key)) return;
|
for (const item of rawList) {
|
||||||
const sanitized = sanitizeVariableName(incomingVars[slot.key]);
|
if (!item || typeof item !== 'object') continue;
|
||||||
|
const slot = String(item.slot || '').trim();
|
||||||
|
if (!slot) continue;
|
||||||
|
const rawName = String(item.name || '').trim();
|
||||||
|
if (!rawName) continue;
|
||||||
|
const sanitized = sanitizeVariableName(rawName);
|
||||||
if (sanitized === null) {
|
if (sanitized === null) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
'VALIDATION_FAILED',
|
'VALIDATION_FAILED',
|
||||||
{ field: `${def.key}.${slot.key}` },
|
{ field: `${def.key}.${slot}` },
|
||||||
`Invalid SMS variable name for ${def.key}.${slot.key}`
|
`Invalid SMS variable name "${rawName}" for ${def.key}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
variables[slot.key] = sanitized || slot.defaultName;
|
validVars.push({ slot, name: sanitized });
|
||||||
});
|
}
|
||||||
|
variables = validVars;
|
||||||
}
|
}
|
||||||
} else if (!storedEntry.templateId) {
|
} else if (!storedEntry.templateId) {
|
||||||
templateId = envFallbackFor(def);
|
templateId = envFallbackFor(def);
|
||||||
@@ -141,7 +157,7 @@ const saveSettings = async (body = {}) => {
|
|||||||
|
|
||||||
nextMap[def.key] = {
|
nextMap[def.key] = {
|
||||||
templateId,
|
templateId,
|
||||||
variables: resolveVariableMap(def, variables)
|
variables: resolveVariablesList(def, variables)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ const SMS_TEMPLATE_DEFS = [
|
|||||||
envKey: 'SMS_TEMPLATE_ACCOUNT_CREATED',
|
envKey: 'SMS_TEMPLATE_ACCOUNT_CREATED',
|
||||||
slots: [
|
slots: [
|
||||||
{ key: 'username', label: 'نام کاربری', defaultName: 'user' },
|
{ key: 'username', label: 'نام کاربری', defaultName: 'user' },
|
||||||
{ key: 'password', label: 'رمز عبور', defaultName: 'password' }
|
{ key: 'password', label: 'رمز عبور', defaultName: 'password' },
|
||||||
|
{ key: 'fullName', label: 'نام کاربر', defaultName: 'name' },
|
||||||
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -15,7 +17,10 @@ const SMS_TEMPLATE_DEFS = [
|
|||||||
label: 'ثبتنام در کلاس',
|
label: 'ثبتنام در کلاس',
|
||||||
envKey: 'SMS_TEMPLATE_CLASS_REGISTERED',
|
envKey: 'SMS_TEMPLATE_CLASS_REGISTERED',
|
||||||
slots: [
|
slots: [
|
||||||
{ key: 'className', label: 'نام کلاس', defaultName: 'className' }
|
{ key: 'className', label: 'نام کلاس', defaultName: 'className' },
|
||||||
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
||||||
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'fullName' },
|
||||||
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -24,8 +29,11 @@ const SMS_TEMPLATE_DEFS = [
|
|||||||
envKey: 'SMS_TEMPLATE_CLASS_REMINDER',
|
envKey: 'SMS_TEMPLATE_CLASS_REMINDER',
|
||||||
slots: [
|
slots: [
|
||||||
{ key: 'className', label: 'نام کلاس', defaultName: 'className' },
|
{ key: 'className', label: 'نام کلاس', defaultName: 'className' },
|
||||||
|
{ key: 'courseName', label: 'نام دوره', defaultName: 'courseName' },
|
||||||
{ key: 'time', label: 'ساعت', defaultName: 'time' },
|
{ key: 'time', label: 'ساعت', defaultName: 'time' },
|
||||||
{ key: 'place', label: 'مکان', defaultName: 'place' }
|
{ key: 'place', label: 'مکان', defaultName: 'place' },
|
||||||
|
{ key: 'fullName', label: 'نام کاربر', defaultName: 'fullName' },
|
||||||
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'mobile' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -35,7 +43,8 @@ const SMS_TEMPLATE_DEFS = [
|
|||||||
slots: [
|
slots: [
|
||||||
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' },
|
{ key: 'fullName', label: 'نام کارآموز', defaultName: 'FULLNAME' },
|
||||||
{ key: 'amount', label: 'مبلغ', defaultName: 'PAYMENT_PRICE' },
|
{ key: 'amount', label: 'مبلغ', defaultName: 'PAYMENT_PRICE' },
|
||||||
{ key: 'course', label: 'نام دوره', defaultName: 'COURSE' }
|
{ key: 'course', label: 'نام دوره', defaultName: 'COURSE' },
|
||||||
|
{ key: 'phoneNumber', label: 'شماره همراه', defaultName: 'MOBILE' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -63,37 +72,53 @@ const sanitizeVariableName = (value) => {
|
|||||||
return name;
|
return name;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultVariableMap = (def) => {
|
const defaultVariablesList = (def) => {
|
||||||
const map = {};
|
return (def?.slots || []).map((slot) => ({
|
||||||
(def.slots || []).forEach((slot) => {
|
slot: slot.key,
|
||||||
map[slot.key] = slot.defaultName;
|
name: slot.defaultName
|
||||||
});
|
}));
|
||||||
return map;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyTemplateEntry = (def) => ({
|
const emptyTemplateEntry = (def) => ({
|
||||||
templateId: '',
|
templateId: '',
|
||||||
variables: defaultVariableMap(def)
|
variables: defaultVariablesList(def)
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeStoredEntry = (raw) => {
|
const normalizeStoredEntry = (raw, def) => {
|
||||||
if (raw == null || raw === '') {
|
if (raw == null || raw === '') {
|
||||||
return { templateId: '', variables: {} };
|
return {
|
||||||
|
templateId: '',
|
||||||
|
variables: def ? defaultVariablesList(def) : []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (typeof raw === 'string' || typeof raw === 'number') {
|
if (typeof raw === 'string' || typeof raw === 'number') {
|
||||||
return { templateId: String(raw), variables: {} };
|
return {
|
||||||
|
templateId: String(raw),
|
||||||
|
variables: def ? defaultVariablesList(def) : []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (typeof raw !== 'object') {
|
if (typeof raw !== 'object') {
|
||||||
return { templateId: '', variables: {} };
|
return {
|
||||||
|
templateId: '',
|
||||||
|
variables: def ? defaultVariablesList(def) : []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let variables = {};
|
let variables = [];
|
||||||
if (raw.variables && typeof raw.variables === 'object' && !Array.isArray(raw.variables)) {
|
if (Array.isArray(raw.variables)) {
|
||||||
variables = { ...raw.variables };
|
variables = raw.variables
|
||||||
} else if (Array.isArray(raw.variables)) {
|
.filter((item) => item && typeof item === 'object' && item.slot)
|
||||||
raw.variables.forEach((item) => {
|
.map((item) => ({
|
||||||
if (item?.slot) variables[item.slot] = item.name;
|
slot: String(item.slot).trim(),
|
||||||
});
|
name: String(item.name || '').trim()
|
||||||
|
}));
|
||||||
|
} else if (raw.variables && typeof raw.variables === 'object') {
|
||||||
|
variables = Object.entries(raw.variables).map(([slot, name]) => ({
|
||||||
|
slot: String(slot).trim(),
|
||||||
|
name: String(name || '').trim()
|
||||||
|
}));
|
||||||
|
} else if (def) {
|
||||||
|
variables = defaultVariablesList(def);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -102,21 +127,51 @@ const normalizeStoredEntry = (raw) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveVariableMap = (def, storedVariables = {}) => {
|
const resolveVariablesList = (def, storedVariables) => {
|
||||||
const map = {};
|
if (!storedVariables) {
|
||||||
(def.slots || []).forEach((slot) => {
|
return defaultVariablesList(def);
|
||||||
const sanitized = sanitizeVariableName(storedVariables[slot.key]);
|
}
|
||||||
map[slot.key] = sanitized || slot.defaultName;
|
|
||||||
});
|
let list = [];
|
||||||
return map;
|
if (Array.isArray(storedVariables)) {
|
||||||
|
list = storedVariables;
|
||||||
|
} else if (typeof storedVariables === 'object') {
|
||||||
|
list = Object.entries(storedVariables).map(([slot, name]) => ({ slot, name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
.map((item) => {
|
||||||
|
const slot = String(item.slot || '').trim();
|
||||||
|
if (!slot) return null;
|
||||||
|
const slotDef = (def?.slots || []).find((s) => s.key === slot);
|
||||||
|
const rawName = item.name !== undefined ? item.name : slotDef?.defaultName || '';
|
||||||
|
const sanitized = sanitizeVariableName(rawName);
|
||||||
|
return {
|
||||||
|
slot,
|
||||||
|
name: sanitized || slotDef?.defaultName || slot
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildSmsParameters = (variableMap, valuesBySlot) => {
|
const buildSmsParameters = (variables, valuesBySlot = {}) => {
|
||||||
return Object.entries(valuesBySlot)
|
let list = [];
|
||||||
.map(([slot, value]) => {
|
if (Array.isArray(variables)) {
|
||||||
const name = String(variableMap?.[slot] || '').trim();
|
list = variables;
|
||||||
|
} else if (variables && typeof variables === 'object') {
|
||||||
|
list = Object.entries(variables).map(([slot, name]) => ({ slot, name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
.map((item) => {
|
||||||
|
const name = sanitizeVariableName(item?.name);
|
||||||
if (!name) return null;
|
if (!name) return null;
|
||||||
return { name, value: String(value ?? '') };
|
const slot = item?.slot;
|
||||||
|
const value = slot ? valuesBySlot?.[slot] : '';
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
value: String(value ?? '')
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
};
|
};
|
||||||
@@ -127,9 +182,9 @@ module.exports = {
|
|||||||
envFallbackFor,
|
envFallbackFor,
|
||||||
sanitizeTemplateId,
|
sanitizeTemplateId,
|
||||||
sanitizeVariableName,
|
sanitizeVariableName,
|
||||||
defaultVariableMap,
|
defaultVariablesList,
|
||||||
emptyTemplateEntry,
|
emptyTemplateEntry,
|
||||||
normalizeStoredEntry,
|
normalizeStoredEntry,
|
||||||
resolveVariableMap,
|
resolveVariablesList,
|
||||||
buildSmsParameters
|
buildSmsParameters
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ const resolveUserIdByPhone = async (phoneNumber) => {
|
|||||||
|
|
||||||
const sendAccountCreatedSms = async (receiver, username, password, userId = null) => {
|
const sendAccountCreatedSms = async (receiver, username, password, userId = null) => {
|
||||||
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
||||||
|
let fullName = '';
|
||||||
|
if (resolvedUserId) {
|
||||||
|
const user = await User.findById(resolvedUserId).select('name').lean();
|
||||||
|
if (user?.name) fullName = user.name;
|
||||||
|
}
|
||||||
const { templateId, variables } = await getSmsTemplate('accountCreated');
|
const { templateId, variables } = await getSmsTemplate('accountCreated');
|
||||||
return recordAndSend({
|
return recordAndSend({
|
||||||
userId: resolvedUserId,
|
userId: resolvedUserId,
|
||||||
@@ -24,13 +29,22 @@ const sendAccountCreatedSms = async (receiver, username, password, userId = null
|
|||||||
relatedEvent: 'user.created',
|
relatedEvent: 'user.created',
|
||||||
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
||||||
username,
|
username,
|
||||||
password
|
password,
|
||||||
|
fullName: fullName || username,
|
||||||
|
name: fullName || username,
|
||||||
|
phoneNumber: receiver,
|
||||||
|
mobile: receiver
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendClassRegisteredSms = async (receiver, className, userId = null) => {
|
const sendClassRegisteredSms = async (receiver, className, userId = null, extraContext = {}) => {
|
||||||
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
||||||
|
let fullName = extraContext.fullName || '';
|
||||||
|
if (!fullName && resolvedUserId) {
|
||||||
|
const user = await User.findById(resolvedUserId).select('name').lean();
|
||||||
|
if (user?.name) fullName = user.name;
|
||||||
|
}
|
||||||
const { templateId, variables } = await getSmsTemplate('classRegistered');
|
const { templateId, variables } = await getSmsTemplate('classRegistered');
|
||||||
return recordAndSend({
|
return recordAndSend({
|
||||||
userId: resolvedUserId,
|
userId: resolvedUserId,
|
||||||
@@ -39,13 +53,23 @@ const sendClassRegisteredSms = async (receiver, className, userId = null) => {
|
|||||||
body: `ثبتنام شما در کلاس «${className}» انجام شد.`,
|
body: `ثبتنام شما در کلاس «${className}» انجام شد.`,
|
||||||
relatedEvent: 'user.enrolled',
|
relatedEvent: 'user.enrolled',
|
||||||
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
||||||
className
|
className,
|
||||||
|
courseName: extraContext.courseName || className,
|
||||||
|
fullName: fullName || 'کارآموز',
|
||||||
|
phoneNumber: receiver,
|
||||||
|
mobile: receiver,
|
||||||
|
...extraContext
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendClassReminderSms = async (receiver, className, time, place = '', userId = null) => {
|
const sendClassReminderSms = async (receiver, className, time, place = '', userId = null, extraContext = {}) => {
|
||||||
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
const resolvedUserId = userId || await resolveUserIdByPhone(receiver);
|
||||||
|
let fullName = extraContext.fullName || '';
|
||||||
|
if (!fullName && resolvedUserId) {
|
||||||
|
const user = await User.findById(resolvedUserId).select('name').lean();
|
||||||
|
if (user?.name) fullName = user.name;
|
||||||
|
}
|
||||||
const { templateId, variables } = await getSmsTemplate('classReminder');
|
const { templateId, variables } = await getSmsTemplate('classReminder');
|
||||||
return recordAndSend({
|
return recordAndSend({
|
||||||
userId: resolvedUserId,
|
userId: resolvedUserId,
|
||||||
@@ -55,8 +79,13 @@ const sendClassReminderSms = async (receiver, className, time, place = '', userI
|
|||||||
relatedEvent: 'session.reminder',
|
relatedEvent: 'session.reminder',
|
||||||
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
||||||
className,
|
className,
|
||||||
|
courseName: extraContext.courseName || className,
|
||||||
time,
|
time,
|
||||||
place: place || '-'
|
place: place || '-',
|
||||||
|
fullName: fullName || 'کارآموز',
|
||||||
|
phoneNumber: receiver,
|
||||||
|
mobile: receiver,
|
||||||
|
...extraContext
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -90,7 +119,9 @@ const sendInvoiceCreatedSms = async (receiver, payload, userId = null) => {
|
|||||||
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
sendFn: () => sendSingleSms(receiver, templateId, buildSmsParameters(variables, {
|
||||||
fullName: fullNameLabel,
|
fullName: fullNameLabel,
|
||||||
amount: amountLabel,
|
amount: amountLabel,
|
||||||
course: courseLabel
|
course: courseLabel,
|
||||||
|
phoneNumber: receiver,
|
||||||
|
mobile: receiver
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user