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 { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
|
||||
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 { notifyAction } = require('../../utils/actionNotify');
|
||||
const logger = require('../../utils/logger');
|
||||
@@ -18,6 +18,17 @@ const applyScheduleFields = (payload, body) => {
|
||||
if (body.days !== undefined) payload.days = normalizeWeekdays(body.days);
|
||||
if (body.startTime !== undefined) payload.startTime = normalizeClockTime(body.startTime);
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -63,10 +63,6 @@ const findExistingUser = async ({ nationalIdCode, phoneNumber, name }) => {
|
||||
const byId = await User.findOne({ nationalIdCode });
|
||||
if (byId) return byId;
|
||||
}
|
||||
if (phoneNumber) {
|
||||
const byPhone = await User.findOne({ phoneNumber });
|
||||
if (byPhone) return byPhone;
|
||||
}
|
||||
const target = normalizePersonName(name);
|
||||
if (target) {
|
||||
const firstToken = target.split(' ')[0];
|
||||
@@ -76,6 +72,14 @@ const findExistingUser = async ({ nationalIdCode, phoneNumber, name }) => {
|
||||
const match = candidates.find((user) => namesMatch(user.name, target));
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -108,7 +112,7 @@ const applyStudentProfile = (user, student) => {
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (!phoneNumber && !nationalIdCode) {
|
||||
@@ -129,23 +133,22 @@ const upsertStudent = async (student, userRole, stats, warnings) => {
|
||||
return user;
|
||||
}
|
||||
|
||||
if (!phoneNumber) {
|
||||
warnings.push({
|
||||
reason: 'missing_phone',
|
||||
student: { name: student.name, nationalIdCode }
|
||||
});
|
||||
stats.studentsSkipped += 1;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!nationalIdCode) {
|
||||
nationalIdCode = await allocatePlaceholderNationalId(phoneNumber);
|
||||
nationalIdCode = await allocatePlaceholderNationalId(phoneNumber || student.name);
|
||||
warnings.push({
|
||||
reason: 'placeholder_national_id',
|
||||
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 });
|
||||
if (conflictById) {
|
||||
applyStudentProfile(conflictById, student);
|
||||
@@ -154,6 +157,17 @@ const upsertStudent = async (student, userRole, stats, warnings) => {
|
||||
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 plainPassword = generateSimplePassword();
|
||||
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) => {
|
||||
if (classInput.startDate && !cls.startDate) {
|
||||
const startDate = parseImportDate(classInput.startDate);
|
||||
if (startDate) cls.startDate = startDate;
|
||||
}
|
||||
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.endTime) cls.endTime = String(classInput.endTime).trim();
|
||||
@@ -581,7 +638,7 @@ const importData = async (rawPayload) => {
|
||||
course: course._id,
|
||||
startDate: parseImportDate(classInput.startDate) || undefined,
|
||||
tuitionFee: Number(classInput.tuitionFee) || Number(courseInput.price) || 0,
|
||||
days: Array.isArray(classInput.days) ? classInput.days : [],
|
||||
days: normalizeWeekdaysInput(classInput.days),
|
||||
startTime: classInput.startTime || '',
|
||||
endTime: classInput.endTime || '',
|
||||
isActive: true
|
||||
|
||||
@@ -5,14 +5,22 @@ const AppError = require('../../utils/AppError');
|
||||
const eventEmitter = require('../../events/eventEmitter');
|
||||
const EVENT_NAMES = require('../../constants/eventNames');
|
||||
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
|
||||
const { allocatePlaceholderNationalId } = require('../../utils/nationalId');
|
||||
|
||||
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 = {
|
||||
...data,
|
||||
name: String(data.name || '').trim(),
|
||||
surname: String(data.surname || '').trim(),
|
||||
nationalIdCode: String(data.nationalIdCode || data.nationalId || '').trim(),
|
||||
phoneNumber: String(data.phoneNumber || data.phone || '').trim(),
|
||||
nationalIdCode,
|
||||
phoneNumber,
|
||||
email: data.email ? String(data.email).trim().toLowerCase() : undefined,
|
||||
cardNumber: data.cardNumber ? String(data.cardNumber).trim() : undefined,
|
||||
shabaNumber: data.shabaNumber || data.iban ? String(data.shabaNumber || data.iban).trim() : undefined,
|
||||
|
||||
@@ -17,6 +17,7 @@ const {
|
||||
assertCanResetPasswordAndSms,
|
||||
isCredentialsSmsDelivered
|
||||
} = require('./passwordReset');
|
||||
const { allocatePlaceholderNationalId } = require('../../utils/nationalId');
|
||||
|
||||
const normalizeAdminNotes = (value) => {
|
||||
if (value == null) return undefined;
|
||||
@@ -106,6 +107,10 @@ const signUp = async (body) => {
|
||||
const userRole = await Role.findOne({ name: 'User' });
|
||||
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 user = await User.create({
|
||||
...profile,
|
||||
@@ -171,6 +176,10 @@ const createUserAdmin = async (body) => {
|
||||
}
|
||||
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 username = await allocateUniqueUsername(requestedUsername);
|
||||
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 = {
|
||||
IRAN_WEEK_ORDER,
|
||||
PERSIAN_WEEKDAYS,
|
||||
@@ -118,5 +143,6 @@ module.exports = {
|
||||
formatClassStartDate,
|
||||
normalizeWeekdays,
|
||||
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