// /components/dataImport/dataImportService.js 'use strict'; const bcrypt = require('bcryptjs'); const Course = require('../courses/courseModel'); const Class = require('../classes/classModel'); const User = require('../users/userModel'); const Role = require('../roles/roleModel'); const AppError = require('../../utils/AppError'); const { generateUsername, generateSimplePassword } = require('../../utils/credentials'); const { mergeFullName, normalizeGender } = require('../../utils/userProfile'); const toEnglishDigits = (value) => String(value ?? '') .replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d)) .replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d)); const normalizePhone = (raw) => { if (raw == null || raw === '') return ''; let digits = toEnglishDigits(raw).replace(/\D/g, ''); if (digits.startsWith('98') && digits.length === 12) digits = `0${digits.slice(2)}`; if (digits.length === 10 && digits.startsWith('9')) digits = `0${digits}`; return digits; }; const normalizeNationalId = (raw) => { if (raw == null || raw === '') return ''; return toEnglishDigits(raw).replace(/\D/g, ''); }; const allocateUniqueUsername = async () => { for (let attempt = 0; attempt < 20; attempt += 1) { const username = generateUsername(); if (!(await User.exists({ username }))) return username; } throw new AppError('INTERNAL_SERVER_ERROR', null, 'Could not generate a unique username'); }; 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 })) { i += 1; candidate = `${base.slice(0, 7)}${String(i).padStart(3, '0')}`.slice(0, 10); } return candidate; }; const findExistingUser = async ({ nationalIdCode, phoneNumber }) => { if (nationalIdCode) { const byId = await User.findOne({ nationalIdCode }); if (byId) return byId; } if (phoneNumber) { const byPhone = await User.findOne({ phoneNumber }); if (byPhone) return byPhone; } return null; }; const applyStudentProfile = (user, student) => { const name = mergeFullName(student.name, student.surname); if (name) user.name = name; const gender = normalizeGender(student.gender); if (gender) user.gender = gender; const optionalFields = [ 'address', 'birthCertificateNumber', 'postalCode', 'placeOfIssue', 'fatherName', 'education', 'parentPhoneNumber' ]; for (const field of optionalFields) { if (student[field] && !user[field]) { user[field] = String(student[field]).trim(); } } if (student.birthDate && !user.birthDate) { const date = new Date(student.birthDate); if (!Number.isNaN(date.getTime())) user.birthDate = date; } }; const upsertStudent = async (student, userRole, stats, warnings) => { const phoneNumber = normalizePhone(student.phoneNumber || student.phone || student.parentPhoneNumber); let nationalIdCode = normalizeNationalId(student.nationalIdCode || student.nationalId); if (!phoneNumber && !nationalIdCode) { warnings.push({ reason: 'missing_identity', student: { name: student.name } }); stats.studentsSkipped += 1; return null; } let user = await findExistingUser({ nationalIdCode, phoneNumber }); if (user) { applyStudentProfile(user, student); if (phoneNumber && !user.phoneNumber) user.phoneNumber = phoneNumber; await user.save(); stats.studentsUpdated += 1; 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); warnings.push({ reason: 'placeholder_national_id', student: { name: student.name, phoneNumber, nationalIdCode } }); } // Collision: national id exists with different phone, or reverse const conflictById = await User.findOne({ nationalIdCode }); if (conflictById) { applyStudentProfile(conflictById, student); await conflictById.save(); stats.studentsUpdated += 1; return conflictById; } const username = await allocateUniqueUsername(); const plainPassword = generateSimplePassword(); const passwordHash = await bcrypt.hash(plainPassword, 10); user = await User.create({ name: mergeFullName(student.name, student.surname) || `کاربر ${phoneNumber}`, gender: normalizeGender(student.gender), nationalIdCode, phoneNumber, address: student.address, birthCertificateNumber: student.birthCertificateNumber, postalCode: student.postalCode, placeOfIssue: student.placeOfIssue, fatherName: student.fatherName, birthDate: student.birthDate ? new Date(student.birthDate) : undefined, education: student.education, parentPhoneNumber: student.parentPhoneNumber ? normalizePhone(student.parentPhoneNumber) : undefined, username, passwordHash, role: userRole._id, preferredMessenger: ['SMS'] }); stats.studentsCreated += 1; return user; }; const importData = async (payload) => { if (!payload || !Array.isArray(payload.courses)) { throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload: courses array required'); } const userRole = await Role.findOne({ name: 'User' }); if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND'); const stats = { coursesCreated: 0, coursesReused: 0, classesCreated: 0, classesReused: 0, studentsCreated: 0, studentsUpdated: 0, studentsSkipped: 0, enrollmentsAdded: 0 }; const warnings = []; for (const courseInput of payload.courses) { const title = String(courseInput.title || '').trim(); if (!title) continue; const type = courseInput.type === 'Private' ? 'Private' : 'General'; let course = await Course.findOne({ title }); if (!course) { course = await Course.create({ title, type, price: Number(courseInput.price) || 0, showOnFrontend: false, description: courseInput.description || '' }); stats.coursesCreated += 1; } else { stats.coursesReused += 1; if (type === 'Private' && course.type !== 'Private') { course.type = 'Private'; await course.save(); } } const classes = Array.isArray(courseInput.classes) ? courseInput.classes : []; for (const classInput of classes) { const className = String(classInput.name || '').trim(); if (!className) continue; let cls = await Class.findOne({ name: className, course: course._id }); if (!cls) { cls = await Class.create({ name: className, course: course._id, startDate: classInput.startDate ? new Date(classInput.startDate) : undefined, tuitionFee: Number(classInput.tuitionFee) || Number(courseInput.price) || 0, isActive: true }); stats.classesCreated += 1; } else { stats.classesReused += 1; if (!cls.startDate && classInput.startDate) { cls.startDate = new Date(classInput.startDate); await cls.save(); } } const students = Array.isArray(classInput.students) ? classInput.students : []; for (const student of students) { const user = await upsertStudent(student, userRole, stats, warnings); if (!user) continue; if (!user.courses.map(String).includes(String(course._id))) { user.courses.push(course._id); await user.save(); } if (!cls.students.map(String).includes(String(user._id))) { cls.students.push(user._id); stats.enrollmentsAdded += 1; } } await cls.save(); } } return { stats, warnings }; }; module.exports = { importData };