File: /var/www/api_matriculas/app/Services/StudentService.php
<?php
namespace App\Services;
use App\Http\Resources\StudentResource;
use App\Repositories\ParentRepository;
use App\Repositories\StudentRepository;
use App\Repositories\PeriodRepository;
use App\Repositories\ContractRepository;
use App\Services\ApiInboundService;
use App\Services\DebtCalculationService;
use App\Models\StudentPeriod;
use App\Models\Contract;
use App\Models\StatusContract;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;
class StudentService
{
protected $studentRepository;
public function __construct(
StudentRepository $studentRepository
)
{
$this->studentRepository = $studentRepository;
}
public function list($parentId = null)
{
$registers = $this->studentRepository->getAll($parentId);
if ($registers->count() < 1) {
return [];
}
return $registers->map(function ($register) {
return new StudentResource($register, true);
});
}
public function store($request)
{
$isPassport = ($request->document_type ?? 'RUT') === 'PASSPORT';
$rut = $isPassport ? strUpper($request->rut) : formatterRut($request->rut, false);
$validateField = $this->studentRepository->getByRut($rut);
if ($validateField) {
$docLabel = $isPassport ? 'Pasaporte' : 'Rut';
throw new Exception("$docLabel de estudiante ya existe en la base de datos", 409);
}
DB::beginTransaction();
try {
// Fase 1: Crear al estudiante
$student = $this->studentRepository->create((object)$request);
if (!$student) {
throw new Exception("Ocurrió un problema al registrar al estudiante", 500);
}
// Determinar periodo: el seleccionado en el formulario, o el activo
$periodRepository = new PeriodRepository();
$period = !empty($request->period)
? $periodRepository->getById($request->period)
: $periodRepository->getActive();
if (!$period) {
throw new Exception("No existe un periodo activo configurado para asignar la matrícula", 400);
}
StudentPeriod::firstOrCreate([
'student_id' => $student->id,
'period_id' => $period->id,
]);
// Fase 2: Agrupar por apoderado y crear contratos (como v1 API)
$contractRepo = app(ContractRepository::class);
$debtService = app(DebtCalculationService::class);
$parentId = $student->financial_parent_id;
$periodId = $period->id;
$parent = \App\Models\Parents::find($parentId);
if ($parent) {
// Determinar si es Playgroup o Regular
$pgCourseIds = [ApiInboundService::PG_COURSE_ID, ApiInboundService::JI_COURSE_ID];
$isPlayGroup = in_array($student->course_id, $pgCourseIds);
$enrollmentType = $isPlayGroup ? 'playgroup' : 'regular';
$inCourseStatusId = $contractRepo->getStatus('in_course', 'contract');
// A. Buscar si el apoderado ya tiene un contrato de este tipo este año
$existingContract = Contract::where('financial_parent_id', $parentId)
->where('period_id', $periodId)
->where('enrollment_type', $enrollmentType)
->where('status_contract_id', $inCourseStatusId)
->first();
// B. Si no existe, crear el contrato nuevo
if (!$existingContract) {
$periodYear = $period->period_year ?? date('Y');
$apiService = app(ApiInboundService::class);
$contractCode = $apiService->generateContractCode($periodYear, $parentId, $periodId);
$isPassportParent = ($parent->document_type ?? 'RUT') === 'PASSPORT';
$signatureStatus = $isPlayGroup ? 'not_required' : ($isPassportParent ? 'pending_manual' : 'pending');
$existingContract = $contractRepo->findOrcreate($contractCode, $parent, $periodId);
$existingContract->enrollment_type = $enrollmentType;
$existingContract->status_contract_id = $inCourseStatusId;
$existingContract->status_payment_id = $contractRepo->getStatus('pending', 'payment');
$existingContract->status_signature_id = $contractRepo->getStatus($signatureStatus, 'signature');
$existingContract->observation = $isPlayGroup
? 'Contrato Play Group generado desde formulario web.'
: 'Contrato generado desde formulario web.';
$existingContract->save();
Log::info("Contrato {$existingContract->code_contract} creado para apoderado {$parentId}");
}
// C. Generar deudas para este alumno
// Leemos si paga incorporación directamente del request
$paysIncFee = isset($request->pays_incorporation_fee) ? (bool)$request->pays_incorporation_fee : true;
$debts = $debtService->calculateStudentDebts($student->id, $periodId, $existingContract->id, null, $paysIncFee);
Log::info("Estudiante {$student->id} agregado a contrato con " . count($debts) . " deudas");
try {
$debtService->recalculateFamilySiblingDebts($parentId, $periodId);
}
catch (\Exception $e) {
Log::warning("Error al recalcular hermanos: " . $e->getMessage());
throw new Exception("Error al recalcular hermanos: " . $e->getMessage());
}
}
DB::commit();
return $student;
}
catch (\Exception $e) {
DB::rollBack();
Log::warning("Error al crear estudiante: " . $e->getMessage());
throw $e;
}
}
public function update($request, $id)
{
$register = $this->show($id, true, true);
$isPassport = ($request->document_type ?? 'RUT') === 'PASSPORT';
$rut = $isPassport ? trim($request->rut) : formatterRut($request->rut, false);
$validateField = $this->studentRepository->getByRut($rut, $id);
if ($validateField) {
$docLabel = $isPassport ? 'Pasaporte' : 'Rut';
throw new Exception("$docLabel de estudiante ya existe en la base de datos", 409);
}
$update = $this->studentRepository->update($register, $request);
if (!$update) {
throw new Exception("Ocurrió un problema al actualizar registro", 500);
}
return true;
}
public function show($id, $allData = true, $allDataEdit = false)
{
$register = $this->studentRepository->getById($id);
if (!$register) {
throw new Exception("Estudiante no existe o fue eliminado", 400);
}
if ($allDataEdit) {
return $register;
}
return (object)(new StudentResource($register, $allData))->resolve();
}
/**
* createOrUpdate masivo para API v1
*/
public function createOrUpdateBatch(array $students, $periodId = null)
{
$parentRepository = new ParentRepository();
$results = [];
foreach ($students as $studentData) {
try {
// Resolve parent by RUT
$parentRutValue = $studentData['parent_rut'] ?? null;
if (!$parentRutValue) {
throw new Exception("parent_rut es requerido");
}
$parentRut = strUpper(formatterRut($parentRutValue, false));
$parent = $parentRepository->getByRut($parentRut);
// Fallback: buscar sin importar mayúsculas/minúsculas
if (!$parent) {
$parent = \App\Models\Parents::whereRaw('UPPER(rut) = ?', [$parentRut])
->where('deleted', false)->first();
}
if (!$parent) {
throw new Exception("Apoderado con RUT {$parentRutValue} no encontrado en el sistema");
}
// Resolver document_type: si viene student_passport, es PASSPORT
$isPassport = ($studentData['document_type'] ?? 'RUT') === 'PASSPORT';
$result = $this->studentRepository->createOrUpdateByRut(
(object)$studentData,
$parent->id,
$isPassport
);
$results[] = [
'success' => true,
'id' => $result['student']->id,
'key' => $studentData['student_rut'],
'operation' => $result['operation'],
'parent_id' => $parent->id,
'course_id' => $result['student']->course_id,
'errors' => '',
];
}
catch (Exception $e) {
$results[] = [
'success' => false,
'id' => null,
'key' => $studentData['student_rut'] ?? '',
'operation' => 'error',
'parent_id' => null,
'course_id' => null,
'errors' => $e->getMessage(),
];
}
}
return $results;
}
/**
* Obtener estudiante por RUT (API v1)
*/
public function getByRut($rut)
{
$register = $this->studentRepository->getByRutGlobal($rut);
if (!$register) {
throw new Exception("Estudiante no encontrado", 404);
}
return (object)(new StudentResource($register, true))->resolve();
}
public function delete($id)
{
$register = $this->show($id, true, true);
$register->deleted = 1;
$register->deleted_at = now();
$register->user_deleted = auth()->user()->id;
if (!$register->save()) {
throw new Exception("Ocurrió un problema al eliminar registro", 500);
}
return true;
}
}