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/api_matriculas/app/Services/ApiInboundService.php
<?php

namespace App\Services;

use App\Models\Contract;
use App\Models\ContractDetail;
use App\Models\Course;
use App\Models\Parents;
use App\Models\Period;
use App\Models\Student;
use App\Models\SyncLog;
use App\Repositories\ContractRepository;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;

class ApiInboundService
{
    protected $contractRepository;

    // Play Group / Jardin course codes (same treatment: no signature required)
    const PG_COURSE_CODE = 'PG';
    const PG_COURSE_ID = 14;
    const JI_COURSE_CODE = 'JI';
    const JI_COURSE_ID = 1;

    public function __construct(ContractRepository $contractRepository)
    {
        $this->contractRepository = $contractRepository;
    }

    /**
     * Trigger automático de contrato al cargar un alumno via API
     * - Si curso = PG → contrato separado con enrollment_type = 'playgroup', firma = 'not_required'
     * - Si curso != PG → contrato separado con enrollment_type = 'permanent', firma = 'pending'
     * PG y no-PG van a contratos distintos del mismo apoderado
     */
    public function triggerContractForStudent($studentId, $parentId, $courseId, $periodId, $forceEnrollmentType = null)
    {
        DB::beginTransaction();

        try {
            $student = Student::find($studentId);
            if (!$student) {
                throw new Exception("Estudiante ID {$studentId} no encontrado");
            }

            $parent = Parents::find($parentId);
            if (!$parent) {
                throw new Exception("Apoderado ID {$parentId} no encontrado");
            }

            // Determine enrollment type based on course or forced type
            if ($forceEnrollmentType) {
                $isPlayGroup = false;
                $enrollmentType = $forceEnrollmentType;
            } else {
                $isPlayGroup = $this->isPlayGroupCourse($courseId);
                $enrollmentType = $isPlayGroup ? 'playgroup' : 'permanent';
            }

            // Check if an active (in_course) contract already exists for this enrollment type + period
            $inCourseStatusId = $this->contractRepository->getStatus('in_course', 'contract');

            $existingContract = Contract::where('financial_parent_id', $parentId)
                ->where('period_id', $periodId)
                ->where('enrollment_type', $enrollmentType)
                ->where('status_contract_id', $inCourseStatusId)
                ->first();

            if ($existingContract) {
                // Solo agregar alumno si el contrato no tiene pagos ni firma
                $pendingPaymentId = $this->contractRepository->getStatus('pending', 'payment');
                $pendingSignatureId = $this->contractRepository->getStatus('pending', 'signature');
                $notRequiredSignatureId = $this->contractRepository->getStatus('not_required', 'signature');

                $canAddStudent = $existingContract->status_payment_id == $pendingPaymentId
                    && in_array($existingContract->status_signature_id, [$pendingSignatureId, $notRequiredSignatureId]);

                if (!$canAddStudent) {
                    DB::commit();

                    Log::info("Contrato {$existingContract->code_contract} ya tiene pagos o firma. No se puede agregar alumno.", [
                        'contract_id' => $existingContract->id,
                        'student_id' => $studentId,
                        'status_payment_id' => $existingContract->status_payment_id,
                        'status_signature_id' => $existingContract->status_signature_id,
                    ]);

                    return [
                        'success' => false,
                        'action' => 'skipped_has_payments_or_signature',
                        'contract_code' => $existingContract->code_contract,
                        'contract_id' => $existingContract->id,
                        'enrollment_type' => $enrollmentType,
                        'message' => 'El contrato ya tiene pagos o firma. No se puede agregar alumno.',
                    ];
                }

                $this->addStudentToContract($existingContract, $student, $periodId);

                DB::commit();

                $this->logSync('api_inbound', 'inbound', 'contract', $existingContract->id, 'success', [
                    'action' => 'student_added_to_existing_contract',
                    'student_id' => $studentId,
                    'contract_code' => $existingContract->code_contract,
                ]);

                return [
                    'success' => true,
                    'action' => 'student_added',
                    'contract_code' => $existingContract->code_contract,
                    'contract_id' => $existingContract->id,
                    'enrollment_type' => $existingContract->enrollment_type,
                ];
            }

            // Si ya existe un contrato finalizado para este tipo/período → no crear otro
            $finishedStatusId = $this->contractRepository->getStatus('finished', 'contract');

            $finishedContract = Contract::where('financial_parent_id', $parentId)
                ->where('period_id', $periodId)
                ->where('enrollment_type', $enrollmentType)
                ->where('status_contract_id', $finishedStatusId)
                ->first();

            if ($finishedContract) {
                DB::commit();

                Log::info("Contrato finalizado ya existe para apoderado {$parentId}, período {$periodId}, tipo {$enrollmentType}. No se crea nuevo.", [
                    'contract_id' => $finishedContract->id,
                    'contract_code' => $finishedContract->code_contract,
                    'student_id' => $studentId,
                ]);

                return [
                    'success' => true,
                    'action' => 'skipped_finished',
                    'contract_code' => $finishedContract->code_contract,
                    'contract_id' => $finishedContract->id,
                    'enrollment_type' => $enrollmentType,
                    'message' => 'Contrato finalizado existente. No se creó uno nuevo.',
                ];
            }

            // No active/finished contract — create a new one (cancelled ones are ignored)

            // Generate unique contract code (sin sufijo -PG, correlativo estándar)
            $periodYear = Period::where('id', $periodId)->value('period_year') ?? date('Y');
            $contractCode = $this->generateContractCode($periodYear, $parent->id, $periodId);

            // Create new contract
            $isPassport = ($parent->document_type ?? 'RUT') === 'PASSPORT';
            $signatureStatus = $isPlayGroup ? 'not_required' : ($isPassport ? 'pending_manual' : 'pending');

            $contract = $this->contractRepository->findOrcreate($contractCode, $parent, $periodId);

            // Update enrollment type and signature status
            $contract->enrollment_type = $enrollmentType;
            $contract->status_contract_id = $this->contractRepository->getStatus('in_course', 'contract');
            $contract->status_payment_id = $this->contractRepository->getStatus('pending', 'payment');
            $contract->status_signature_id = $this->contractRepository->getStatus($signatureStatus, 'signature');
            $contract->observation = $isPlayGroup
                ? 'Contrato Play Group generado automáticamente desde API.'
                : 'Contrato Admisión Permanente generado automáticamente desde API.';
            $contract->save();

            // Add student to contract
            $this->addStudentToContract($contract, $student, $periodId);

            DB::commit();

            $this->logSync('api_inbound', 'inbound', 'contract', $contract->id, 'success', [
                'action' => 'contract_created',
                'student_id' => $studentId,
                'contract_code' => $contractCode,
                'enrollment_type' => $enrollmentType,
            ]);

            return [
                'success' => true,
                'action' => 'contract_created',
                'contract_code' => $contractCode,
                'contract_id' => $contract->id,
                'enrollment_type' => $enrollmentType,
            ];

        } catch (Exception $e) {
            DB::rollBack();

            Log::error('Error en triggerContractForStudent: ' . $e->getMessage(), [
                'student_id' => $studentId,
                'parent_id' => $parentId,
            ]);

            $this->logSync('api_inbound', 'inbound', 'contract', null, 'error', [
                'student_id' => $studentId,
                'parent_id' => $parentId,
            ], null, $e->getMessage());

            throw $e;
        }
    }

    /**
     * Agregar un estudiante al detalle de un contrato existente
     * Calcula y crea las deudas automáticamente
     */
    private function addStudentToContract(Contract $contract, Student $student, $periodId)
    {
        // Check if student already has details in this contract
        $existingDetails = ContractDetail::where('contract_id', $contract->id)
            ->where('student_id', $student->id)
            ->exists();

        if ($existingDetails) {
            Log::info("Estudiante {$student->id} ya tiene detalles en contrato {$contract->id}");
            return;
        }

        // Calculate and create debts for this student
        $debtService = app(DebtCalculationService::class);
        $debts = $debtService->calculateStudentDebts($student->id, $periodId, $contract->id);

        Log::info("Estudiante {$student->id} asociado a contrato {$contract->code_contract} con " . count($debts) . " deudas", [
            'contract_id' => $contract->id,
            'course_id' => $student->course_id,
            'period_id' => $periodId,
            'debts_count' => count($debts),
        ]);

        // Recalcular deudas de hermanos (el nuevo alumno puede alterar el sibling ordering)
        if ($student->family) {
            try {
                $debtService->recalculateFamilySiblingDebts($student->financial_parent_id, $periodId);
            } catch (Exception $e) {
                Log::warning("Error recalculando hermanos tras agregar estudiante {$student->id}: " . $e->getMessage());
            }
        }
    }

    /**
     * Generate a unique contract code with global sequential correlative.
     * Format: CTR-YYYY-NNNN where NNNN is the next available number for that year.
     */
    public function generateContractCode($periodYear, $parentId = null, $periodId = null)
    {
        $prefix = sprintf('CTR-%s-', $periodYear);

        // Find the highest existing correlative for this year (only exact format CTR-YYYY-NNNN)
        $lastCode = Contract::where('code_contract', 'like', $prefix . '%')
            ->whereRaw("code_contract REGEXP ?", ['^CTR-' . $periodYear . '-[0-9]+$'])
            ->orderByRaw("CAST(SUBSTRING(code_contract, ?) AS UNSIGNED) DESC", [strlen($prefix) + 1])
            ->value('code_contract');

        $nextNumber = 1;
        if ($lastCode) {
            $numericPart = substr($lastCode, strlen($prefix));
            $parsed = intval($numericPart);
            if ($parsed > 0) {
                $nextNumber = $parsed + 1;
            }
        }

        return sprintf('CTR-%s-%04d', $periodYear, $nextNumber);
    }

    /**
     * Determine if a course is Play Group
     */
    private function isPlayGroupCourse($courseId): bool
    {
        if (in_array($courseId, [self::PG_COURSE_ID, self::JI_COURSE_ID])) {
            return true;
        }

        $course = Course::find($courseId);
        return $course && in_array($course->code, [self::PG_COURSE_CODE, self::JI_COURSE_CODE]);
    }

    /**
     * Log sync operation
     */
    private function logSync($syncType, $direction, $entityType, $entityId, $status, $requestPayload = null, $responsePayload = null, $errorMessage = null)
    {
        SyncLog::create([
            'sync_type'        => $syncType,
            'direction'        => $direction,
            'entity_type'      => $entityType,
            'entity_id'        => $entityId,
            'status'           => $status,
            'request_payload'  => $requestPayload,
            'response_payload' => $responsePayload,
            'error_message'    => $errorMessage,
        ]);
    }
}