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/ReportService.php
<?php

namespace App\Services;

use App\Repositories\ReportRepository;
use App\Exports\ComparativeReportExport;
use App\Models\Period;
use App\Models\PresencialPayment;
use Maatwebsite\Excel\Facades\Excel;
use Exception;

class ReportService
{
    protected $repository;

    public function __construct(ReportRepository $repository)
    {
        $this->repository = $repository;
    }

    // ─── REPORTE MAESTRO DE MATRÍCULAS ────────────────────────

    /**
     * Reporte de contratos con desglose por alumno y conceptos.
     */
    public function contractsReport($periodId, $filters = [])
    {
        $contracts = $this->repository->getContractsForReport($periodId, $filters);

        if ($contracts->isEmpty()) {
            return [];
        }

        $ufRate = getUfValue();
        $paidClpMap = $this->buildPaidClpMap($contracts->pluck('id')->toArray());
        $rows = [];

        foreach ($contracts as $contract) {
            $studentGroups = $contract->details->groupBy('student_id');

            foreach ($studentGroups as $studentId => $details) {
                $row = $this->buildContractRow($contract, $studentId, $details, $ufRate, $paidClpMap);

                if ($this->excludeByMatriculaPaidAt($row, $filters)) {
                    continue;
                }

                $rows[] = $row;
            }
        }

        return $rows;
    }

    /**
     * Determina si una fila debe excluirse por filtros de fecha de pago de matrícula.
     * El filtro SQL pre-filtra contratos, pero como cada fila es por alumno,
     * aquí filtramos a nivel fila para excluir alumnos cuya matrícula no cae en el rango.
     */
    protected function excludeByMatriculaPaidAt($row, $filters)
    {
        $desde = $filters['fecha_pago_matricula_desde'] ?? null;
        $hasta = $filters['fecha_pago_matricula_hasta'] ?? null;

        if (empty($desde) && empty($hasta)) {
            return false;
        }

        $paidAt = $row['matricula_paid_at'] ?? null;
        if (empty($paidAt)) {
            return true; // Sin fecha de pago de matrícula → excluir cuando hay filtro activo
        }

        try {
            $paidDate = \Carbon\Carbon::createFromFormat('d-m-Y H:i:s', $paidAt)->format('Y-m-d');
        } catch (\Throwable $e) {
            return true;
        }

        if (!empty($desde) && $paidDate < $desde) return true;
        if (!empty($hasta) && $paidDate > $hasta) return true;

        return false;
    }

    /**
     * Construye una fila del reporte maestro para un alumno dentro de un contrato.
     */
    protected function buildContractRow($contract, $studentId, $details, $ufRate, $paidClpMap = [])
    {
        $firstDetail = $details->first();
        $student = $firstDetail->student;
        $course = $firstDetail->studentCourse ?? ($student ? $student->course : null);
        $letter = $firstDetail->studentCourseLetter ?? ($student ? $student->courseLetter : null);

        $studentName = trim(implode(' ', array_filter([
            $firstDetail->student_first_name ?? ($student->first_name ?? ''),
            $firstDetail->student_last_name ?? ($student->last_name ?? ''),
        ])));
        $studentRut = $firstDetail->student_rut ?? ($student->rut ?? '');
        $courseName = $course->course ?? '';
        $letterName = $letter->course_letter ?? '';
        $courseLevel = trim($courseName . ' ' . $letterName);

        // Buscar fecha de pago de matrícula
        $matriculaPaidAt = null;
        foreach ($details as $detail) {
            $pcd = $detail->paymentConceptDetail;
            if (!$pcd) continue;
            $concept = $pcd->payment_concept;
            if (!$concept) continue;
            $categoryKey = $this->mapConceptCategory($concept->code, $pcd->code ?? '');
            if ($categoryKey === 'matricula' && $detail->paid && $detail->paid_at) {
                $matriculaPaidAt = $detail->paid_at;
                break;
            }
        }

        // Calcular conceptos agrupados por payment_concept.code
        $concepts = $this->calculateConcepts($details, $ufRate, $paidClpMap);

        // Si la matrícula no está pagada, contrato anulable → pendiente = $0
        $matriculaPaid = collect($concepts)->contains(fn($c) => $this->isMatriculaGroup($this->mapConceptCategory($c['code'], $c['detail_code'] ?? '')) && $c['paid_clp'] > 0);
        if (!$matriculaPaid) {
            $concepts = array_map(function ($c) {
                $c['pending'] = 0;
                $c['pending_clp'] = 0;
                return $c;
            }, $concepts);
        }

        $totalClp = collect($concepts)->sum('total_clp');

        $parent = $contract->financialParent;

        // Obtener code_toku del primer detail que lo tenga
        $codeToku = $details->pluck('code_toku')->filter()->first();

        $totalPaidClp = collect($concepts)->sum('paid_clp');
        $totalPendingClp = collect($concepts)->sum('pending_clp');

        return [
            'id' => "{$contract->id}_{$studentId}",
            'code_contract' => $contract->code_contract,
            'period' => $contract->period ? [
                'id' => $contract->period->id,
                'period_year' => $contract->period->period_year,
            ] : null,
            'date_contract' => $contract->date_contract,
            'registration_year' => $contract->registration_year,
            'enrollment_type' => $contract->enrollment_type,
            'created_at' => $contract->created_at ? $contract->created_at->format('d-m-Y H:i:s') : null,
            'students' => [[
                'full_name' => $studentName,
                'rut' => $studentRut,
                'course' => $courseName,
                'course_id' => $course->id ?? null,
                'letter' => $letterName,
                'course_level' => $courseLevel,
                'status' => $student ? (bool) $student->status : null,
                'blocked_student' => $student->blocked_student ?? null,
            ]],
            'financial_parent' => $parent ? [
                'id' => $parent->id,
                'full_name' => trim(implode(' ', array_filter([
                    $parent->first_name, $parent->last_name,
                ]))),
                'rut' => $parent->rut ?? '',
                'email' => $parent->email ?? '',
                'mobile' => $parent->mobile ?? '',
                'relationship' => $parent->relationship->description ?? '',
                'account_confirmed' => $parent->user ? (bool) $parent->user->account_confirmed : false,
            ] : null,
            'status_contract' => $contract->statusContract ? [
                'code' => $contract->statusContract->code,
                'description' => $contract->statusContract->description,
            ] : null,
            'status_payment' => $contract->statusPayment ? [
                'code' => $contract->statusPayment->code,
                'description' => $contract->statusPayment->description,
            ] : null,
            'status_signature' => $contract->statusSignature ? [
                'code' => $contract->statusSignature->code,
                'description' => $contract->statusSignature->description,
            ] : null,
            'concepts' => $concepts,
            'matricula_paid' => $matriculaPaid,
            'matricula_paid_at' => $matriculaPaidAt ? (is_string($matriculaPaidAt) ? \Carbon\Carbon::parse($matriculaPaidAt)->format('d-m-Y H:i:s') : $matriculaPaidAt->format('d-m-Y H:i:s')) : null,
            'total_amount' => (float) $contract->total_amount,
            'total_amount_clp' => $totalClp,
            'total_paid_clp' => $totalPaidClp,
            'total_pending_clp' => $totalPendingClp,
            'payment_method' => $contract->observation ?? '',
            'has_contract_format' => !empty($contract->contract_format_id),
            'file_data' => $contract->file_data ?? null,
            'observations' => $contract->observation ?? '',
            'code_toku' => $codeToku,
        ];
    }

    /**
     * Construye mapa detail_id → amount_clp desde presencial_payments.
     * Replica la lógica del historial de pagos para usar montos reales.
     */
    protected function buildPaidClpMap(array $contractIds)
    {
        if (empty($contractIds)) return [];

        $payments = PresencialPayment::whereIn('contract_id', $contractIds)->get();
        $map = [];

        foreach ($payments as $payment) {
            $receiptData = $payment->receipt_data;
            if (!is_array($receiptData) || empty($receiptData['details'])) continue;

            $ufRate = $receiptData['uf_rate'] ?? 0;

            foreach ($receiptData['details'] as $detail) {
                $detailId = $detail['id'] ?? null;
                if (!$detailId) continue;

                $currency = $detail['currency'] ?? '$';

                if ($currency === 'UF') {
                    $clp = $detail['amount_clp'] ?? (($ufRate > 0) ? (int) round((float) $detail['amount'] * $ufRate) : 0);
                } else {
                    $clp = (int) round((float) ($detail['amount'] ?? 0));
                }

                // Acumular si un detail aparece en múltiples pagos (parciales)
                $map[$detailId] = ($map[$detailId] ?? 0) + $clp;
            }
        }

        return $map;
    }

    /**
     * Calcula conceptos agrupados por payment_concept.code con conversión UF→CLP.
     */
    protected function calculateConcepts($details, $ufRate, $paidClpMap = [])
    {
        $grouped = [];

        foreach ($details as $detail) {
            $pcd = $detail->paymentConceptDetail;
            if (!$pcd) continue;

            $concept = $pcd->payment_concept;
            if (!$concept) continue;

            // Agrupar por payment_concept_detail para mantener detalle individual
            $key = $pcd->id;
            $code = $concept->code;
            $detailCode = $pcd->code ?? '';
            $name = $pcd->description ?? $concept->payment_concept;
            $currency = $pcd->currency_type;
            $currencySymbol = $currency->currency_symbol ?? '$';

            $amount = (float) ($detail->amount ?? 0);
            $amountClp = $this->convertToClp($amount, $currencySymbol, $ufRate);

            // Usar monto real del pago si existe, sino recalcular
            if ($detail->paid && isset($paidClpMap[$detail->id])) {
                $paidClp = $paidClpMap[$detail->id];
            } else {
                $paid = $detail->paid ? $amount : 0;
                $paidClp = $this->convertToClp($paid, $currencySymbol, $ufRate);
            }
            // Pendiente: UF a valor de hoy si no está pagado, 0 si ya se pagó
            $pendingClp = $detail->paid ? 0 : $amountClp;

            if (!isset($grouped[$key])) {
                $grouped[$key] = [
                    'code' => $code,
                    'detail_code' => $detailCode,
                    'name' => $name,
                    'currency' => $currencySymbol,
                    'total' => 0,
                    'total_clp' => 0,
                    'paid' => 0,
                    'paid_clp' => 0,
                    'pending' => 0,
                    'pending_clp' => 0,
                ];
            }

            $grouped[$key]['total'] += $amount;
            $grouped[$key]['total_clp'] += $amountClp;
            $grouped[$key]['paid'] += $detail->paid ? $amount : 0;
            $grouped[$key]['paid_clp'] += $paidClp;
            $grouped[$key]['pending'] += $detail->paid ? 0 : $amount;
            $grouped[$key]['pending_clp'] += $pendingClp;
        }

        return array_values($grouped);
    }

    /**
     * Convierte un monto a CLP si la moneda es UF.
     */
    protected function convertToClp($amount, $currencySymbol, $ufRate)
    {
        if ($currencySymbol === 'UF' && $ufRate > 0) {
            return (int) round((float) $amount * $ufRate);
        }
        return (int) round((float) $amount);
    }

    // ─── REPORTE FINANCIERO ──────────────────────────────────

    /**
     * Reporte financiero con desglose por 3 categorías de concepto.
     */
    public function financialReport($periodId, $filters = [])
    {
        $contracts = $this->repository->getContractsForFinancialReport($periodId, $filters);

        if ($contracts->isEmpty()) {
            return ['contracts' => [], 'summary' => $this->emptyFinancialSummary()];
        }

        $ufRate = getUfValue();
        $paidClpMap = $this->buildPaidClpMap($contracts->pluck('id')->toArray());
        $rows = [];
        $processedContractIds = [];

        foreach ($contracts as $contract) {
            $studentGroups = $contract->details->groupBy('student_id');

            foreach ($studentGroups as $studentId => $details) {
                $row = $this->buildFinancialRow($contract, $studentId, $details, $ufRate, $paidClpMap);

                // Aplicar filtro de búsqueda client-side si viene search
                if (!empty($filters['search']) && !$this->matchesSearch($row, $filters['search'])) {
                    continue;
                }

                // Filtrar a nivel fila por fecha de pago de matrícula
                if ($this->excludeByMatriculaPaidAt($row, $filters)) {
                    continue;
                }

                $rows[] = $row;

                // Para el summary, contar contratos únicos
                if (!in_array($contract->id, $processedContractIds)) {
                    $processedContractIds[] = $contract->id;
                }
            }
        }

        $summary = $this->buildFinancialSummary($rows, $processedContractIds, $contracts);

        return ['contracts' => $rows, 'summary' => $summary];
    }

    /**
     * Construye una fila del reporte financiero.
     */
    protected function buildFinancialRow($contract, $studentId, $details, $ufRate, $paidClpMap = [])
    {
        $firstDetail = $details->first();
        $student = $firstDetail->student;
        $course = $firstDetail->studentCourse ?? ($student ? $student->course : null);

        $studentName = trim(implode(' ', array_filter([
            $firstDetail->student_first_name ?? ($student->first_name ?? ''),
            $firstDetail->student_last_name ?? ($student->last_name ?? ''),
        ])));
        $studentRut = $firstDetail->student_rut ?? ($student->rut ?? '');
        $courseName = $course->course ?? '';

        $parent = $contract->financialParent;

        // Mapear conceptos a las 4 categorías
        $emptyCategory = ['total' => 0, 'total_clp' => 0, 'paid' => 0, 'paid_clp' => 0, 'pending' => 0, 'pending_clp' => 0, 'currency' => '$'];
        $conceptCategories = [
            'matricula' => $emptyCategory,
            'incorporacion' => $emptyCategory,
            'colegiatura' => $emptyCategory,
            'familiar' => $emptyCategory,
        ];

        $lastPaymentDate = null;
        $paymentCount = 0;
        $codeToku = null;
        $colegiaturaPaymentType = null;
        $matriculaPaidAt = null;

        // Construir mapa detail_id → método de pago desde presencial_payments
        $detailPaymentMethodMap = [];
        if ($contract->relationLoaded('presencialPayments')) {
            foreach ($contract->presencialPayments as $pp) {
                if ($pp->subscription_status === 'cancelled') continue;
                $detailIds = $pp->receipt_data['detail_ids'] ?? [];
                $method = $pp->paymentMethodRecord->payment_method ?? null;
                foreach ($detailIds as $did) {
                    if ($method && !isset($detailPaymentMethodMap[$did])) {
                        $detailPaymentMethodMap[$did] = $method;
                    }
                }
            }
        }

        foreach ($details as $detail) {
            $pcd = $detail->paymentConceptDetail;
            if (!$pcd) continue;

            $concept = $pcd->payment_concept;
            if (!$concept) continue;

            // Mapear por code del payment_concept + detail code
            $categoryKey = $this->mapConceptCategory($concept->code, $pcd->code ?? '');

            // Detectar tipo de pago colegiatura
            if ($categoryKey === 'colegiatura' && !$colegiaturaPaymentType) {
                if (isset($detailPaymentMethodMap[$detail->id])) {
                    $colegiaturaPaymentType = $detailPaymentMethodMap[$detail->id];
                } elseif (!empty($detail->toku_subscription_id)) {
                    $colegiaturaPaymentType = 'PAC/PAT';
                } elseif ($detail->paid) {
                    $colegiaturaPaymentType = 'Presencial';
                }
            }

            // Capturar fecha de pago de matrícula
            if ($categoryKey === 'matricula' && !$matriculaPaidAt && $detail->paid && $detail->paid_at) {
                $matriculaPaidAt = $detail->paid_at;
            }

            $currency = $pcd->currency_type;
            $currencySymbol = $currency->currency_symbol ?? '$';

            $amount = (float) ($detail->amount ?? 0);
            $amountClp = $this->convertToClp($amount, $currencySymbol, $ufRate);

            // Usar monto real del pago si existe, sino recalcular
            if ($detail->paid && isset($paidClpMap[$detail->id])) {
                $paidClp = $paidClpMap[$detail->id];
            } else {
                $paid = $detail->paid ? $amount : 0;
                $paidClp = $this->convertToClp($paid, $currencySymbol, $ufRate);
            }
            // Pendiente: UF a valor de hoy si no está pagado, 0 si ya se pagó
            $pendingClp = $detail->paid ? 0 : $amountClp;

            $conceptCategories[$categoryKey]['total'] += $amount;
            $conceptCategories[$categoryKey]['total_clp'] += $amountClp;
            $conceptCategories[$categoryKey]['paid'] += $detail->paid ? $amount : 0;
            $conceptCategories[$categoryKey]['paid_clp'] += $paidClp;
            $conceptCategories[$categoryKey]['pending'] += $detail->paid ? 0 : $amount;
            $conceptCategories[$categoryKey]['pending_clp'] += $pendingClp;
            $conceptCategories[$categoryKey]['currency'] = $currencySymbol;

            if ($detail->paid && $detail->paid_at) {
                $paymentCount++;
                if (!$lastPaymentDate || $detail->paid_at > $lastPaymentDate) {
                    $lastPaymentDate = $detail->paid_at;
                }
            }

            if (!$codeToku && $detail->code_toku) {
                $codeToku = $detail->code_toku;
            }
        }

        // Si la matrícula no está pagada, el contrato es anulable → pendiente = $0
        // Cualquier pago en matrícula O incorporación activa el contrato
        $matriculaPaid = $conceptCategories['matricula']['paid_clp'] > 0 || $conceptCategories['incorporacion']['paid_clp'] > 0;
        if (!$matriculaPaid) {
            foreach (array_keys($conceptCategories) as $cat) {
                $conceptCategories[$cat]['pending'] = 0;
                $conceptCategories[$cat]['pending_clp'] = 0;
            }
        }

        $totalAmountClp = 0;
        $paidAmountClp = 0;
        $pendingAmountClp = 0;
        foreach ($conceptCategories as $cat) {
            $totalAmountClp += $cat['total_clp'];
            $paidAmountClp += $cat['paid_clp'];
            $pendingAmountClp += $cat['pending_clp'];
        }

        // Desglose individual de conceptos (para Excel)
        $conceptDetails = $this->calculateConcepts($details, $ufRate, $paidClpMap);
        // Aplicar regla de matrícula pagada al desglose individual
        if (!$matriculaPaid) {
            $conceptDetails = array_map(function ($c) {
                $c['pending'] = 0;
                $c['pending_clp'] = 0;
                return $c;
            }, $conceptDetails);
        }

        return [
            'id' => "{$contract->id}_{$studentId}",
            'code_contract' => $contract->code_contract,
            'period' => $contract->period ? [
                'id' => $contract->period->id,
                'period_year' => $contract->period->period_year,
            ] : null,
            'date_contract' => $contract->date_contract,
            'registration_year' => $contract->registration_year,
            'enrollment_type' => $contract->enrollment_type,
            'created_at' => $contract->created_at ? $contract->created_at->format('d-m-Y H:i:s') : null,
            'student_name' => $studentName,
            'student_rut' => $studentRut,
            'course_name' => $courseName,
            'course_id' => $course->id ?? null,
            'financial_parent' => $parent ? trim(implode(' ', array_filter([
                $parent->first_name, $parent->last_name,
            ]))) : '',
            'financial_parent_rut' => $parent->rut ?? '',
            'financial_parent_email' => $parent->email ?? '',
            'financial_parent_mobile' => $parent->mobile ?? '',
            'financial_parent_relationship' => $parent && $parent->relationship ? $parent->relationship->description : '',
            'financial_parent_profession' => $parent->profession ?? '',
            'financial_parent_account_confirmed' => $parent && $parent->user ? (bool) $parent->user->account_confirmed : false,
            'status_contract' => $contract->statusContract ? [
                'code' => $contract->statusContract->code,
                'description' => $contract->statusContract->description,
            ] : null,
            'status_payment' => $contract->statusPayment ? [
                'code' => $contract->statusPayment->code,
                'description' => $contract->statusPayment->description,
            ] : null,
            'concepts' => $conceptCategories,
            'concept_details' => $conceptDetails,
            'matricula_paid' => $matriculaPaid,
            'total_amount_clp' => $totalAmountClp,
            'paid_amount' => $paidAmountClp,
            'pending_amount' => $pendingAmountClp,
            'last_payment_date' => $lastPaymentDate,
            'payment_count' => $paymentCount,
            'code_toku' => $codeToku,
            'colegiatura_payment_type' => $colegiaturaPaymentType ?? 'Sin pagar',
            'matricula_paid_at' => $matriculaPaidAt ? (is_string($matriculaPaidAt) ? \Carbon\Carbon::parse($matriculaPaidAt)->format('d-m-Y H:i:s') : $matriculaPaidAt->format('d-m-Y H:i:s')) : null,
        ];
    }

    /**
     * Mapea a una de las 4 categorías usando parent code + detail code.
     * matricula   = ENROLLMENT, ENROLLMENT_PG (pago único)
     * incorporacion = CUOTA_INCORPORACION, _1, _2 (puede tener cuotas)
     * colegiatura = COLEGIATURA_*
     * familiar    = todo lo demás
     */
    protected function mapConceptCategory($parentCode, $detailCode = null)
    {
        if ($parentCode === 'matricula') {
            if ($detailCode && strpos($detailCode, 'CUOTA_INCORPORACION') === 0) {
                return 'incorporacion';
            }
            return 'matricula';
        }

        if ($parentCode === 'colegiatura') {
            return 'colegiatura';
        }

        return 'familiar';
    }

    /**
     * Verifica si una categoría pertenece al grupo matrícula (para regla de matrícula pagada).
     */
    protected function isMatriculaGroup($category)
    {
        return in_array($category, ['matricula', 'incorporacion']);
    }

    /**
     * Búsqueda multi-campo para el reporte financiero.
     */
    protected function matchesSearch($row, $search)
    {
        $search = strtolower(trim($search));
        $searchNumeric = preg_replace('/[^0-9]/', '', $search);

        $textFields = [
            $row['code_contract'] ?? '',
            $row['student_name'] ?? '',
            $row['student_rut'] ?? '',
            $row['course_name'] ?? '',
            $row['financial_parent'] ?? '',
            $row['financial_parent_rut'] ?? '',
            $row['financial_parent_email'] ?? '',
            $row['created_at'] ?? '',
        ];

        foreach ($textFields as $field) {
            if (stripos($field, $search) !== false) {
                return true;
            }
        }

        // Búsqueda numérica (RUT sin puntos, teléfono, montos)
        if ($searchNumeric && strlen($searchNumeric) >= 3) {
            $numericFields = [
                preg_replace('/[^0-9]/', '', $row['student_rut'] ?? ''),
                preg_replace('/[^0-9]/', '', $row['financial_parent_rut'] ?? ''),
                preg_replace('/[^0-9]/', '', $row['financial_parent_mobile'] ?? ''),
            ];

            foreach ($numericFields as $field) {
                if ($field && strpos($field, $searchNumeric) !== false) {
                    return true;
                }
            }
        }

        return false;
    }

    protected function emptyFinancialSummary()
    {
        return [
            'total_contracts' => 0,
            'total_amount' => 0,
            'paid_amount' => 0,
            'pending_amount' => 0,
            'failed_count' => 0,
            'failed_amount' => 0,
        ];
    }

    protected function buildFinancialSummary($rows, $processedContractIds, $contracts)
    {
        $paidAmount = 0;
        $pendingAmount = 0;
        $failedCount = 0;
        $failedAmount = 0;

        // Sumar montos de TODAS las filas agrupando por contrato
        // (un contrato con 2 hermanos genera 2 filas, ambas deben sumarse)
        $contractTotals = [];
        foreach ($rows as $row) {
            $contractId = explode('_', $row['id'])[0];
            if (!isset($contractTotals[$contractId])) {
                $contractTotals[$contractId] = ['paid' => 0, 'pending' => 0];
            }
            $contractTotals[$contractId]['paid'] += $row['paid_amount'];
            $contractTotals[$contractId]['pending'] += $row['pending_amount'];
        }

        foreach ($contractTotals as $totals) {
            $paidAmount += $totals['paid'];
            $pendingAmount += $totals['pending'];
        }

        // Contar contratos con pago fallido
        $ufRate = getUfValue();
        foreach ($contracts as $contract) {
            if ($contract->statusPayment && $contract->statusPayment->code === 'failed') {
                $failedCount++;
                $total = 0;
                foreach ($contract->details as $detail) {
                    $pcd = $detail->paymentConceptDetail;
                    if (!$pcd) continue;
                    $symbol = $pcd->currency_type->currency_symbol ?? '$';
                    $total += $this->convertToClp((float) ($detail->amount ?? 0), $symbol, $ufRate);
                }
                $failedAmount += $total;
            }
        }

        return [
            'total_contracts' => count($processedContractIds),
            'total_amount' => $paidAmount + $pendingAmount,
            'paid_amount' => $paidAmount,
            'pending_amount' => $pendingAmount,
            'failed_count' => $failedCount,
            'failed_amount' => $failedAmount,
        ];
    }

    // ─── REPORTE COMPARATIVO ─────────────────────────────────

    /**
     * Reporte comparativo multi-período.
     */
    public function comparativeReport($periodIds)
    {
        if (empty($periodIds)) {
            throw new Exception("Debe seleccionar al menos 2 períodos", 400);
        }

        $periods = Period::whereIn('id', $periodIds)
            ->orderBy('period_year', 'asc')
            ->get();

        if ($periods->count() < 2) {
            throw new Exception("Debe seleccionar al menos 2 períodos válidos", 400);
        }

        $ufRate = getUfValue();
        $periodsData = [];
        $studentIdsByPeriod = [];

        // Calcular stats y financiero por período
        $paidClpMapByPeriod = [];
        foreach ($periods as $period) {
            $contracts = $this->repository->getContractsByPeriodForComparative($period->id);
            $activeStudentIds = $this->repository->activeStudentIdsForPeriod($period->id);

            $contractIds = $contracts->pluck('id')->toArray();
            $paidClpMap = $this->buildPaidClpMap($contractIds);
            $paidClpMapByPeriod[$period->id] = $paidClpMap;

            $stats = $this->calculatePeriodStats($contracts, $activeStudentIds);
            $financial = $this->calculatePeriodFinancial($contracts, $ufRate, $paidClpMap);

            $studentIdsByPeriod[$period->id] = $activeStudentIds->toArray();

            $periodsData[] = [
                'id' => $period->id,
                'label' => (string) $period->period_year,
                'period_year' => $period->period_year,
                'stats' => $stats,
                'financial' => $financial,
            ];
        }

        // Calcular movimientos entre períodos consecutivos
        $movements = [];
        $periodsList = $periods->values();

        for ($i = 1; $i < $periodsList->count(); $i++) {
            $prevPeriod = $periodsList[$i - 1];
            $currPeriod = $periodsList[$i];

            $prevStudentIds = $studentIdsByPeriod[$prevPeriod->id];
            $currStudentIds = $studentIdsByPeriod[$currPeriod->id];

            // Nuevos ingresos: en actual pero no en anterior
            $newStudentIds = array_values(array_diff($currStudentIds, $prevStudentIds));
            // Desistimiento: en anterior pero no en actual
            $droppedStudentIds = array_values(array_diff($prevStudentIds, $currStudentIds));

            $newContracts = !empty($newStudentIds)
                ? $this->buildMovementContracts($newStudentIds, $currPeriod->id, $ufRate, $paidClpMapByPeriod[$currPeriod->id] ?? [])
                : [];
            $droppedContracts = !empty($droppedStudentIds)
                ? $this->buildMovementContracts($droppedStudentIds, $prevPeriod->id, $ufRate, $paidClpMapByPeriod[$prevPeriod->id] ?? [])
                : [];

            // Guardar desistimiento count en stats del período actual
            $periodsData[$i]['stats']['desistimiento_count'] = count($droppedStudentIds);

            $movements[] = [
                'period_from' => ['id' => $prevPeriod->id, 'label' => (string) $prevPeriod->period_year],
                'period_to' => ['id' => $currPeriod->id, 'label' => (string) $currPeriod->period_year],
                'nuevos_ingresos' => [
                    'count' => count($newStudentIds),
                    'contracts' => $newContracts,
                ],
                'desistimiento' => [
                    'count' => count($droppedStudentIds),
                    'contracts' => $droppedContracts,
                ],
            ];
        }

        return [
            'periods' => $periodsData,
            'movements' => $movements,
        ];
    }

    /**
     * Estadísticas de contratos por período.
     */
    protected function calculatePeriodStats($contracts, $activeStudentIds)
    {
        $stats = [
            'total' => $contracts->count(),
            'total_students' => $activeStudentIds->count(),
            'active' => 0,
            'finished' => 0,
            'in_course' => 0,
            'pending_payment' => 0,
            'pending_signature' => 0,
            'canceled' => 0,
            'desistimiento_count' => 0,
            'courses' => [],
        ];

        $courseStudents = [];

        foreach ($contracts as $contract) {
            $statusCode = $contract->statusContract->code ?? '';

            switch ($statusCode) {
                case 'finished':
                    $stats['finished']++;
                    $stats['active']++;
                    break;
                case 'in_course':
                    $stats['in_course']++;
                    $stats['active']++;
                    break;
                case 'pending_payment':
                    $stats['pending_payment']++;
                    $stats['active']++;
                    break;
                case 'pending_signature':
                    $stats['pending_signature']++;
                    $stats['active']++;
                    break;
                case 'canceled':
                    $stats['canceled']++;
                    break;
                default:
                    $stats['active']++;
            }

            // Cursos por alumno único (solo no cancelados)
            if ($statusCode !== 'canceled') {
                foreach ($contract->details as $detail) {
                    if ($detail->student_id && $detail->studentCourse) {
                        $key = $detail->studentCourse->id . '_' . $detail->student_id;
                        if (!isset($courseStudents[$key])) {
                            $courseStudents[$key] = $detail->studentCourse;
                        }
                    }
                }
            }
        }

        // Agrupar cursos
        $courseCounts = [];
        foreach ($courseStudents as $course) {
            $courseId = $course->id;
            if (!isset($courseCounts[$courseId])) {
                $courseCounts[$courseId] = ['id' => $courseId, 'name' => $course->course, 'count' => 0];
            }
            $courseCounts[$courseId]['count']++;
        }

        $stats['courses'] = array_values($courseCounts);

        return $stats;
    }

    /**
     * Totales financieros de un período.
     */
    protected function calculatePeriodFinancial($contracts, $ufRate, $paidClpMap = [])
    {
        $paid = 0;
        $pending = 0;

        foreach ($contracts as $contract) {
            if ($contract->statusContract && $contract->statusContract->code === 'canceled') {
                continue;
            }

            $contractPaid = 0;
            $contractPending = 0;
            $matriculaPaid = false;

            // Primera pasada: detectar si la matrícula está pagada
            foreach ($contract->details as $detail) {
                $pcd = $detail->paymentConceptDetail;
                if (!$pcd) continue;
                $concept = $pcd->payment_concept;
                if ($concept && $this->isMatriculaGroup($this->mapConceptCategory($concept->code, $pcd->code ?? '')) && $detail->paid) {
                    $matriculaPaid = true;
                    break;
                }
            }

            foreach ($contract->details as $detail) {
                $pcd = $detail->paymentConceptDetail;
                if (!$pcd) continue;

                $symbol = $pcd->currency_type->currency_symbol ?? '$';
                $amount = (float) ($detail->amount ?? 0);
                $amountClp = $this->convertToClp($amount, $symbol, $ufRate);

                if ($detail->paid && isset($paidClpMap[$detail->id])) {
                    $contractPaid += $paidClpMap[$detail->id];
                } elseif ($detail->paid) {
                    $contractPaid += $amountClp;
                }

                // Si matrícula no pagada, contrato anulable → pendiente = $0
                $contractPending += ($detail->paid || !$matriculaPaid) ? 0 : $amountClp;
            }

            $paid += $contractPaid;
            $pending += $contractPending;
        }

        return [
            'total' => round($paid + $pending),
            'paid' => round($paid),
            'pending' => round($pending),
        ];
    }

    /**
     * Construye lista de contratos para movimientos del comparativo.
     */
    protected function buildMovementContracts($studentIds, $periodId, $ufRate, $paidClpMap = [])
    {
        $contracts = $this->repository->getContractsForStudentIds($studentIds, $periodId);
        $rows = [];

        foreach ($contracts as $contract) {
            $relevantDetails = $contract->details->whereIn('student_id', $studentIds);
            $studentGroups = $relevantDetails->groupBy('student_id');

            foreach ($studentGroups as $studentId => $details) {
                $rows[] = $this->buildContractRow($contract, $studentId, $details, $ufRate, $paidClpMap);
            }
        }

        return $rows;
    }

    // ─── REPORTE ODOO ──────────────────────────────────────

    /**
     * Reporte con formato de carga Odoo.
     * Una fila por cada contract_detail (concepto individual).
     * - Solo se incluyen contratos con matrícula pagada.
     * - invoice_date e invoice_date_due usan la fecha de pago de la matrícula.
     * - Colegiatura se expande en cuotas (igual que el flujo PAC/PAT real).
     * - CUOTA_INCORPORACION_2 usa vencimiento 31-Oct del año del período.
     */
    public function odooReport($periodId, $filters = [])
    {
        $contracts = $this->repository->getContractsForOdooReport($periodId, $filters);

        if ($contracts->isEmpty()) {
            return [];
        }

        $rows = [];

        foreach ($contracts as $contract) {
            $periodYear = (int) ($contract->period->period_year ?? date('Y'));
            $parentRut = $contract->financial_parent_rut ?? '';

            // 1. Skip si no hay matrícula pagada
            $matriculaPaidAtRaw = $this->getOdooMatriculaPaidAt($contract);
            if (!$matriculaPaidAtRaw) continue;

            $matriculaDate = is_string($matriculaPaidAtRaw)
                ? explode(' ', $matriculaPaidAtRaw)[0]
                : $matriculaPaidAtRaw->format('Y-m-d');

            $schedule = $contract->getInstallmentSchedule();
            // 2da cuota de incorporación: misma fórmula que cobra Toku (PresencialService::getSecondIncorporationDueDate)
            // y que muestra el front (ContractResource): created_at.year + 1, 31 de octubre.
            $secondIncorpBaseYear = \Carbon\Carbon::parse($contract->created_at)->year;
            $secondIncorpDue = \Carbon\Carbon::create($secondIncorpBaseYear + 1, 10, 31)->format('Y-m-d');

            foreach ($contract->details as $detail) {
                $pcd = $detail->paymentConceptDetail;
                if (!$pcd) continue;

                $concept = $pcd->payment_concept;
                if (!$concept) continue;

                $currency = $pcd->currency_type;
                $currencySymbol = $currency->currency_symbol ?? '$';
                $currencyId = ($currencySymbol === 'UF') ? 'UF' : 'CLP';

                $student = $detail->student;
                $studentRut = $detail->student_rut ?? ($student->rut ?? '');

                $course = $detail->studentCourse ?? ($student ? $student->course : null);
                $courseName = $course->course ?? '';

                $productName = $pcd->description ?? ($concept->payment_concept ?? '');
                $label = implode('-', array_filter([$productName, $courseName, $periodYear]));
                $productId = $pcd->code_toku ?? '';

                // Si detail.code_toku está vacío (caso heredado), construirlo on-the-fly
                // con el patrón estándar: {concept_code_toku}_{period_year}_{rut_sin_dv}
                $codeToku = $detail->code_toku ?: $this->buildFallbackCodeToku($pcd, $studentRut, $periodYear);

                $isColegiatura = str_starts_with(strUpperSinTildes($pcd->description ?? ''), 'COLEGIATURA');

                if ($isColegiatura) {
                    // 4. Expandir en N cuotas según calendario (igual que PAC/PAT)
                    $total = (float) ($detail->amount ?? 0);
                    if ($total <= 0) continue;

                    $base = round($total / 10, 4);
                    $numInstallments = $schedule['num_installments'];

                    for ($i = 1; $i <= $numInstallments; $i++) {
                        $cuotaAmount = ($i === 1 && $schedule['half_first_installment'])
                            ? round($base / 2, 4)
                            : $base;

                        // Cuota 1 vence el mismo día del pago de matrícula (se cobra desde ahí).
                        // Cuotas 2..N vencen el día 5 de su mes correspondiente.
                        if ($i === 1) {
                            $dueDate = $matriculaDate;
                        } else {
                            $dueDate = \Carbon\Carbon::create(
                                $schedule['period_year'],
                                $schedule['start_month'] + $i - 1,
                                5
                            )->format('Y-m-d');
                        }

                        $rows[] = [
                            'partner_id'                  => $parentRut,
                            'x_studio_alumnoa'            => $studentRut,
                            'invoice_date'                => $matriculaDate,
                            'currency_id'                 => $currencyId,
                            'payment_reference'           => str_replace('_', '-', $codeToku . '_cuota_' . $i),
                            'invoice_line_ids_name'       => $label . " - Cuota {$i}/{$numInstallments}",
                            'invoice_line_ids_product_id' => $productId,
                            'invoice_line_ids_price_unit' => $cuotaAmount,
                            'invoice_date_due'            => $dueDate,
                            'x_studio_anio_vencimiento'   => $periodYear,
                            'x_studio_nivel_alumno'       => $courseName,
                        ];
                    }
                } else {
                    // Conceptos no-colegiatura: 1 fila
                    // CUOTA_INCORPORACION_2 tiene due especial (31-Oct año período)
                    $isSecondIncorporation = ($pcd->code ?? '') === 'CUOTA_INCORPORACION_2';
                    $dueDate = $isSecondIncorporation ? $secondIncorpDue : $matriculaDate;

                    $rows[] = [
                        'partner_id'                  => $parentRut,
                        'x_studio_alumnoa'            => $studentRut,
                        'invoice_date'                => $matriculaDate,
                        'currency_id'                 => $currencyId,
                        'payment_reference'           => str_replace('_', '-', $codeToku),
                        'invoice_line_ids_name'       => $label,
                        'invoice_line_ids_product_id' => $productId,
                        'invoice_line_ids_price_unit' => (float) ($detail->amount ?? 0),
                        'invoice_date_due'            => $dueDate,
                        'x_studio_anio_vencimiento'   => $periodYear,
                        'x_studio_nivel_alumno'       => $courseName,
                    ];
                }
            }
        }

        return $rows;
    }

    /**
     * Busca la fecha de pago de la matrícula (ENROLLMENT/ENROLLMENT_PG, NO incorporación).
     * Retorna null si no hay matrícula pagada.
     */
    protected function getOdooMatriculaPaidAt($contract)
    {
        foreach ($contract->details as $detail) {
            $pcd = $detail->paymentConceptDetail;
            if (!$pcd) continue;
            $concept = $pcd->payment_concept;
            if (!$concept) continue;
            if ($concept->code !== 'matricula') continue;
            if (str_starts_with($pcd->code ?? '', 'CUOTA_INCORPORACION')) continue;
            if ($detail->paid && $detail->paid_at) return $detail->paid_at;
        }
        return null;
    }

    /**
     * Construye un code_toku con el patrón estándar cuando detail.code_toku está vacío:
     *   {concept_code_toku}_{period_year}_{rut_sin_dv}
     * Ej: colegiatura-anual_2026_28065442
     */
    protected function buildFallbackCodeToku($pcd, $studentRut, $periodYear): string
    {
        $conceptCode = $pcd->code_toku ?? '';
        $rutClean = str_replace('.', '', $studentRut ?? '');
        if (strpos($rutClean, '-') !== false) {
            $rutClean = substr($rutClean, 0, strrpos($rutClean, '-'));
        }
        $rutClean = strtolower($rutClean);
        return $conceptCode . '_' . $periodYear . '_' . $rutClean;
    }

    /**
     * Exportar reporte Odoo a Excel/CSV.
     */
    public function exportOdooReport($periodId, $filters, $format = 'xlsx')
    {
        $rows = $this->odooReport($periodId, $filters);

        $exportData = [];
        foreach ($rows as $row) {
            $exportData[] = [
                $row['partner_id'],
                $row['x_studio_alumnoa'],
                $row['invoice_date'],
                $row['currency_id'],
                $row['payment_reference'],
                $row['invoice_line_ids_name'],
                $row['invoice_line_ids_product_id'],
                $row['invoice_line_ids_price_unit'],
                $row['invoice_date_due'],
                $row['x_studio_anio_vencimiento'],
                $row['x_studio_nivel_alumno'],
            ];
        }

        $headings = [
            'partner_id',
            'x_studio_alumnoa',
            'invoice_date',
            'currency_id',
            'Payment_reference',
            'invoice_line_ids/name',
            'invoice_line_ids/product_id',
            'invoice_line_ids/price_unit',
            'invoice_date_due',
            'x_studio_anio_vencimiento',
            'x_studio_nivel_alumno',
        ];

        return $this->generateExport($headings, $exportData, 'reporte_odoo', $format);
    }

    // ─── EXPORTS ────────────────────────────────────────────

    /**
     * Exportar reporte maestro a Excel/CSV.
     */
    public function exportContractsReport($periodId, $filters, $format = 'xlsx')
    {
        $rows = $this->contractsReport($periodId, $filters);

        // Recopilar todos los nombres de conceptos individuales que aparecen en los datos
        $conceptNames = [];
        foreach ($rows as $row) {
            foreach ($row['concepts'] ?? [] as $concept) {
                $key = $concept['code'] . '|' . $concept['name'];
                if (!isset($conceptNames[$key])) {
                    $conceptNames[$key] = $concept['name'];
                }
            }
        }

        $exportData = [];
        foreach ($rows as $row) {
            $student = $row['students'][0] ?? [];
            $cm = $this->mapConceptsToColumns($row['concepts'] ?? []);

            $totalPaid = 0;
            $totalPending = 0;
            foreach ($cm as $catVals) {
                $totalPaid += $catVals['paid'];
                $totalPending += $catVals['pending'];
            }

            // Estado alumno: Bloqueado CAE/CFE > Activo/Inactivo
            $studentStatus = '';
            if (!empty($student['blocked_student'])) {
                $studentStatus = 'Bloqueado ' . $student['blocked_student'];
            } elseif (isset($student['status'])) {
                $studentStatus = $student['status'] ? 'Activo' : 'Inactivo';
            }

            $baseRow = [
                $row['code_contract'] ?? '',
                $row['period']['period_year'] ?? '',
                $row['registration_year'] ?? '',
                $row['enrollment_type'] ?? '',
                $student['full_name'] ?? '',
                $student['rut'] ?? '',
                $student['course'] ?? '',
                $student['letter'] ?? '',
                $student['course_level'] ?? '',
                $studentStatus,
                $row['financial_parent']['full_name'] ?? '',
                $row['financial_parent']['rut'] ?? '',
                $row['financial_parent']['email'] ?? '',
                $row['financial_parent']['mobile'] ?? '',
                $row['financial_parent']['relationship'] ?? '',
                ($row['financial_parent']['account_confirmed'] ?? false) ? 'Sí' : 'No',
                $row['status_contract']['description'] ?? '',
                $row['status_payment']['description'] ?? '',
                $row['status_signature']['description'] ?? '',
                $row['matricula_paid'] ? 'Sí' : 'No',
                $row['matricula_paid_at'] ?? '',
                $row['payment_method'] ?? '',
                $row['date_contract'] ?? '',
                $row['created_at'] ?? '',
            ];

            // Columnas por categoría (4 categorías)
            foreach (['matricula', 'incorporacion', 'colegiatura', 'familiar'] as $cat) {
                $baseRow[] = $cm[$cat]['total'];
                $baseRow[] = $cm[$cat]['paid'];
                $baseRow[] = $cm[$cat]['pending'];
            }

            // Totales
            $baseRow[] = $row['total_amount_clp'] ?? 0;
            $baseRow[] = $totalPaid;
            $baseRow[] = $totalPending;

            // Columnas individuales por concepto (total CLP de cada concepto)
            $conceptMap = [];
            foreach ($row['concepts'] ?? [] as $concept) {
                $key = $concept['code'] . '|' . $concept['name'];
                $conceptMap[$key] = $concept;
            }
            foreach ($conceptNames as $key => $name) {
                $c = $conceptMap[$key] ?? null;
                $baseRow[] = $c ? $c['total_clp'] : 0;        // Total
                $baseRow[] = $c ? $c['paid_clp'] : 0;         // Pagado
                $baseRow[] = $c ? $c['pending_clp'] : 0;      // Pendiente
            }

            $baseRow[] = $row['file_data'] ? 'Sí' : 'No';
            $baseRow[] = $row['observations'] ?? '';

            $exportData[] = $baseRow;
        }

        $headings = [
            'Código Contrato', 'Período', 'Año Matrícula', 'Tipo Matrícula',
            'Nombre Alumno', 'RUT Alumno', 'Curso', 'Letra', 'Curso/Nivel', 'Estado Alumno',
            'Apoderado', 'RUT Apoderado', 'Email Apoderado', 'Teléfono Apoderado', 'Parentesco', 'Cuenta Activa',
            'Estado Contrato', 'Estado Pago', 'Estado Firma',
            'Matrícula Pagada', 'Fecha Pago Matrícula', 'Método Pago',
            'Fecha Contrato', 'Fecha Creación',
            'Matrícula Total ($)', 'Matrícula Pagado ($)', 'Matrícula Pendiente ($)',
            'Incorporación Total ($)', 'Incorporación Pagado ($)', 'Incorporación Pendiente ($)',
            'Colegiatura Total ($)', 'Colegiatura Pagado ($)', 'Colegiatura Pendiente ($)',
            'Seguros Total ($)', 'Seguros Pagado ($)', 'Seguros Pendiente ($)',
            'Monto Total ($)', 'Total Pagado ($)', 'Total Pendiente ($)',
        ];

        // Encabezados dinámicos por concepto individual
        foreach ($conceptNames as $name) {
            $headings[] = "{$name} Total (\$)";
            $headings[] = "{$name} Pagado (\$)";
            $headings[] = "{$name} Pendiente (\$)";
        }

        $headings[] = 'Contrato PDF';
        $headings[] = 'Observaciones';

        return $this->generateExport($headings, $exportData, 'reporte_matriculas', $format);
    }

    /**
     * Exportar reporte financiero a Excel/CSV.
     */
    public function exportFinancialReport($periodId, $filters, $format = 'xlsx')
    {
        $result = $this->financialReport($periodId, $filters);
        $rows = $result['contracts'];

        // Recopilar conceptos individuales de los detalles del reporte
        $conceptNames = [];
        foreach ($rows as $row) {
            foreach ($row['concept_details'] ?? [] as $cd) {
                $key = $cd['code'] . '|' . $cd['name'];
                if (!isset($conceptNames[$key])) {
                    $conceptNames[$key] = $cd['name'];
                }
            }
        }

        $exportData = [];
        foreach ($rows as $row) {
            $concepts = $row['concepts'] ?? [];
            $baseRow = [
                $row['code_contract'] ?? '',
                $row['period']['period_year'] ?? '',
                $row['registration_year'] ?? '',
                $row['enrollment_type'] ?? '',
                $row['student_name'] ?? '',
                $row['student_rut'] ?? '',
                $row['course_name'] ?? '',
                $row['financial_parent'] ?? '',
                $row['financial_parent_rut'] ?? '',
                $row['financial_parent_email'] ?? '',
                $row['financial_parent_mobile'] ?? '',
                $row['financial_parent_relationship'] ?? '',
                $row['financial_parent_profession'] ?? '',
                ($row['financial_parent_account_confirmed'] ?? false) ? 'Sí' : 'No',
                $row['status_contract']['description'] ?? '',
                $row['status_payment']['description'] ?? '',
                $row['matricula_paid'] ? 'Sí' : 'No',
                $row['matricula_paid_at'] ?? '',
            ];

            // 4 categorías
            foreach (['matricula', 'incorporacion', 'colegiatura', 'familiar'] as $cat) {
                $baseRow[] = $concepts[$cat]['total_clp'] ?? 0;
                $baseRow[] = $concepts[$cat]['paid_clp'] ?? 0;
                $baseRow[] = $concepts[$cat]['pending_clp'] ?? 0;
                if ($cat === 'colegiatura') {
                    $baseRow[] = $row['colegiatura_payment_type'] ?? 'Sin pagar';
                }
            }

            $baseRow[] = $row['total_amount_clp'] ?? 0;
            $baseRow[] = $row['paid_amount'] ?? 0;
            $baseRow[] = $row['pending_amount'] ?? 0;

            // Columnas individuales por concepto
            $cdMap = [];
            foreach ($row['concept_details'] ?? [] as $cd) {
                $key = $cd['code'] . '|' . $cd['name'];
                $cdMap[$key] = $cd;
            }
            foreach ($conceptNames as $key => $name) {
                $c = $cdMap[$key] ?? null;
                $baseRow[] = $c ? $c['total_clp'] : 0;
                $baseRow[] = $c ? $c['paid_clp'] : 0;
                $baseRow[] = $c ? $c['pending_clp'] : 0;
            }

            $baseRow[] = $row['payment_count'] ?? 0;
            $baseRow[] = $row['last_payment_date'] ?? '';
            $baseRow[] = $row['date_contract'] ?? '';
            $baseRow[] = $row['created_at'] ?? '';

            $exportData[] = $baseRow;
        }

        $headings = [
            'Código Contrato', 'Período', 'Año Matrícula', 'Tipo Matrícula',
            'Alumno', 'RUT Alumno', 'Curso',
            'Apoderado', 'RUT Apoderado', 'Email', 'Teléfono',
            'Parentesco', 'Profesión', 'Cuenta Activa',
            'Estado Contrato', 'Estado Pago', 'Matrícula Pagada', 'Fecha Pago Matrícula',
            'Matrícula Total ($)', 'Matrícula Pagado ($)', 'Matrícula Pendiente ($)',
            'Incorporación Total ($)', 'Incorporación Pagado ($)', 'Incorporación Pendiente ($)',
            'Colegiatura Total ($)', 'Colegiatura Pagado ($)', 'Colegiatura Pendiente ($)', 'Tipo Pago Colegiatura',
            'Seguros Total ($)', 'Seguros Pagado ($)', 'Seguros Pendiente ($)',
            'Total ($)', 'Pagado ($)', 'Pendiente ($)',
        ];

        foreach ($conceptNames as $name) {
            $headings[] = "{$name} Total (\$)";
            $headings[] = "{$name} Pagado (\$)";
            $headings[] = "{$name} Pendiente (\$)";
        }

        $headings[] = 'N° Pagos';
        $headings[] = 'Último Pago';
        $headings[] = 'Fecha Contrato';
        $headings[] = 'Fecha Creación';

        return $this->generateExport($headings, $exportData, 'reporte_financiero', $format);
    }

    /**
     * Exportar reporte comparativo a Excel multi-hoja.
     */
    public function exportComparativeReport($periodIds, $format = 'xlsx')
    {
        $data = $this->comparativeReport($periodIds);

        $fileName = 'reporte_comparativo.' . $format;

        return Excel::download(new ComparativeReportExport($data), $fileName);
    }

    /**
     * Mapea conceptos a las 3 categorías con total/pagado/pendiente en CLP.
     */
    protected function mapConceptsToColumns($concepts)
    {
        $empty = ['total' => 0, 'paid' => 0, 'pending' => 0];
        $map = [
            'matricula' => $empty,
            'incorporacion' => $empty,
            'colegiatura' => $empty,
            'familiar' => $empty,
        ];

        foreach ($concepts as $concept) {
            $category = $this->mapConceptCategory($concept['code'] ?? '', $concept['detail_code'] ?? '');
            $map[$category]['total'] += $concept['total_clp'] ?? 0;
            $map[$category]['paid'] += $concept['paid_clp'] ?? 0;
            $map[$category]['pending'] += $concept['pending_clp'] ?? 0;
        }

        return $map;
    }

    /**
     * Genera un export simple (una hoja) en formato xlsx o csv.
     */
    protected function generateExport($headings, $data, $filename, $format)
    {
        if ($format === 'csv') {
            $output = chr(0xEF) . chr(0xBB) . chr(0xBF); // UTF-8 BOM
            $output .= implode(';', $headings) . "\n";

            foreach ($data as $row) {
                $output .= implode(';', array_map(function ($val) {
                    return '"' . str_replace('"', '""', $val ?? '') . '"';
                }, $row)) . "\n";
            }

            return response($output, 200, [
                'Content-Type' => 'text/csv; charset=UTF-8',
                'Content-Disposition' => "attachment; filename={$filename}.csv",
            ]);
        }

        // XLSX usando clase anónima
        $export = new class($headings, $data, $filename) implements
            \Maatwebsite\Excel\Concerns\FromArray,
            \Maatwebsite\Excel\Concerns\WithHeadings,
            \Maatwebsite\Excel\Concerns\WithTitle,
            \Maatwebsite\Excel\Concerns\ShouldAutoSize {

            protected $headings;
            protected $data;
            protected $title;

            public function __construct($headings, $data, $title)
            {
                $this->headings = $headings;
                $this->data = $data;
                $this->title = $title;
            }

            public function array(): array { return $this->data; }
            public function headings(): array { return $this->headings; }
            public function title(): string { return $this->title; }
        };

        return Excel::download($export, "{$filename}.xlsx");
    }
}