File: /var/www/api_matriculas/app/Services/ExcelProcessors/BaseExcelProcessor.php
<?php
namespace App\Services\ExcelProcessors;
abstract class BaseExcelProcessor
{
/**
* Procesar una línea del Excel y validarla
*
* @param array $data
* @param int $lineNumber
* @return array ['status' => 'SUCCESS|ERROR|WARNING', 'error' => '', 'warning' => '']
*/
abstract public function processLine(array $data, int $lineNumber): array;
/**
* Crear o actualizar el registro en la base de datos
*
* @param array $data
* @return array ['action' => 'created|updated', 'model' => Model]
*/
abstract public function createOrUpdateRecord(array $data): array;
/**
* Orden de columnas para mostrar en la tabla del frontend.
* Cada procesador puede sobreescribir este método para definir su propio orden.
* Si retorna null, se usa el orden original del Excel (Object.keys del JSON).
*/
public function getColumnOrder(): ?array
{
return null;
}
/**
* Validar fecha
*/
protected function parseFlexibleDate(string $value): ?string
{
try {
$value = trim($value);
// 1️⃣ Si viene como número serial de Excel
if (is_numeric($value)) {
return \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value)->format('Y-m-d');
}
// 2️⃣ Si viene con guiones o barras
$formats = ['d-m-Y', 'Y-m-d', 'd/m/Y', 'Y/m/d', 'm-d-Y', 'm/d/Y'];
foreach ($formats as $format) {
$dt = \DateTime::createFromFormat($format, $value);
if ($dt && $dt->format($format) === $value) {
return $dt->format('Y-m-d');
}
}
// 3️⃣ Si Laravel puede parsearlo automáticamente
$dt = \Carbon\Carbon::parse($value);
return $dt ? $dt->format('Y-m-d') : null;
} catch (\Exception $e) {
return null;
}
}
/**
* Obtener valor del array con clave insensible a mayúsculas
*/
public function getValue(array $data, string $key, $default = null)
{
// Buscar la clave exacta
if (isset($data[$key])) {
return $data[$key];
}
// Buscar insensible a mayúsculas
$lowerKey = strtolower($key);
foreach ($data as $k => $v) {
if (strtolower($k) === $lowerKey) {
return $v;
}
}
// Buscar con clave normalizada (sin caracteres especiales como ¿, ?, !, etc.)
$normalizedKey = preg_replace('/[^a-z0-9_]/', '', $lowerKey);
foreach ($data as $k => $v) {
$normalizedK = preg_replace('/[^a-z0-9_]/', '', strtolower($k));
if ($normalizedK === $normalizedKey) {
return $v;
}
}
return $default;
}
/**
* Generate a unique contract code with global sequential correlative.
* Format: CTR-YYYY-NNNN where NNNN is the next available number for that year.
*/
protected function generateContractCode($periodYear)
{
$prefix = sprintf('CTR-%s-', $periodYear);
$lastCode = \App\Models\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);
}
}