fix(professors, settings): add auto-migration for legacy professors, deep populate user in class/session/course services, and fix SMS bypass number persistence

This commit is contained in:
2026-08-26 05:17:41 +03:30
parent 0a022c0917
commit 43404f2dfc
12 changed files with 337 additions and 57 deletions
+54 -12
View File
@@ -61,6 +61,7 @@ const normalizeNumberOfSessions = (value) => {
};
const enrichClassForDisplay = (cls) => {
if (!cls) return cls;
const tuitionFee = cls.tuitionFee || 0;
const discount = cls.hasDiscount ? (cls.discount || 0) : 0;
const finalTuitionFee = Math.max(0, tuitionFee - discount);
@@ -68,7 +69,23 @@ const enrichClassForDisplay = (cls) => {
if (cls.startDate) {
daysUntilStart = Math.ceil((new Date(cls.startDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
}
return { ...cls, finalTuitionFee, daysUntilStart };
let professor = cls.professor;
if (professor && typeof professor === 'object') {
const profUser = professor.user && typeof professor.user === 'object' ? professor.user : null;
const profName = String(profUser?.name || professor.name || '').trim();
const profPhone = String(profUser?.phoneNumber || professor.phoneNumber || '').trim();
const parts = profName.split(/\s+/);
professor = {
...professor,
name: parts[0] || profName || 'استاد',
surname: parts.length > 1 ? parts.slice(1).join(' ') : (professor.surname || ''),
fullName: profName || 'استاد',
phoneNumber: profPhone
};
}
return { ...cls, professor, finalTuitionFee, daysUntilStart };
};
const getAll = async (query = {}) => {
@@ -83,8 +100,13 @@ const getAll = async (query = {}) => {
filter.isDeleted = { $ne: true };
}
if (query.courseId) filter.course = query.courseId;
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
if (query.courseId) {
filter.course = query.courseId;
}
if (query.isActive !== undefined) {
filter.isActive = query.isActive === 'true';
}
const searchTerm = getSearchTerm(query);
if (searchTerm) {
@@ -95,7 +117,10 @@ const getAll = async (query = {}) => {
Class.find(filter)
.select(CLASS_LIST_FIELDS)
.populate({ path: 'course', select: 'title type price sectionCount hoursPerSection' })
.populate({ path: 'professor', select: 'name surname' })
.populate({
path: 'professor',
populate: { path: 'user', select: 'name phoneNumber email nationalIdCode' }
})
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
Class.countDocuments(filter)
]);
@@ -106,8 +131,11 @@ const getAll = async (query = {}) => {
const getOne = async (id) => {
const cls = await Class.findById(id)
.populate({ path: 'course', select: 'title type price sectionCount hoursPerSection' })
.populate({ path: 'professor', select: 'name surname phoneNumber' })
.populate({ path: 'students', select: 'name phoneNumber gender' })
.populate({
path: 'professor',
populate: { path: 'user', select: 'name phoneNumber email nationalIdCode' }
})
.populate({ path: 'students', select: 'name phoneNumber gender' })
.lean();
if (!cls) throw new AppError('CLASS_NOT_FOUND');
return enrichClassForDisplay(cls);
@@ -247,7 +275,10 @@ const getMyClasses = async (userId, query = {}) => {
const [items, total] = await Promise.all([
Class.find(filter)
.populate({ path: 'course', select: 'title type price description' })
.populate({ path: 'professor', select: 'name surname' })
.populate({
path: 'professor',
populate: { path: 'user', select: 'name phoneNumber nationalIdCode' }
})
.skip(skip).limit(limit).sort({ startDate: -1, createdAt: -1 }).lean(),
Class.countDocuments(filter)
]);
@@ -271,7 +302,10 @@ const getPublicClasses = async (query = {}) => {
Class.find(filter)
.select('name course professor capacity tuitionFee hasDiscount discount freeSpots startDate endDate days startTime endTime')
.populate({ path: 'course', select: 'title type description' })
.populate({ path: 'professor', select: 'name surname' })
.populate({
path: 'professor',
populate: { path: 'user', select: 'name phoneNumber' }
})
.skip(skip).limit(limit).sort({ startDate: 1, createdAt: -1 }).lean(),
Class.countDocuments(filter)
]);
@@ -288,7 +322,10 @@ const getPublicOne = async (id) => {
})
.select('name course professor capacity tuitionFee hasDiscount discount freeSpots startDate endDate days startTime endTime isActive')
.populate({ path: 'course', select: 'title type description sectionCount hoursPerSection price' })
.populate({ path: 'professor', select: 'name surname' })
.populate({
path: 'professor',
populate: { path: 'user', select: 'name phoneNumber' }
})
.lean();
if (!cls) throw new AppError('CLASS_NOT_FOUND', null, 'کلاس یافت نشد یا برای ثبت‌نام در دسترس نیست.');
@@ -298,7 +335,10 @@ const getPublicOne = async (id) => {
const sendClassPlanToProfessor = async (classId) => {
const classDoc = await Class.findById(classId)
.populate('course', 'title')
.populate('professor', 'name surname phoneNumber');
.populate({
path: 'professor',
populate: { path: 'user', select: 'name phoneNumber email' }
});
if (!classDoc) {
throw new AppError('CLASS_NOT_FOUND', null, 'کلاس یافت نشد.');
@@ -309,11 +349,13 @@ const sendClassPlanToProfessor = async (classId) => {
}
const professor = classDoc.professor;
if (!professor.phoneNumber) {
const profUser = professor.user && typeof professor.user === 'object' ? professor.user : null;
const phoneNumber = profUser?.phoneNumber || professor.phoneNumber;
if (!phoneNumber) {
throw new AppError('VALIDATION_FAILED', { field: 'phoneNumber' }, 'شماره همراه استاد ثبت نشده است.');
}
const profName = `${professor.name || ''} ${professor.surname || ''}`.trim() || 'استاد';
const profName = profUser?.name || `${professor.name || ''} ${professor.surname || ''}`.trim() || 'استاد';
const className = classDoc.name || classDoc.course?.title || 'کلاس';
const classDays = formatClassDaysFromIndexes(classDoc.days) || 'طبق هماهنگی';
const classTimes = (classDoc.startTime && classDoc.endTime)