Release inicial 1.0.0

This commit is contained in:
2025-03-10 18:25:41 -06:00
commit ba2845242d
83 changed files with 309608 additions and 0 deletions

View File

@ -0,0 +1,178 @@
<?php
namespace Koneko\SatCatalogs\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class CsvDatabaseService
{
/**
* Carga datos desde CSV a una tabla de la base de datos.
*/
public static function importCsvToTable(string $csvFilePath, string $tableName, array $columns, bool $updateExisting = false)
{
if (!file_exists($csvFilePath)) {
return ["error" => "Archivo CSV no encontrado: {$csvFilePath}"];
}
$handle = fopen($csvFilePath, 'r');
$batchSize = 5000;
$data = [];
$rowCount = 0;
$insertedCount = 0;
while (($row = fgetcsv($handle, 1000, ";")) !== false) {
$record = [];
foreach ($columns as $index => $column) {
$value = $row[$index] ?? null;
// Aplicar CAST según el tipo de dato
if ($value === '') {
$record[$column] = null;
} elseif (self::isDateColumn($column)) {
$record[$column] = self::formatDate($value);
} elseif (is_numeric($value)) {
$record[$column] = strpos($value, '.') !== false ? (float) $value : (int) $value;
} else {
$record[$column] = trim($value);
}
}
$data[] = $record;
$rowCount++;
if (count($data) >= $batchSize) {
self::insertOrUpdate($tableName, $data, $updateExisting);
$insertedCount += count($data);
$data = [];
}
}
fclose($handle);
if (!empty($data)) {
self::insertOrUpdate($tableName, $data, $updateExisting);
$insertedCount += count($data);
}
return [
"inserted" => $insertedCount,
"total_rows" => $rowCount
];
}
/**
* Inserta o actualiza datos en la tabla.
*/
private static function insertOrUpdate(string $tableName, array $data, bool $updateExisting)
{
if (empty($data)) {
echo "⚠️ Datos vacíos para {$tableName}\n";
return;
}
foreach ($data as &$record) {
foreach ($record as $key => $value) {
if ($value === '?') { // Si hay un '?', lo convertimos en NULL
$record[$key] = null;
}
}
}
unset($record);
try {
$primaryKeys = self::getPrimaryKeys($tableName);
$updateFields = array_diff(array_keys($data[0]), $primaryKeys);
if ($updateExisting && !empty($updateFields)) {
DB::table($tableName)->upsert($data, $primaryKeys, $updateFields);
echo "✅ Upsert ejecutado en {$tableName} (" . count($data) . " filas)\n";
} else {
DB::table($tableName)->insert($data);
echo "✅ Insert ejecutado en {$tableName} (" . count($data) . " filas)\n";
}
} catch (\Exception $e) {
Log::error("🚨 Error en {$tableName}: " . $e->getMessage());
echo "❌ ERROR en {$tableName}: {$e->getMessage()}\n";
die();
}
}
/**
* **Detecta si una columna es de tipo fecha.**
*/
private static function isDateColumn(string $columnName): bool
{
$dateFields = ['fecha_inicio_vigencia', 'fecha_fin_vigencia'];
return in_array($columnName, $dateFields);
}
/**
* **Convierte fechas de diferentes formatos a `YYYY-MM-DD`.**
*/
private static function formatDate($value)
{
if (!$value || strtolower($value) === 'null') {
return null;
}
try {
if (is_numeric($value) && $value > 10000) {
return Carbon::instance(\PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value))->format('Y-m-d');
}
if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $value)) {
return Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d');
}
return Carbon::parse($value)->format('Y-m-d');
} catch (\Exception $e) {
return null;
}
}
/**
* **Obtiene las claves primarias de la tabla.**
*/
private static function getPrimaryKeys(string $tableName): array
{
$primaryKeys = [
'sat_forma_pago' => ['c_forma_pago'],
'sat_moneda' => ['c_moneda'],
'sat_codigo_postal' => ['c_codigo_postal'],
'sat_regimen_fiscal' => ['c_regimen_fiscal'],
'sat_pais' => ['c_pais'],
'sat_uso_cfdi' => ['c_uso_cfdi'],
'sat_clave_prod_serv' => ['c_clave_prod_serv'],
'sat_clave_unidad' => ['c_clave_unidad'],
'sat_aduana' => ['c_aduana'],
'sat_colonia' => ['c_colonia', 'c_codigo_postal'],
'sat_estado' => ['c_estado', 'c_pais'],
'sat_localidad' => ['c_localidad', 'c_estado'],
'sat_municipio' => ['c_municipio', 'c_estado'],
'sat_banco' => ['c_banco'],
'sat_deduccion' => ['c_deduccion'],
'sat_percepcion' => ['c_percepcion'],
'sat_regimen_contratacion' => ['c_regimen_contratacion'],
];
return $primaryKeys[$tableName] ?? ['id'];
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace Koneko\SatCatalogs\Services;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
use PhpOffice\PhpSpreadsheet\Reader\Xls as XlsReader;
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
use Carbon\Carbon;
class CsvGeneratorService
{
private static array $dateColumnsMap = [
'c_formapago' => [12, 13],
'c_moneda' => [4, 5],
'c_codigopostal' => [5, 6],
'c_regimenfiscal' => [4, 5],
'c_usocfdi' => [4, 5],
'c_claveprodserv' => [5, 6],
'c_claveunidad' => [4, 5],
'c_aduana' => [2, 3],
'c_estado' => [3],
'c_localidad' => [3],
'c_municipio' => [3],
];
/**
* Convierte hojas de un XLSX o XLS a un CSV optimizado.
*/
public static function convertToCsv(string $filePath, $sheetNames, string $csvName, int $skipRows = 7)
{
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$reader = ($extension === 'xls') ? new XlsReader() : new XlsxReader();
try {
$reader->setReadDataOnly(true);
$reader->setLoadSheetsOnly($sheetNames);
$spreadsheet = $reader->load($filePath);
} catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
die("❌ Error al cargar hojas: " . implode(", ", (array) $sheetNames) . "\n");
}
$csvPath = database_path("seeders/sat_cache/{$csvName}.csv");
$handle = fopen($csvPath, 'w');
if (!$handle) {
return ["error" => "No se pudo crear el archivo CSV."];
}
foreach ((array) $sheetNames as $sheetName) {
if (!$spreadsheet->sheetNameExists($sheetName)) {
echo "⚠️ Hoja no encontrada: {$sheetName}, saltando...\n";
continue;
}
$sheet = $spreadsheet->getSheetByName($sheetName);
if (!$sheet) continue;
$dateColumns = self::$dateColumnsMap[$csvName] ?? []; // Índices de columnas de fechas
foreach ($sheet->getRowIterator($skipRows + 1) as $row) {
$cells = [];
$colIndex = 0;
foreach ($row->getCellIterator() as $cell) {
$value = trim((string) $cell->getValue());
// 🛑 Omitir filas con "Continúa en..."
if (str_starts_with($value, 'Continúa en ')) {
echo "⚠️ Fila omitida: {$value}\n";
continue 2; // Salta toda la fila
}
// 🔥 Convertir "?" y valores vacíos a NULL
if ($value === '?' || $value === '') {
$value = '';
}
// 📅 Si es fecha de Excel, convertir a YYYY-MM-DD
if (in_array($colIndex, $dateColumns) && self::isExcelDate($value)) {
$value = self::convertExcelDate($value);
}
// 🔹 Escapar valores que contienen punto y coma, saltos de línea o comillas
$value = self::escapeCsvValue($value);
$cells[] = $value;
$colIndex++;
}
if (self::isEmptyRow($cells)) {
echo "⚠️ Fila vacía detectada y omitida\n";
continue;
}
fputcsv($handle, $cells, ';'); // 💡 Asegura que los valores se escapen correctamente
}
}
fclose($handle);
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
return $csvPath;
}
/**
* **Detecta si el valor es un número de fecha de Excel**
*/
private static function isExcelDate($value): bool
{
return is_numeric($value) && $value > 10000;
}
/**
* **Convierte un número de fecha de Excel a formato YYYY-MM-DD**
*/
private static function convertExcelDate($value): string
{
try {
return Carbon::instance(ExcelDate::excelToDateTimeObject($value))->format('Y-m-d');
} catch (\Exception $e) {
return $value;
}
}
/**
* **Escapa valores con punto y coma, comillas o saltos de línea en CSV**
*/
private static function escapeCsvValue($value): string
{
// Si el valor contiene punto y coma, salto de línea o comillas dobles, se escapa
if (strpbrk($value, ";\"\n")) {
$value = '"' . str_replace('"', '""', $value) . '"'; // Duplicar comillas internas
}
return $value;
}
/**
* **Verifica si una fila está vacía o contiene valores inválidos.**
*/
private static function isEmptyRow(array $cells): bool
{
$empty = empty(array_filter($cells, fn($cell) => $cell !== ''));
if ($empty) {
echo "⚠️ Fila detectada como vacía y omitida\n";
}
return $empty;
}
/**
* **Cuenta las filas de un archivo CSV.**
*/
public static function countCsvRows(string $csvPath): int
{
if (!file_exists($csvPath)) {
return 0;
}
$file = fopen($csvPath, 'r');
$count = 0;
while (fgetcsv($file, 0, ';') !== false) {
$count++;
}
fclose($file);
return $count;
}
}

View File

@ -0,0 +1,416 @@
<?php
namespace Koneko\SatCatalogs\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class SatCatalogService
{
protected $catalogs = [
'banco' => [
'table' => 'sat_banco',
'key' => 'c_banco',
'value' => 'descripcion AS item',
'order_by' => 'descripcion',
'search_columns' => ['descripcion'],
'use_status' => true,
'limit' => 10
],
'clave_prod_serv' => [
'table' => 'sat_clave_prod_serv',
'key' => 'c_clave_prod_serv',
'value' => "CONCAT_WS(' - ', c_clave_prod_serv , descripcion) as item",
'search_type' => 'MATCH',
'search_columns' => ['c_clave_prod_serv_text', 'descripcion'],
],
'clave_unidad' => [
'table' => 'sat_clave_unidad',
'key' => 'c_clave_unidad',
'value' => "CONCAT_WS(' - ', c_clave_unidad , nombre) as item",
'search_columns' => ['c_clave_unidad', 'nombre'],
'limit' => 20
],
'deduccion' => [
'table' => 'sat_deduccion',
'key' => 'c_deduccion',
'value' => "CONCAT_WS(' - ', c_deduccion , descripcion) as item",
'search_columns' => ['c_deduccion', 'descripcion'],
'limit' => 20
],
'forma_pago' => [
'table' => 'sat_forma_pago',
'key' => 'c_forma_pago',
'value' => "CONCAT_WS(' - ', c_forma_pago , descripcion) as item",
'search_columns' => ['c_forma_pago', 'descripcion'],
'limit' => 10
],
'moneda' => [
'table' => 'sat_moneda',
'key' => 'c_moneda',
'value' => "CONCAT_WS(' - ', c_moneda , descripcion) as item",
'order_by' => 'descripcion',
'search_columns' => ['c_moneda', 'descripcion'],
'limit' => 10
],
'pais' => [
'table' => 'sat_pais',
'key' => 'c_pais',
'value' => 'descripcion AS item',
'order_by' => 'descripcion',
'search_columns' => ['c_pais', 'descripcion'],
'limit' => 10
],
'estado' => [
'table' => 'sat_estado',
'key' => 'c_estado',
'value' => 'nombre_del_estado AS item',
'order_by' => 'nombre_del_estado',
'extra_conditions' => ['c_pais'],
'search_columns' => ['nombre_del_estado'],
],
'municipio' => [
'table' => 'sat_municipio',
'key' => 'c_municipio',
'value' => 'descripcion AS item',
'order_by' => 'descripcion',
'extra_conditions' => ['c_estado'],
'search_columns' => ['descripcion'],
],
'localidad' => [
'table' => 'sat_localidad',
'key' => 'c_localidad',
'value' => 'descripcion AS item',
'order_by' => 'descripcion',
'extra_conditions' => ['c_estado'],
'search_columns' => ['descripcion'],
],
'colonia' => [
'table' => 'sat_colonia',
'joins' => [
[
'table' => 'sat_codigo_postal',
'first' => 'sat_colonia.c_codigo_postal',
'second' => 'sat_codigo_postal.c_codigo_postal',
],
],
'key' => 'sat_colonia.c_colonia',
'value' => 'sat_colonia.nombre_del_asentamiento AS item',
'columns' => [
'sat_colonia.c_codigo_postal',
],
'order_by' => 'sat_colonia.nombre_del_asentamiento',
'extra_conditions' => ['sat_codigo_postal.c_codigo_postal', 'sat_codigo_postal.c_estado', 'sat_codigo_postal.c_municipio', 'sat_colonia.c_colonia'],
'search_columns' => ['sat_colonia.nombre_del_asentamiento'],
],
'codigo_postal' => [
'table' => 'sat_codigo_postal',
'joins' => [
[
'table' => 'sat_localidad',
'first' => 'sat_codigo_postal.c_localidad',
'second' => 'sat_localidad.c_localidad',
'and' => ['sat_codigo_postal.c_estado = sat_localidad.c_estado'],
'type' => 'leftJoin',
],
[
'table' => 'sat_municipio',
'first' => 'sat_codigo_postal.c_municipio',
'second' => 'sat_municipio.c_municipio',
'and' => ['sat_codigo_postal.c_estado = sat_municipio.c_estado'],
'type' => 'leftJoin',
],
[
'table' => 'sat_estado',
'first' => 'sat_codigo_postal.c_estado',
'second' => 'sat_estado.c_estado',
'type' => 'leftJoin',
],
],
'key' => 'sat_codigo_postal.c_codigo_postal',
'columns' => [
'sat_codigo_postal.c_estado',
'sat_estado.nombre_del_estado as estado',
'sat_codigo_postal.c_localidad',
'sat_localidad.descripcion as localidad',
'sat_codigo_postal.c_municipio',
'sat_municipio.descripcion as municipio',
],
'search_columns' => ['sat_codigo_postal.c_codigo_postal'],
],
];
protected $fixedCatalogs = [
'exportacion' => [
1 => 'No aplica',
2 => 'Definitiva con clave A1',
3 => 'Temporal',
4 => 'Definitiva con clave distinta a A1 o cuando no existe enajenación en términos del CFF'
],
'horas_extra' => [
'01' => 'Dobles',
'02' => 'Triples',
'03' => 'Simples'
],
'impuestos' => [
1 => 'ISR',
2 => 'IVA',
3 => 'IEPS'
],
'incapacidad' => [
1 => 'Riesgo de trabajo',
2 => 'Enfermedad en general',
3 => 'Maternidad',
4 => 'Licencia por cuidados médicos de hijos diagnosticados con cáncer'
],
'jornada' => [
'01' => 'Diurna',
'02' => 'Nocturna',
'03' => 'Mixta',
'04' => 'Por hora',
'05' => 'Reducida',
'06' => 'Continuada',
'07' => 'Partida',
'08' => 'Por turnos',
'99' => 'Otra Jornada'
],
'metodo_pago' => [
'PUE' => 'Pago en una sola exhibición',
'PPD' => 'Pago en parcialidades o diferido'
],
'nomina' => [
'O' => 'Nómina ordinaria',
'E' => 'Nómina extraordinaria'
],
'objeto_imp' => [
1 => 'No objeto de impuesto.',
2 => 'Sí objeto de impuesto.',
3 => 'Sí objeto del impuesto y no obligado al desglose.',
4 => 'Sí objeto del impuesto y no causa impuesto.',
5 => 'Sí objeto del impuesto, IVA crédito PODEBI.'
],
'origen_recurso' => [
'IP' => 'Ingresos propios',
'IF' => 'Ingresos federales',
'IM' => 'Ingresos mixtos'
],
'otro_pago' => [
1 => 'Reintegro de ISR pagado en exceso',
2 => 'Subsidio para el empleo',
3 => 'Viáticos',
4 => 'Aplicación de saldo a favor por compensación anual',
5 => 'Reintegro de ISR retenido en exceso de ejercicio anterior',
6 => 'Alimentos en bienes',
7 => 'ISR ajustado por subsidio',
8 => 'Subsidio efectivamente entregado que no correspondía',
999 => 'Pagos distintos a los listados'
],
'periodicidad' => [
1 => 'Diario',
2 => 'Semanal',
3 => 'Quincenal',
4 => 'Mensual',
5 => 'Bimestral'
],
'periodicidad_pago' => [
1 => 'Diario',
2 => 'Semanal',
3 => 'Catorcenal',
4 => 'Quincenal',
5 => 'Mensual',
6 => 'Bimestral',
7 => 'Unidad obra',
8 => 'Comisión',
9 => 'Precio alzado',
10 => 'Decenal',
99 => 'Otra Periodicidad'
],
'riesgo_puesto' => [
'1' => 'Clase I',
'2' => 'Clase II',
'3' => 'Clase III',
'4' => 'Clase IV',
'5' => 'Clase V'
],
'tipo_comprobante' => [
'I' => 'Ingreso',
'E' => 'Egreso',
'T' => 'Traslado',
'N' => 'Nómina',
'P' => 'Pago'
],
'tipo_factor' => [
1 => 'Tasa',
2 => 'Cuota',
3 => 'Exento'
],
'tipo_relacion' => [
1 => 'Nota de crédito de los documentos relacionados',
2 => 'Nota de débito de los documentos relacionados',
3 => 'Devolución de mercancía sobre facturas o traslados previos',
4 => 'Sustitución de los CFDI previos',
5 => 'Traslados de mercancías facturados previamente',
6 => 'Factura generada por los traslados previos',
7 => 'CFDI por aplicación de anticipo'
]
];
public function searchCatalog(string $catalog, string $searchTerm = '', array $options = []): array
{
// 1. Validar si es un catálogo fijo o uno definido en $this->catalogs
if (isset($this->fixedCatalogs[$catalog])) {
return $this->fixedCatalogs[$catalog];
}
if (!isset($this->catalogs[$catalog])) {
return [];
}
$config = $this->catalogs[$catalog];
// 2. Construye Query Builder base
$query = DB::table($config['table']);
// 3. Aplica joins
if (!empty($config['joins'])) {
foreach ($config['joins'] as $join) {
$type = $join['type'] ?? 'join';
$query->{$type}($join['table'], function($joinObj) use ($join) {
$joinObj->on($join['first'], '=', $join['second']);
// Soporte para AND en ON, si está definidio
if (!empty($join['and'])) {
foreach ((array) $join['and'] as $andCondition) {
// 'sat_codigo_postal.c_estado = sat_localidad.c_estado'
$parts = explode('=', $andCondition);
if (count($parts) === 2) {
$left = trim($parts[0]);
$right = trim($parts[1]);
$joinObj->whereRaw("$left = $right");
}
}
}
});
}
}
// 4. Construir la lista de columnas a seleccionar
$selectFields = [];
// - Si hay "columns", añádelas
if (!empty($config['columns'])) {
foreach ($config['columns'] as $col) {
$selectFields[] = DB::raw($col);
}
}
// - Si también tienes "key" y "value" (por ejemplo para select2),
// añádelos (si no están ya en columns).
if (!empty($config['key'])) {
$selectFields[] = DB::raw($config['key']);
}
if (!empty($config['value'])) {
$selectFields[] = DB::raw($config['value']);
}
// - Si al final no hay nada, por fallback selecciona todo (o lanza un error)
if (empty($selectFields)) {
$query->select('*');
} else {
$query->select($selectFields);
}
// 5. Filtrado por status si aplica
if (($config['use_status'] ?? false) === true) {
$status = isset($options['status'])
? $options['status']
: (isset($config['status']) ? $config['status']: null);
if ($status !== null) {
$query->where('status', $status);
}
}
// 6. Filtrar según extra_conditions (ahora puede incluir columnas de la tabla unida)
if (isset($config['extra_conditions'])) {
foreach ($config['extra_conditions'] as $field) {
if (array_key_exists($field, $options)) {
$query->where($field, $options[$field]);
}
}
}
// 7. Búsqueda
if (!empty($searchTerm) && !empty($config['search_columns'])) {
if (($config['search_type'] ?? 'LIKE') === 'MATCH') {
// Ejemplo: MATCH..AGAINST
$cols = implode(',', $config['search_columns']);
$query->whereRaw("MATCH ($cols) AGAINST (? IN BOOLEAN MODE)", [$searchTerm]);
} else {
// Búsqueda por LIKE
$query->where(function ($subQ) use ($config, $searchTerm) {
foreach ($config['search_columns'] as $col) {
$subQ->orWhere($col, 'LIKE', "%{$searchTerm}%");
}
});
}
}
// 8. Ordenar resultados
$orderBy = $options['order_by'] ?? $config['order_by'] ?? $config['key'];
$orderDir = $options['order_dir'] ?? $config['order_dir'] ?? 'asc';
$query->orderBy($orderBy, $orderDir);
// 9. Limitar
$limit = array_key_exists('limit', $options) ? $options['limit'] : ($config['limit'] ?? 50);
if ($limit !== null) {
$query->limit($limit);
}
// Para ver la sentencia SQL (con placeholders ?)
// dump($query->toSql()); dd($query->getBindings());
// 10. Revisar modo de respuesta
$rawMode = ($config['rawMode'] ?? false) || ($options['rawMode'] ?? false);
$firstRow = ($config['firstRow'] ?? false) || ($options['firstRow'] ?? false);
$select2Mode = $options['select2Mode'] ?? false;
// (a) Modo raw -> devolvemos tal cual
if ($rawMode) {
if($firstRow){
return (array) $query->first();
}
return $query->get()->toArray();
}
$shortKey = Str::afterLast($config['key'], '.');
// (b) Devuelve en formato "select2" o en un array
if ($select2Mode) {
$response = [];
foreach ($query->get() as $row) {
$response[] = [
'id' => $row->{$shortKey},
'text' => $row->item,
];
}
return $response;
}
// (c) Por defecto, regresa "pluck" id => texto
return $query->pluck('item', $shortKey)->toArray();
}
}