HEX
Server: Apache/2.4.58 (Ubuntu)
System: Linux Bradford-Sitios 6.14.0-1017-azure #17~24.04.1-Ubuntu SMP Mon Dec 1 20:10:50 UTC 2025 x86_64
User: www-data (33)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
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;
    }
}