feat: add professor import support, user account creation, and IBAN/card fields
This commit is contained in:
@@ -5,6 +5,7 @@ const bcrypt = require('bcryptjs');
|
||||
const Course = require('../courses/courseModel');
|
||||
const Class = require('../classes/classModel');
|
||||
const User = require('../users/userModel');
|
||||
const Professor = require('../professors/professorModel');
|
||||
const Role = require('../roles/roleModel');
|
||||
const Session = require('../sessions/sessionModel');
|
||||
const Payment = require('../payments/paymentModel');
|
||||
@@ -13,7 +14,7 @@ const paymentService = require('../payments/paymentService');
|
||||
const AppError = require('../../utils/AppError');
|
||||
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
||||
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
|
||||
const { parseImportDate, utcDayRange } = require('../../utils/jalaliDate');
|
||||
const { parseImportDate, utcDayRange, toEnglishDigits } = require('../../utils/jalaliDate');
|
||||
const {
|
||||
namesMatch,
|
||||
normalizePersonName,
|
||||
@@ -38,13 +39,25 @@ const allocatePlaceholderNationalId = async (phoneNumber) => {
|
||||
const base = `TMP${(phoneNumber || '').replace(/\D/g, '').slice(-10) || Date.now().toString().slice(-10)}`;
|
||||
let candidate = base.padEnd(10, '0').slice(0, 10);
|
||||
let i = 0;
|
||||
while (await User.exists({ nationalIdCode: candidate })) {
|
||||
while ((await User.exists({ nationalIdCode: candidate })) || (await Professor.exists({ nationalIdCode: candidate }))) {
|
||||
i += 1;
|
||||
candidate = `${base.slice(0, 7)}${String(i).padStart(3, '0')}`.slice(0, 10);
|
||||
}
|
||||
return candidate;
|
||||
};
|
||||
|
||||
const allocatePlaceholderPhone = async (seed) => {
|
||||
const baseDigits = (seed || Date.now().toString()).replace(/\D/g, '');
|
||||
const suffix = baseDigits.slice(-7).padStart(7, '0');
|
||||
let candidate = `0999${suffix}`.slice(0, 11);
|
||||
let i = 0;
|
||||
while ((await User.exists({ phoneNumber: candidate })) || (await Professor.exists({ phoneNumber: candidate }))) {
|
||||
i += 1;
|
||||
candidate = `0999${String(i).padStart(7, '0')}`.slice(0, 11);
|
||||
}
|
||||
return candidate;
|
||||
};
|
||||
|
||||
const findExistingUser = async ({ nationalIdCode, phoneNumber, name }) => {
|
||||
if (nationalIdCode) {
|
||||
const byId = await User.findOne({ nationalIdCode });
|
||||
@@ -379,18 +392,134 @@ const migrateEmbeddedPaymentTransactions = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const upsertProfessorAndUser = async (profInput, professorRole, userRole, stats, warnings) => {
|
||||
const name = normalizePersonName(profInput.name);
|
||||
const surname = normalizePersonName(profInput.surname);
|
||||
const fullName = mergeFullName(name, surname);
|
||||
|
||||
let phoneNumber = normalizePhone(profInput.phoneNumber || profInput.phone);
|
||||
let nationalIdCode = normalizeNationalId(profInput.nationalIdCode || profInput.nationalId);
|
||||
const cardNumber = profInput.cardNumber ? String(profInput.cardNumber).replace(/\D/g, '').trim() : undefined;
|
||||
const shabaNumber = (profInput.shabaNumber || profInput.iban)
|
||||
? String(profInput.shabaNumber || profInput.iban).replace(/[^0-9A-Za-z]/g, '').trim()
|
||||
: undefined;
|
||||
const email = profInput.email ? String(profInput.email).trim().toLowerCase() : undefined;
|
||||
|
||||
if (!fullName && !phoneNumber && !nationalIdCode) {
|
||||
warnings.push({ reason: 'missing_identity', professor: profInput });
|
||||
stats.professorsSkipped += 1;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!nationalIdCode) {
|
||||
nationalIdCode = await allocatePlaceholderNationalId(phoneNumber || name);
|
||||
warnings.push({
|
||||
reason: 'placeholder_national_id',
|
||||
professor: { name: fullName, phoneNumber, nationalIdCode }
|
||||
});
|
||||
}
|
||||
|
||||
if (!phoneNumber) {
|
||||
phoneNumber = await allocatePlaceholderPhone(nationalIdCode || name);
|
||||
warnings.push({
|
||||
reason: 'placeholder_phone',
|
||||
professor: { name: fullName, phoneNumber, nationalIdCode }
|
||||
});
|
||||
}
|
||||
|
||||
let existingProf = null;
|
||||
if (nationalIdCode) {
|
||||
existingProf = await Professor.findOne({ nationalIdCode });
|
||||
}
|
||||
if (!existingProf && phoneNumber) {
|
||||
existingProf = await Professor.findOne({ phoneNumber });
|
||||
}
|
||||
if (!existingProf && fullName) {
|
||||
const allProfs = await Professor.find({});
|
||||
existingProf = allProfs.find((p) => namesMatch(`${p.name} ${p.surname}`, fullName) || namesMatch(p.name, name));
|
||||
}
|
||||
|
||||
let professorDoc;
|
||||
if (existingProf) {
|
||||
if (name) existingProf.name = name;
|
||||
if (surname) existingProf.surname = surname;
|
||||
if (cardNumber) existingProf.cardNumber = cardNumber;
|
||||
if (shabaNumber) existingProf.shabaNumber = shabaNumber;
|
||||
if (email && !existingProf.email) existingProf.email = email;
|
||||
if (profInput.bio && !existingProf.bio) existingProf.bio = profInput.bio;
|
||||
await existingProf.save();
|
||||
professorDoc = existingProf;
|
||||
stats.professorsUpdated += 1;
|
||||
} else {
|
||||
professorDoc = await Professor.create({
|
||||
name: name || fullName,
|
||||
surname: surname || '',
|
||||
nationalIdCode,
|
||||
phoneNumber,
|
||||
cardNumber: cardNumber || undefined,
|
||||
shabaNumber: shabaNumber || undefined,
|
||||
email: email || undefined,
|
||||
bio: profInput.bio || undefined,
|
||||
isActive: true
|
||||
});
|
||||
stats.professorsCreated += 1;
|
||||
}
|
||||
|
||||
const targetRole = professorRole || userRole;
|
||||
let user = await findExistingUser({ nationalIdCode, phoneNumber, name: fullName });
|
||||
if (user) {
|
||||
if (fullName && !user.name) user.name = fullName;
|
||||
if (cardNumber && !user.cardNumber) user.cardNumber = cardNumber;
|
||||
if (shabaNumber && !user.shabaNumber) user.shabaNumber = shabaNumber;
|
||||
if (email && !user.email) user.email = email;
|
||||
if (targetRole && String(user.role) === String(userRole?._id)) {
|
||||
user.role = targetRole._id;
|
||||
}
|
||||
await user.save();
|
||||
stats.studentsUpdated += 1;
|
||||
} else {
|
||||
const username = await allocateUniqueUsername();
|
||||
const plainPassword = generateSimplePassword();
|
||||
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
||||
|
||||
user = await User.create({
|
||||
name: fullName || `استاد ${phoneNumber}`,
|
||||
nationalIdCode,
|
||||
phoneNumber,
|
||||
cardNumber: cardNumber || undefined,
|
||||
shabaNumber: shabaNumber || undefined,
|
||||
email: email || undefined,
|
||||
username,
|
||||
passwordHash,
|
||||
role: targetRole._id,
|
||||
preferredMessenger: ['SMS'],
|
||||
isActive: true
|
||||
});
|
||||
stats.studentsCreated += 1;
|
||||
}
|
||||
|
||||
return { professor: professorDoc, user };
|
||||
};
|
||||
|
||||
const importData = async (rawPayload) => {
|
||||
const payload = normalizeImportPayload(rawPayload);
|
||||
if (!payload || !Array.isArray(payload.courses)) {
|
||||
throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload: courses array required');
|
||||
if (!payload || (!Array.isArray(payload.courses) && !Array.isArray(payload.professors))) {
|
||||
throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload: courses or professors array required');
|
||||
}
|
||||
|
||||
const userRole = await Role.findOne({ name: 'User' });
|
||||
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
||||
let professorRole = await Role.findOne({ name: 'Professor' });
|
||||
if (!professorRole) {
|
||||
professorRole = userRole;
|
||||
}
|
||||
|
||||
await migrateEmbeddedPaymentTransactions();
|
||||
|
||||
const stats = {
|
||||
professorsCreated: 0,
|
||||
professorsUpdated: 0,
|
||||
professorsSkipped: 0,
|
||||
coursesCreated: 0,
|
||||
coursesReused: 0,
|
||||
classesCreated: 0,
|
||||
@@ -411,7 +540,13 @@ const importData = async (rawPayload) => {
|
||||
};
|
||||
const warnings = [];
|
||||
|
||||
for (const courseInput of payload.courses) {
|
||||
const professors = Array.isArray(payload.professors) ? payload.professors : [];
|
||||
for (const profInput of professors) {
|
||||
await upsertProfessorAndUser(profInput, professorRole, userRole, stats, warnings);
|
||||
}
|
||||
|
||||
const courses = Array.isArray(payload.courses) ? payload.courses : [];
|
||||
for (const courseInput of courses) {
|
||||
const title = String(courseInput.title || '').trim();
|
||||
if (!title) continue;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user