// /components/professors/professorService.js const Professor = require('./professorModel'); const User = require('../users/userModel'); const Role = require('../roles/roleModel'); 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, 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, bio: data.bio ? String(data.bio).trim() : undefined }; const existing = await Professor.findOne({ $or: [ { nationalIdCode: payload.nationalIdCode }, { phoneNumber: payload.phoneNumber }, ...(payload.email ? [{ email: payload.email }] : []) ] }); if (existing) { throw new AppError('PROFESSOR_ALREADY_EXISTS'); } const professor = await Professor.create(payload); eventEmitter.emit(EVENT_NAMES.PROFESSOR_CREATED, { professorId: professor._id, name: `${professor.name} ${professor.surname}` }); return professor; }; const createProfessorFromUser = async (userId, additionalData = {}) => { if (!userId) { throw new AppError('VALIDATION_FAILED', null, 'User ID is required'); } const user = await User.findById(userId); if (!user) { throw new AppError('USER_NOT_FOUND'); } const professorRole = await Role.findOne({ name: 'Professor' }); if (!professorRole) { throw new AppError('DEFAULT_ROLE_NOT_FOUND', null, 'Professor role not found'); } // Update user role to Professor if not already if (String(user.role) !== String(professorRole._id)) { user.role = professorRole._id; await user.save(); } // Determine name and surname let firstName = String(additionalData.name || '').trim(); let lastName = String(additionalData.surname || '').trim(); if (!firstName && !lastName) { const rawName = String(user.name || '').trim(); const parts = rawName.split(/\s+/); if (parts.length > 1) { firstName = parts[0]; lastName = parts.slice(1).join(' '); } else { firstName = rawName || 'استاد'; lastName = rawName || 'استاد'; } } else if (!lastName && firstName) { lastName = firstName; } else if (!firstName && lastName) { firstName = lastName; } let nationalIdCode = String(additionalData.nationalIdCode || additionalData.nationalId || user.nationalIdCode || '').trim(); const phoneNumber = String(additionalData.phoneNumber || additionalData.phone || user.phoneNumber || '').trim(); if (!nationalIdCode) { nationalIdCode = await allocatePlaceholderNationalId(phoneNumber); } const email = additionalData.email ? String(additionalData.email).trim().toLowerCase() : (user.email ? String(user.email).trim().toLowerCase() : undefined); const cardNumber = additionalData.cardNumber ? String(additionalData.cardNumber).trim() : (user.cardNumber ? String(user.cardNumber).trim() : undefined); const shabaNumber = additionalData.shabaNumber || additionalData.iban ? String(additionalData.shabaNumber || additionalData.iban).trim() : (user.shabaNumber ? String(user.shabaNumber).trim() : undefined); const bio = additionalData.bio ? String(additionalData.bio).trim() : undefined; let expertise = []; if (Array.isArray(additionalData.expertise)) { expertise = additionalData.expertise.map(String).map(s => s.trim()).filter(Boolean); } else if (typeof additionalData.expertise === 'string' && additionalData.expertise.trim()) { expertise = additionalData.expertise.split(',').map(s => s.trim()).filter(Boolean); } // Check if a Professor record already exists for this nationalIdCode or phoneNumber let professor = await Professor.findOne({ $or: [ { nationalIdCode }, { phoneNumber } ] }); if (professor) { professor.name = firstName; professor.surname = lastName; if (email) professor.email = email; if (cardNumber) professor.cardNumber = cardNumber; if (shabaNumber) professor.shabaNumber = shabaNumber; if (bio) professor.bio = bio; if (expertise.length > 0) professor.expertise = expertise; professor.isActive = true; await professor.save(); } else { professor = await Professor.create({ name: firstName, surname: lastName, nationalIdCode, phoneNumber, email, cardNumber, shabaNumber, bio, expertise, isActive: true }); } eventEmitter.emit(EVENT_NAMES.PROFESSOR_CREATED, { professorId: professor._id, name: `${professor.name} ${professor.surname}` }); const updatedUser = await User.findById(user._id) .select('-passwordHash -refreshTokens') .populate({ path: 'role', select: 'name permissions' }) .lean(); return { professor, user: updatedUser }; }; const getProfessorById = async (id) => { const professor = await Professor.findById(id).populate('courses', 'title type price'); if (!professor) { throw new AppError('PROFESSOR_NOT_FOUND'); } return professor; }; const getAllProfessors = async (queryParams) => { const { page, limit, skip, sort } = parsePaginationAndSort(queryParams); const filter = buildFilterQuery(queryParams, ['name', 'surname', 'nationalIdCode', 'phoneNumber', 'email', 'expertise']); const [professors, totalCount] = await Promise.all([ Professor.find(filter).populate('courses', 'title').sort(sort).skip(skip).limit(limit), Professor.countDocuments(filter) ]); const meta = calculateMeta(totalCount, page, limit); return { data: professors, meta }; }; const updateProfessor = async (id, updateData) => { const professor = await Professor.findById(id); if (!professor) { throw new AppError('PROFESSOR_NOT_FOUND'); } const payload = { ...updateData }; if (payload.nationalIdCode !== undefined || payload.nationalId !== undefined) { payload.nationalIdCode = String(payload.nationalIdCode || payload.nationalId || '').trim(); delete payload.nationalId; } if (payload.phoneNumber !== undefined || payload.phone !== undefined) { payload.phoneNumber = String(payload.phoneNumber || payload.phone || '').trim(); delete payload.phone; } if (payload.name !== undefined) payload.name = String(payload.name).trim(); if (payload.surname !== undefined) payload.surname = String(payload.surname).trim(); if (payload.email !== undefined) { payload.email = payload.email ? String(payload.email).trim().toLowerCase() : undefined; } if (payload.cardNumber !== undefined) { payload.cardNumber = payload.cardNumber ? String(payload.cardNumber).trim() : undefined; } if (payload.shabaNumber !== undefined || payload.iban !== undefined) { const val = payload.shabaNumber || payload.iban; payload.shabaNumber = val ? String(val).trim() : undefined; delete payload.iban; } if (payload.bio !== undefined) { payload.bio = payload.bio ? String(payload.bio).trim() : undefined; } Object.assign(professor, payload); await professor.save(); return professor; }; const deleteProfessor = async (id) => { const professor = await Professor.findById(id); if (!professor) { throw new AppError('PROFESSOR_NOT_FOUND'); } await Professor.findByIdAndDelete(id); return null; }; const searchProfessors = async (queryParams) => { return getAllProfessors(queryParams); }; module.exports = { createProfessor, createProfessorFromUser, getProfessorById, getAllProfessors, updateProfessor, deleteProfessor, searchProfessors };