File: /var/www/matriculas_api_dev/app/Services/ExcelProcessors/StudentsProcessor.php
<?php
namespace App\Services\ExcelProcessors;
use App\Models\Student;
use App\Models\Parents;
use App\Models\Period;
use App\Models\StudentPeriod;
use App\Models\ContractDetail;
use App\Models\StatusContract;
use App\Repositories\ParentRepository;
use App\Repositories\StudentRepository;
use App\Services\ApiInboundService;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class StudentsProcessor extends BaseExcelProcessor
{
/**
* Orden de columnas para mostrar en el frontend.
*/
public function getColumnOrder(): ?array
{
return [
'RUT_ESTUDIANTE',
'NRO_PASAPORTE',
'PRIMER_NOMBRE',
'SEGUNDO_NOMBRE',
'PRIMER_APELLIDO',
'SEGUNDO_APELLIDO',
'RUT_APODERADO',
'NRO_PASAPORTE_APODERADO',
'CURSO_ID',
'LETRA_CURSO_ID',
'GENERO_ID',
'FECHA_NACIMIENTO',
'FECHA_INGRESO',
'CELULAR',
'EMAIL',
'DIRECCION',
'ES_HIJO',
'PAGA_CUOTA_INCORPORACION',
'PERIODO',
];
}
/**
* Procesar una línea del Excel y validar los datos.
*/
public function processLine(array $data, int $lineNumber): array
{
$errors = [];
$warnings = [];
// === Obtener valores ===
$rutStudent = $this->getValue($data, 'RUT_ESTUDIANTE');
$nroPasaporte = $this->getValue($data, 'NRO_PASAPORTE');
$rutParent = $this->getValue($data, 'RUT_APODERADO');
$pasaporteParent = $this->getValue($data, 'NRO_PASAPORTE_APODERADO');
$isPassport = !empty($nroPasaporte);
$isParentPassport = !empty($pasaporteParent);
$validationErrors = $this->validateData($data);
foreach ($validationErrors as $msg) {
$errors[] = $msg;
}
// === Validaciones requeridas: RUT o Pasaporte del estudiante, no ambos ===
if ($isPassport && !empty($rutStudent)) {
$errors[] = "Debe indicar RUT_ESTUDIANTE o NRO_PASAPORTE, no ambos.";
} elseif ($isPassport) {
$studentRepository = new StudentRepository();
$existingStudent = $studentRepository->getByRut(strUpper($nroPasaporte));
} elseif (!empty($rutStudent)) {
if (!validateRut($rutStudent)) {
$errors[] = "RUT del estudiante inválido: {$rutStudent}";
} else {
$formattedRut = formatterRut($rutStudent);
$studentRepository = new StudentRepository();
$existingStudent = $studentRepository->getByRut($formattedRut);
}
} else {
$errors[] = "Debe indicar RUT_ESTUDIANTE o NRO_PASAPORTE.";
}
// === Validar apoderado: RUT o Pasaporte, no ambos ===
if (!empty($rutParent) && $isParentPassport) {
$errors[] = "Debe indicar RUT_APODERADO o NRO_PASAPORTE_APODERADO, no ambos.";
} elseif ($isParentPassport) {
$respositoryParent = new ParentRepository();
$parent = $respositoryParent->getByRut(strUpper($pasaporteParent));
if (!$parent) {
$errors[] = "Pasaporte {$pasaporteParent} no es un apoderado del sistema";
}
} elseif (!empty($rutParent)) {
if (!validateRut($rutParent)) {
$errors[] = "RUT del apoderado inválido: {$rutParent}";
} else {
$formattedRutParent = formatterRut($rutParent);
$respositoryParent = new ParentRepository();
$parent = $respositoryParent->getByRut($formattedRutParent);
if (!$parent) {
$errors[] = "RUT {$rutParent} no es un apoderado del sistema";
}
}
} else {
$errors[] = "Debe indicar RUT_APODERADO o NRO_PASAPORTE_APODERADO.";
}
// === Validar periodo existe y está activo ===
$periodoYear = $this->getValue($data, 'PERIODO');
if ($periodoYear) {
$period = Period::where('period_year', $periodoYear)->first();
if (!$period) {
$errors[] = "El periodo {$periodoYear} no existe en el sistema.";
} elseif (!$period->status || $period->finished) {
$errors[] = "El periodo {$periodoYear} no está activo.";
}
}
// === Resultado ===
if (!empty($errors)) {
return [
'status' => 'ERROR',
'line' => $lineNumber,
'error' => implode('. ', $errors),
];
}
if (!empty($warnings)) {
return [
'status' => 'WARNING',
'line' => $lineNumber,
'warning' => implode('. ', $warnings),
];
}
return [
'status' => 'SUCCESS',
'line' => $lineNumber,
'message' => 'Registro válido',
];
}
/**
* Crear o actualizar el registro del estudiante (sin contrato).
* La creación de contratos se maneja en el confirm() agrupando por apoderado.
*/
public function createOrUpdateRecord(array $data, $period = null): array
{
$nroPasaporte = $this->getValue($data, 'NRO_PASAPORTE');
$isPassport = !empty($nroPasaporte);
if ($isPassport) {
$documentType = 'PASSPORT';
$rutStudent = strUpper($nroPasaporte);
} else {
$documentType = 'RUT';
$rutStudent = formatterRut($this->getValue($data, 'RUT_ESTUDIANTE'));
}
$pasaporteParent = $this->getValue($data, 'NRO_PASAPORTE_APODERADO');
$isParentPassport = !empty($pasaporteParent);
if ($isParentPassport) {
$parentIdentifier = strUpper($pasaporteParent);
} else {
$parentIdentifier = formatterRut($this->getValue($data, 'RUT_APODERADO'));
}
$parent = Parents::where('rut', $parentIdentifier)->first();
$esHijoValue = $this->getValue($data, 'ES_HIJO');
$family = is_null($esHijoValue) ? true : (strUpper($esHijoValue) === 'SI');
$periodoYear = $this->getValue($data, 'PERIODO');
$birthDateRaw = $this->getValue($data, 'FECHA_NACIMIENTO');
$dateEntryRaw = $this->getValue($data, 'FECHA_INGRESO');
// Siempre actualizar TODOS los campos del estudiante
$studentData = [
'document_type' => $documentType,
'enrollment_source' => 'excel',
'first_name' => strUpper($this->getValue($data, 'PRIMER_NOMBRE')),
'second_name' => !empty($this->getValue($data, 'SEGUNDO_NOMBRE')) ? strUpper($this->getValue($data, 'SEGUNDO_NOMBRE')) : null,
'last_name' => strUpper($this->getValue($data, 'PRIMER_APELLIDO')),
'second_last_name' => !empty($this->getValue($data, 'SEGUNDO_APELLIDO')) ? strUpper($this->getValue($data, 'SEGUNDO_APELLIDO')) : null,
'rut' => $rutStudent,
'email' => !empty($this->getValue($data, 'EMAIL')) ? strLower($this->getValue($data, 'EMAIL')) : null,
'mobile' => $this->getValue($data, 'CELULAR') ?? $this->getValue($data, 'TELEFONO'),
'gender_id' => $this->getValue($data, 'GENERO_ID'),
'birth_date' => !empty($birthDateRaw) ? $this->parseFlexibleDate(trim($birthDateRaw)) : null,
'date_entry' => !empty($dateEntryRaw) ? $this->parseFlexibleDate(trim($dateEntryRaw)) : null,
'address' => $this->getValue($data, 'DIRECCION'),
'course_id' => $this->getValue($data, 'CURSO_ID'),
'course_letter_id' => $this->getValue($data, 'LETRA_CURSO_ID'),
'financial_parent_id' => $parent->id,
'academic_parent_id' => $parent->id,
'family' => $family,
'status' => true,
];
$student = Student::updateOrCreate(
['rut' => $rutStudent],
$studentData
);
$action = $student->wasRecentlyCreated ? 'CREATED' : 'UPDATED';
// --- El periodo debe existir y estar activo (ya validado en processLine) ---
$period = Period::where('period_year', $periodoYear)
->where('status', 1)
->where('finished', 0)
->first();
if (!$period) {
return [
'status' => 'ERROR',
'action' => $action,
'model' => $student,
'error' => "El periodo {$periodoYear} no existe o no está activo.",
];
}
// Asociar estudiante al periodo
StudentPeriod::firstOrCreate([
'student_id' => $student->id,
'period_id' => $period->id,
]);
return [
'status' => 'SUCCESS',
'action' => $action,
'model' => $student,
];
}
private function validateData(array $data): array
{
$hasPassport = !empty($data['NRO_PASAPORTE']);
$hasParentPassport = !empty($data['NRO_PASAPORTE_APODERADO']);
// RUT y Pasaporte son mutuamente excluyentes (validado en processLine)
$rules = [
'RUT_ESTUDIANTE' => $hasPassport ? 'nullable|string|max:12' : 'required|string|min:8|max:12',
'NRO_PASAPORTE' => 'nullable|string|max:50',
'PRIMER_NOMBRE' => 'required|string|max:100',
'SEGUNDO_NOMBRE' => 'nullable|string|max:100',
'PRIMER_APELLIDO' => 'required|string|max:100',
'SEGUNDO_APELLIDO' => 'nullable|string|max:100',
'RUT_APODERADO' => $hasParentPassport ? 'nullable|string|max:12' : 'required|string|min:8|max:12',
'NRO_PASAPORTE_APODERADO' => 'nullable|string|max:50',
'CURSO_ID' => 'required|integer|exists:courses,id',
'LETRA_CURSO_ID' => 'nullable|integer|exists:courses_letters,id',
'GENERO_ID' => 'nullable|integer|exists:genders,id',
'FECHA_NACIMIENTO' => 'nullable|string',
'FECHA_INGRESO' => 'nullable|string',
'DIRECCION' => 'nullable|string|max:255',
'ES_HIJO' => 'nullable|string|in:SI,NO,si,no,Si,No',
'PAGA_CUOTA_INCORPORACION' => 'nullable|string|in:SI,NO,si,no,Si,No',
'PERIODO' => 'required|integer|min:2020|max:2100',
];
$messages = [
// --- RUT ---
'RUT_ESTUDIANTE.required' => 'El RUT del estudiante es obligatorio.',
'RUT_ESTUDIANTE.min' => 'El RUT del estudiante debe tener al menos 8 caracteres.',
'RUT_ESTUDIANTE.max' => 'El RUT del estudiante no debe superar los 12 caracteres.',
'RUT_APODERADO.required' => 'El RUT del apoderado es obligatorio (o indique NRO_PASAPORTE_APODERADO).',
'RUT_APODERADO.min' => 'El RUT del apoderado debe tener al menos 8 caracteres.',
'RUT_APODERADO.max' => 'El RUT del apoderado no debe superar los 12 caracteres.',
'NRO_PASAPORTE_APODERADO.max' => 'El pasaporte del apoderado no debe superar los 50 caracteres.',
// --- Nombres ---
'PRIMER_NOMBRE.required' => 'El primer nombre del estudiante es obligatorio.',
'PRIMER_APELLIDO.required' => 'El primer apellido del estudiante es obligatorio.',
// --- Curso ---
'CURSO_ID.required' => 'El CURSO_ID es obligatorio.',
'CURSO_ID.integer' => 'El CURSO_ID debe ser un número entero.',
'CURSO_ID.exists' => 'El CURSO_ID no existe en la base de datos.',
'LETRA_CURSO_ID.integer' => 'La LETRA_CURSO_ID debe ser un número entero.',
'LETRA_CURSO_ID.exists' => 'La LETRA_CURSO_ID no existe en la base de datos.',
// --- Género ---
'GENERO_ID.integer' => 'El GÉNERO_ID debe ser un número entero.',
'GENERO_ID.exists' => 'El GÉNERO_ID no existe en la base de datos.',
// --- Fechas ---
'FECHA_NACIMIENTO.string' => 'El campo FECHA_NACIMIENTO debe ser una cadena válida.',
'FECHA_INGRESO.string' => 'El campo FECHA_INGRESO debe ser una cadena válida.',
// --- Datos opcionales ---
'DIRECCION.max' => 'La dirección no puede superar los 255 caracteres.',
// --- Family y Periodo ---
'ES_HIJO.in' => 'El campo ES_HIJO debe ser SI o NO.',
'PAGA_CUOTA_INCORPORACION.in' => 'El campo PAGA_CUOTA_INCORPORACION debe ser SI o NO.',
'PERIODO.required' => 'El PERIODO es obligatorio.',
'PERIODO.integer' => 'El PERIODO debe ser un número entero (año).',
'PERIODO.min' => 'El PERIODO debe ser al menos 2020.',
'PERIODO.max' => 'El PERIODO no puede ser mayor a 2100.',
];
$validator = Validator::make($data, $rules, $messages);
$errors = $validator->fails() ? $validator->errors()->all() : [];
// 🔹 Validación flexible de fechas
if (!empty($data['FECHA_NACIMIENTO'])) {
$fecha = trim($data['FECHA_NACIMIENTO']);
if (!$this->parseFlexibleDate($fecha)) {
$errors[] = "La fecha de nacimiento '{$fecha}' no tiene un formato válido (se admite d-m-Y, Y-m-d o d/m/Y).";
}
}
if (!empty($data['FECHA_INGRESO'])) {
$fecha = trim($data['FECHA_INGRESO']);
if (!$this->parseFlexibleDate($fecha)) {
$errors[] = "La fecha de ingreso '{$fecha}' no tiene un formato válido (se admite d-m-Y, Y-m-d o d/m/Y).";
}
}
return $errors;
}
}