feat: auto generate placeholder nationalId for users and professors, auto calculate class end date
This commit is contained in:
@@ -9,7 +9,7 @@ const Payment = require('../payments/paymentModel');
|
|||||||
const AppError = require('../../utils/AppError');
|
const AppError = require('../../utils/AppError');
|
||||||
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
|
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
|
||||||
const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages');
|
const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages');
|
||||||
const { buildClassScheduleContext, normalizeWeekdays, normalizeClockTime } = require('../../utils/classSchedule');
|
const { buildClassScheduleContext, normalizeWeekdays, normalizeClockTime, calculateClassEndDate } = require('../../utils/classSchedule');
|
||||||
const { resolveNotifyFlags } = require('../../utils/notifyResolver');
|
const { resolveNotifyFlags } = require('../../utils/notifyResolver');
|
||||||
const { notifyAction } = require('../../utils/actionNotify');
|
const { notifyAction } = require('../../utils/actionNotify');
|
||||||
const logger = require('../../utils/logger');
|
const logger = require('../../utils/logger');
|
||||||
@@ -18,6 +18,17 @@ const applyScheduleFields = (payload, body) => {
|
|||||||
if (body.days !== undefined) payload.days = normalizeWeekdays(body.days);
|
if (body.days !== undefined) payload.days = normalizeWeekdays(body.days);
|
||||||
if (body.startTime !== undefined) payload.startTime = normalizeClockTime(body.startTime);
|
if (body.startTime !== undefined) payload.startTime = normalizeClockTime(body.startTime);
|
||||||
if (body.endTime !== undefined) payload.endTime = normalizeClockTime(body.endTime);
|
if (body.endTime !== undefined) payload.endTime = normalizeClockTime(body.endTime);
|
||||||
|
|
||||||
|
const startDate = payload.startDate || body.startDate;
|
||||||
|
const days = payload.days || body.days;
|
||||||
|
const sessions = payload.numberOfSessions || body.numberOfSessions;
|
||||||
|
if (startDate && days?.length && sessions) {
|
||||||
|
const computedEnd = calculateClassEndDate(startDate, days, sessions);
|
||||||
|
if (computedEnd && (!payload.endDate || !body.endDate)) {
|
||||||
|
payload.endDate = computedEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -63,10 +63,6 @@ const findExistingUser = async ({ nationalIdCode, phoneNumber, name }) => {
|
|||||||
const byId = await User.findOne({ nationalIdCode });
|
const byId = await User.findOne({ nationalIdCode });
|
||||||
if (byId) return byId;
|
if (byId) return byId;
|
||||||
}
|
}
|
||||||
if (phoneNumber) {
|
|
||||||
const byPhone = await User.findOne({ phoneNumber });
|
|
||||||
if (byPhone) return byPhone;
|
|
||||||
}
|
|
||||||
const target = normalizePersonName(name);
|
const target = normalizePersonName(name);
|
||||||
if (target) {
|
if (target) {
|
||||||
const firstToken = target.split(' ')[0];
|
const firstToken = target.split(' ')[0];
|
||||||
@@ -76,6 +72,14 @@ const findExistingUser = async ({ nationalIdCode, phoneNumber, name }) => {
|
|||||||
const match = candidates.find((user) => namesMatch(user.name, target));
|
const match = candidates.find((user) => namesMatch(user.name, target));
|
||||||
if (match) return match;
|
if (match) return match;
|
||||||
}
|
}
|
||||||
|
if (phoneNumber) {
|
||||||
|
const byPhone = await User.findOne({ phoneNumber });
|
||||||
|
if (byPhone) {
|
||||||
|
if (!nationalIdCode || !byPhone.nationalIdCode || byPhone.nationalIdCode === nationalIdCode) {
|
||||||
|
return byPhone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +112,7 @@ const applyStudentProfile = (user, student) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const upsertStudent = async (student, userRole, stats, warnings) => {
|
const upsertStudent = async (student, userRole, stats, warnings) => {
|
||||||
const phoneNumber = normalizePhone(student.phoneNumber || student.phone || student.parentPhoneNumber);
|
let phoneNumber = normalizePhone(student.phoneNumber || student.phone || student.parentPhoneNumber);
|
||||||
let nationalIdCode = normalizeNationalId(student.nationalIdCode || student.nationalId);
|
let nationalIdCode = normalizeNationalId(student.nationalIdCode || student.nationalId);
|
||||||
|
|
||||||
if (!phoneNumber && !nationalIdCode) {
|
if (!phoneNumber && !nationalIdCode) {
|
||||||
@@ -129,23 +133,22 @@ const upsertStudent = async (student, userRole, stats, warnings) => {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!phoneNumber) {
|
|
||||||
warnings.push({
|
|
||||||
reason: 'missing_phone',
|
|
||||||
student: { name: student.name, nationalIdCode }
|
|
||||||
});
|
|
||||||
stats.studentsSkipped += 1;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nationalIdCode) {
|
if (!nationalIdCode) {
|
||||||
nationalIdCode = await allocatePlaceholderNationalId(phoneNumber);
|
nationalIdCode = await allocatePlaceholderNationalId(phoneNumber || student.name);
|
||||||
warnings.push({
|
warnings.push({
|
||||||
reason: 'placeholder_national_id',
|
reason: 'placeholder_national_id',
|
||||||
student: { name: student.name, phoneNumber, nationalIdCode }
|
student: { name: student.name, phoneNumber, nationalIdCode }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!phoneNumber) {
|
||||||
|
phoneNumber = await allocatePlaceholderPhone(nationalIdCode || student.name);
|
||||||
|
warnings.push({
|
||||||
|
reason: 'placeholder_phone',
|
||||||
|
student: { name: student.name, nationalIdCode, placeholderPhone: phoneNumber }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const conflictById = await User.findOne({ nationalIdCode });
|
const conflictById = await User.findOne({ nationalIdCode });
|
||||||
if (conflictById) {
|
if (conflictById) {
|
||||||
applyStudentProfile(conflictById, student);
|
applyStudentProfile(conflictById, student);
|
||||||
@@ -154,6 +157,17 @@ const upsertStudent = async (student, userRole, stats, warnings) => {
|
|||||||
return conflictById;
|
return conflictById;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const conflictByPhone = await User.findOne({ phoneNumber });
|
||||||
|
if (conflictByPhone) {
|
||||||
|
const originalPhone = phoneNumber;
|
||||||
|
phoneNumber = await allocatePlaceholderPhone(nationalIdCode || student.name);
|
||||||
|
if (!student.parentPhoneNumber) student.parentPhoneNumber = originalPhone;
|
||||||
|
warnings.push({
|
||||||
|
reason: 'phone_conflict_placeholder_allocated',
|
||||||
|
student: { name: student.name, originalPhone, placeholderPhone: phoneNumber }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const username = await allocateUniqueUsername();
|
const username = await allocateUniqueUsername();
|
||||||
const plainPassword = generateSimplePassword();
|
const plainPassword = generateSimplePassword();
|
||||||
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
||||||
@@ -194,13 +208,56 @@ const enrollUserInClass = async (user, course, cls, stats) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WEEKDAY_STR_TO_NUM = {
|
||||||
|
sunday: 0,
|
||||||
|
monday: 1,
|
||||||
|
tuesday: 2,
|
||||||
|
wednesday: 3,
|
||||||
|
thursday: 4,
|
||||||
|
friday: 5,
|
||||||
|
saturday: 6,
|
||||||
|
'یکشنبه': 0,
|
||||||
|
'یک شنبه': 0,
|
||||||
|
'دوشنبه': 1,
|
||||||
|
'دو شنبه': 1,
|
||||||
|
'سه شنبه': 2,
|
||||||
|
'سهشنبه': 2,
|
||||||
|
'سهشنبه': 2,
|
||||||
|
'چهارشنبه': 3,
|
||||||
|
'چهار شنبه': 3,
|
||||||
|
'پنجشنبه': 4,
|
||||||
|
'پنج شنبه': 4,
|
||||||
|
'جمعه': 5,
|
||||||
|
'شنبه': 6
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeWeekdaysInput = (days) => {
|
||||||
|
if (!Array.isArray(days)) return [];
|
||||||
|
const res = [];
|
||||||
|
for (const d of days) {
|
||||||
|
if (typeof d === 'number' && d >= 0 && d <= 6) {
|
||||||
|
if (!res.includes(d)) res.push(d);
|
||||||
|
} else if (typeof d === 'string') {
|
||||||
|
const lower = d.trim().toLowerCase();
|
||||||
|
if (WEEKDAY_STR_TO_NUM[lower] != null) {
|
||||||
|
const num = WEEKDAY_STR_TO_NUM[lower];
|
||||||
|
if (!res.includes(num)) res.push(num);
|
||||||
|
} else if (!Number.isNaN(Number(d))) {
|
||||||
|
const num = Number(d);
|
||||||
|
if (num >= 0 && num <= 6 && !res.includes(num)) res.push(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
const applyClassSchedule = (cls, classInput) => {
|
const applyClassSchedule = (cls, classInput) => {
|
||||||
if (classInput.startDate && !cls.startDate) {
|
if (classInput.startDate && !cls.startDate) {
|
||||||
const startDate = parseImportDate(classInput.startDate);
|
const startDate = parseImportDate(classInput.startDate);
|
||||||
if (startDate) cls.startDate = startDate;
|
if (startDate) cls.startDate = startDate;
|
||||||
}
|
}
|
||||||
if (Array.isArray(classInput.days) && classInput.days.length) {
|
if (Array.isArray(classInput.days) && classInput.days.length) {
|
||||||
cls.days = classInput.days;
|
cls.days = normalizeWeekdaysInput(classInput.days);
|
||||||
}
|
}
|
||||||
if (classInput.startTime) cls.startTime = String(classInput.startTime).trim();
|
if (classInput.startTime) cls.startTime = String(classInput.startTime).trim();
|
||||||
if (classInput.endTime) cls.endTime = String(classInput.endTime).trim();
|
if (classInput.endTime) cls.endTime = String(classInput.endTime).trim();
|
||||||
@@ -581,7 +638,7 @@ const importData = async (rawPayload) => {
|
|||||||
course: course._id,
|
course: course._id,
|
||||||
startDate: parseImportDate(classInput.startDate) || undefined,
|
startDate: parseImportDate(classInput.startDate) || undefined,
|
||||||
tuitionFee: Number(classInput.tuitionFee) || Number(courseInput.price) || 0,
|
tuitionFee: Number(classInput.tuitionFee) || Number(courseInput.price) || 0,
|
||||||
days: Array.isArray(classInput.days) ? classInput.days : [],
|
days: normalizeWeekdaysInput(classInput.days),
|
||||||
startTime: classInput.startTime || '',
|
startTime: classInput.startTime || '',
|
||||||
endTime: classInput.endTime || '',
|
endTime: classInput.endTime || '',
|
||||||
isActive: true
|
isActive: true
|
||||||
|
|||||||
@@ -5,14 +5,22 @@ const AppError = require('../../utils/AppError');
|
|||||||
const eventEmitter = require('../../events/eventEmitter');
|
const eventEmitter = require('../../events/eventEmitter');
|
||||||
const EVENT_NAMES = require('../../constants/eventNames');
|
const EVENT_NAMES = require('../../constants/eventNames');
|
||||||
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
|
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
|
||||||
|
const { allocatePlaceholderNationalId } = require('../../utils/nationalId');
|
||||||
|
|
||||||
const createProfessor = async (data) => {
|
const createProfessor = async (data) => {
|
||||||
|
let nationalIdCode = String(data.nationalIdCode || data.nationalId || '').trim();
|
||||||
|
const phoneNumber = String(data.phoneNumber || data.phone || '').trim();
|
||||||
|
|
||||||
|
if (!nationalIdCode) {
|
||||||
|
nationalIdCode = await allocatePlaceholderNationalId(phoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...data,
|
...data,
|
||||||
name: String(data.name || '').trim(),
|
name: String(data.name || '').trim(),
|
||||||
surname: String(data.surname || '').trim(),
|
surname: String(data.surname || '').trim(),
|
||||||
nationalIdCode: String(data.nationalIdCode || data.nationalId || '').trim(),
|
nationalIdCode,
|
||||||
phoneNumber: String(data.phoneNumber || data.phone || '').trim(),
|
phoneNumber,
|
||||||
email: data.email ? String(data.email).trim().toLowerCase() : undefined,
|
email: data.email ? String(data.email).trim().toLowerCase() : undefined,
|
||||||
cardNumber: data.cardNumber ? String(data.cardNumber).trim() : undefined,
|
cardNumber: data.cardNumber ? String(data.cardNumber).trim() : undefined,
|
||||||
shabaNumber: data.shabaNumber || data.iban ? String(data.shabaNumber || data.iban).trim() : undefined,
|
shabaNumber: data.shabaNumber || data.iban ? String(data.shabaNumber || data.iban).trim() : undefined,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const {
|
|||||||
assertCanResetPasswordAndSms,
|
assertCanResetPasswordAndSms,
|
||||||
isCredentialsSmsDelivered
|
isCredentialsSmsDelivered
|
||||||
} = require('./passwordReset');
|
} = require('./passwordReset');
|
||||||
|
const { allocatePlaceholderNationalId } = require('../../utils/nationalId');
|
||||||
|
|
||||||
const normalizeAdminNotes = (value) => {
|
const normalizeAdminNotes = (value) => {
|
||||||
if (value == null) return undefined;
|
if (value == null) return undefined;
|
||||||
@@ -106,6 +107,10 @@ const signUp = async (body) => {
|
|||||||
const userRole = await Role.findOne({ name: 'User' });
|
const userRole = await Role.findOne({ name: 'User' });
|
||||||
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
||||||
|
|
||||||
|
if (!profile.nationalIdCode) {
|
||||||
|
profile.nationalIdCode = await allocatePlaceholderNationalId(profile.phoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
const passwordHash = await bcrypt.hash(password, 10);
|
const passwordHash = await bcrypt.hash(password, 10);
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
...profile,
|
...profile,
|
||||||
@@ -171,6 +176,10 @@ const createUserAdmin = async (body) => {
|
|||||||
}
|
}
|
||||||
if (!roleObj) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
if (!roleObj) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
||||||
|
|
||||||
|
if (!profile.nationalIdCode) {
|
||||||
|
profile.nationalIdCode = await allocatePlaceholderNationalId(profile.phoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
const plainPassword = (requestedPassword && String(requestedPassword).trim()) || generateSimplePassword();
|
const plainPassword = (requestedPassword && String(requestedPassword).trim()) || generateSimplePassword();
|
||||||
const username = await allocateUniqueUsername(requestedUsername);
|
const username = await allocateUniqueUsername(requestedUsername);
|
||||||
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
||||||
|
|||||||
+27
-1
@@ -109,6 +109,31 @@ const buildClassScheduleContext = (classDoc, sessions = [], currentSession = nul
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calculateClassEndDate = (startDate, days = [], numberOfSessions = 0) => {
|
||||||
|
const date = toDate(startDate);
|
||||||
|
const countNeeded = parseInt(numberOfSessions, 10);
|
||||||
|
if (!date || isNaN(countNeeded) || countNeeded < 1) return null;
|
||||||
|
const validDays = normalizeWeekdays(days);
|
||||||
|
if (!validDays.length) return null;
|
||||||
|
|
||||||
|
let current = new Date(date);
|
||||||
|
let count = 0;
|
||||||
|
let lastMatchingDate = null;
|
||||||
|
let safetyLimit = 365 * 5;
|
||||||
|
|
||||||
|
while (count < countNeeded && safetyLimit > 0) {
|
||||||
|
safetyLimit--;
|
||||||
|
if (validDays.includes(current.getDay())) {
|
||||||
|
count++;
|
||||||
|
lastMatchingDate = new Date(current);
|
||||||
|
}
|
||||||
|
if (count >= countNeeded) break;
|
||||||
|
current.setDate(current.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastMatchingDate;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
IRAN_WEEK_ORDER,
|
IRAN_WEEK_ORDER,
|
||||||
PERSIAN_WEEKDAYS,
|
PERSIAN_WEEKDAYS,
|
||||||
@@ -118,5 +143,6 @@ module.exports = {
|
|||||||
formatClassStartDate,
|
formatClassStartDate,
|
||||||
normalizeWeekdays,
|
normalizeWeekdays,
|
||||||
normalizeClockTime,
|
normalizeClockTime,
|
||||||
buildClassScheduleContext
|
buildClassScheduleContext,
|
||||||
|
calculateClassEndDate
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const User = require('../components/users/userModel');
|
||||||
|
const Professor = require('../components/professors/professorModel');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique 10-character placeholder national ID (starting with TMP).
|
||||||
|
* @param {string} [seed] - Optional seed like phone number or timestamp
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
const allocatePlaceholderNationalId = async (seed) => {
|
||||||
|
const cleanDigits = String(seed || '').replace(/\D/g, '');
|
||||||
|
const baseSuffix = (cleanDigits.slice(-7) || Date.now().toString().slice(-7)).padStart(7, '0').slice(0, 7);
|
||||||
|
let candidate = `TMP${baseSuffix}`.slice(0, 10);
|
||||||
|
let i = 0;
|
||||||
|
while ((await User.exists({ nationalIdCode: candidate })) || (await Professor.exists({ nationalIdCode: candidate }))) {
|
||||||
|
i += 1;
|
||||||
|
candidate = `TMP${String(i).padStart(7, '0')}`.slice(0, 10);
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allocatePlaceholderNationalId
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user