feat: auto generate placeholder nationalId for users and professors, auto calculate class end date

This commit is contained in:
2026-08-21 15:03:18 +03:30
parent cbcf3f55db
commit 30db6582b7
6 changed files with 157 additions and 21 deletions
+12 -1
View File
@@ -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;
};
+74 -17
View File
@@ -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
+10 -2
View File
@@ -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,
+9
View File
@@ -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);