Skip to content

Sistema de Importación XLSX - Documentación Técnica

Módulo: Core Componente: Data Import Infrastructure Fecha: 2026-02-03


Descripción

Sistema profesional de importación de datos desde archivos Excel (XLSX) al sistema PostgreSQL multi-tenant de Bautista. Migra datos legacy del sistema FoxPro utilizando Symfony Console con patrón Command + Template Method.

Características principales:

  • Dry-run mode con rollback automático
  • Validación de duplicados configurable
  • Transformación de datos (fechas, CUIT, relaciones FK)
  • Progress tracking y logging estructurado
  • Confirmación interactiva de seguridad

ADRs (Architecture Decision Records)

ADR-001: Symfony Console sobre Scripts PHP

Contexto: Necesitamos importar datos legacy con gestión robusta de comandos CLI.

Decisión: Usar Symfony Console Component en lugar de scripts PHP planos.

Razones:

  1. UI profesional (progress bars, tablas, colores, prompts)
  2. Gestión automática de opciones y validación de parámetros
  3. Help integrado y documentación auto-generada
  4. Testeable con InputInterface/OutputInterface mocks

Trade-offs:

  • ➕ Mejor UX para operadores
  • ➕ Código mantenible y consistente
  • ➖ Dependencia adicional (symfony/console)
  • ➖ Curva de aprendizaje inicial

ADR-002: Template Method Pattern

Contexto: Todos los importadores comparten flujo similar (leer Excel → validar → insertar → estadísticas).

Decisión: Clase base AbstractImportCommand con Template Method.

Razones:

  1. DRY: Lógica común una sola vez (progress bar, transacciones, estadísticas)
  2. Consistencia: Todos los importadores se comportan igual
  3. Extensibilidad: Nuevos importadores solo implementan runImport()
  4. Mantenibilidad: Cambios en flujo común se propagan automáticamente

Implementación:

AbstractImportCommand::execute() {
    validateParams()
    setupDatabase()
    displayHeader()
    confirmProduction()
    runImport() ← HOOK METHOD (abstracto)
    displayResults()
}

Trade-offs:

  • ➕ Menos código duplicado (DRY)
  • ➕ Comportamiento predecible
  • ➖ Acoplamiento por herencia (mitigado con traits)

ADR-003: Dry-Run con Rollback Automático

Contexto: Importaciones masivas son riesgosas, necesitamos pruebas seguras sin afectar BD.

Decisión: Modo --dry-run ejecuta lógica completa pero hace ROLLBACK en lugar de COMMIT.

Razones:

  1. Validación completa: Ejecuta todas las validaciones (FK, constraints, triggers)
  2. Sin side effects: No persiste datos en BD
  3. Estadísticas reales: Operadores ven qué pasaría (inserted, skipped, errors)
  4. Confianza: Probar N veces sin limpiar BD

Alternativas descartadas:

  • ❌ Simulación sin BD: No detecta errores reales de constraints
  • ❌ BD temporal: Complejidad de setup/teardown

Trade-offs:

  • ➕ Seguridad total para pruebas
  • ➕ Feedback inmediato y preciso
  • ➖ Transacciones largas en archivos grandes (mitigado con --limit)

ADR-004: Validación de Duplicados Opcional

Contexto: Algunas importaciones permiten duplicados intencionalmente (reimportaciones, actualizaciones).

Decisión: Validación habilitada por defecto, deshabilitada con --skip-validation.

Razones:

  1. Seguridad por defecto: Evita duplicados accidentales
  2. Flexibilidad: Permite reimportaciones con datos actualizados
  3. Performance: En importaciones limpias, se puede skipear

Trade-offs:

  • ➕ Control fino por caso de uso
  • ➖ Requiere conocimiento del flag

ADR-005: Logging Estructurado con Índice de Fila

Contexto: Necesitamos debuggear errores en archivos de miles de filas.

Decisión: Todos los warnings/errores incluyen [Fila N] como prefijo.

Razones:

  1. Trazabilidad: Operador puede abrir Excel y encontrar fila exacta
  2. Debugging: Fácil reproducir error con --limit=N
  3. Reportes: Se puede generar informe de errores por fila

Implementación: Ver AbstractImportCommand::addWarning() y AbstractImportCommand::addError()


Arquitectura

Componentes

bin/import (Symfony Console Application)

    ├─ Registra comandos concretos

AbstractImportCommand (Template Method)

    ├─ execute() → Orquestación completa
    ├─ runImport() → HOOK METHOD (abstracto)

Comandos concretos (ProveedorImport, ClienteImport)

    ├─ Implementan runImport()
    ├─ Transforman datos legacy → actual

XlsImporter (PhpSpreadsheet)

    ├─ Lee Excel → Array asociativo

ConnectionManager → PostgreSQL Multi-Tenant

Flujo de Ejecución

Usuario ejecuta comando

Validar parámetros (--file, --database, --schema)

Configurar conexión PostgreSQL

Mostrar banner informativo

Confirmar si modo PRODUCCIÓN (--dry-run = false)

Leer archivo Excel (XlsImporter)

BEGIN TRANSACTION

Procesar filas con Progress Bar
    ├─ Transformar datos
    ├─ Buscar relaciones FK
    ├─ Validar duplicados
    ├─ Insertar en BD
    └─ Registrar warnings/errores

COMMIT (producción) o ROLLBACK (dry-run)

Mostrar estadísticas finales

Patrones Implementados

Template Method

Qué resuelve: Evitar duplicación de flujo común entre importadores.

Cómo: Clase base define algoritmo completo en execute(), subclases implementan pasos específicos en runImport().

Trade-off: Acoplamiento por herencia, pero ganancia en consistencia.

Command Pattern

Qué resuelve: Encapsular operaciones de importación como objetos ejecutables.

Cómo: Symfony Console Component proporciona infraestructura de comandos con opciones, validación, help.

Trade-off: Dependencia de framework, pero ganancia en UX y mantenibilidad.


Componentes Principales

XlsImporter

Ubicación: Imports/XlsImporter.php

Responsabilidad: Leer archivos Excel y convertirlos a arrays asociativos PHP.

Tecnología: PhpOffice/PhpSpreadsheet

Comportamiento:

  1. Detecta formato automático (XLS, XLSX)
  2. Lee primera fila como headers (nombres de columnas)
  3. Convierte filas a arrays asociativos: ['header' => valor]
  4. Filtra filas vacías
  5. Retorna: array<int, array<string, mixed>>

Manejo de errores:

  • Archivo no existe → RuntimeException
  • Error al procesar → RuntimeException con causa original

AbstractImportCommand

Ubicación: Commands/AbstractImportCommand.php

Responsabilidad: Clase base con funcionalidad común para todos los importadores.

Opciones de línea de comandos:

OpciónAliasRequeridoDescripciónDefault
--file-fRuta al archivo XLSX-
--database-dNombre de la base de datosbautista
--schema-sSchema multi-tenantsuc0001
--dry-run-Ejecutar sin guardarfalse
--skip-validation-Omitir validaciones duplicadosfalse
--limit-lLimitar número de filasnull

Métodos abstractos (HOOKS):

  • runImport(string $file, ?int $limit): void
  • getResourceName(): string

Estadísticas internas:

  • $totalRows: Total filas leídas
  • $inserted: Insertadas exitosamente
  • $skipped: Omitidas (duplicadas)
  • $errors: Errores encontrados
  • $warnings: Advertencias

Importador Concreto: ProveedorImportCommand

Ubicación: Commands/Import/ProveedorImportCommand.php

Responsabilidad: Importar proveedores desde Excel legacy FoxPro.

Transformaciones principales:

  • Limpieza de CUIT: Solo dígitos
  • Parsing de fechas: d-M-yY-m-d
  • Normalización de strings: Trim + uppercase
  • Búsqueda de localidades por CP + nombre
  • Validación de IVA con fallback a defecto

Validaciones:

  1. Nombre requerido (skip si vacío)
  2. Duplicados por nombre o CUIT (configurable)
  3. Localidad buscada, warning si no existe
  4. IVA buscado, fallback a defecto con warning

Ver implementación completa en: Commands/Import/ProveedorImportCommand.php


Integración con Arquitectura Bautista

Multi-Tenancy

Schema-based tenancy:

  • Parámetro --schema setea schema PostgreSQL correcto
  • Usa ConnectionManager para gestión de conexiones
  • Queries ejecutan en schema especificado automáticamente

Ejemplo:

bash
# Importar a sucursal 1
php bin/import import:proveedor -f /tmp/data.xlsx --schema=suc0001

# Importar a sucursal 2
php bin/import import:proveedor -f /tmp/data.xlsx --schema=suc0002

Flow interno:

  1. Comando recibe --schema=suc0001
  2. ConnectionManager setea SET search_path = suc0001
  3. Queries ejecutan en schema correcto

Database Modes (Oficial vs Prueba)

Soporte para bases de datos de prueba:

  • Parámetro --database especifica BD (con sufijo _p para prueba)
  • Permite probar en BD de prueba antes de producción

Ejemplo:

bash
# Importar a BD prueba
php bin/import import:proveedor --file=/tmp/data.xlsx --database=bautista_p

NO Usa 5-Layer Architecture

IMPORTANTE: Los importadores NO pasan por la arquitectura 5-layer estándar del sistema.

Arquitectura estándar (NO usada aquí):

❌ Routes → Controller → Service → Domain → Model

Arquitectura importadores:

✅ Command → Model → Database

Razones:

  1. Batch operation: No es request HTTP individual
  2. Performance: Evita overhead de validaciones HTTP/JWT
  3. Transacciones masivas: Una transacción para N inserts
  4. No auditoría automática: Son datos legacy

Trade-offs:

  • ➕ Performance superior (sin overhead HTTP)
  • ➕ Control fino de transacciones
  • ➖ No hay audit logging automático
  • ➖ Validaciones manuales (no reutilizan Validators)

Extensibilidad

Crear Nuevo Importador Simple

Pasos:

  1. Crear clase extendiendo AbstractImportCommand
  2. Implementar getResourceName() y runImport()
  3. Registrar comando en bin/import

Estructura básica:

php
class ClienteImportCommand extends AbstractImportCommand
{
    protected function getResourceName(): string {
        return 'Clientes';
    }

    protected function configure(): void {
        $this->setName('import:cliente')
             ->setDescription('Importar clientes desde Excel');
        parent::configure();
    }

    protected function runImport(string $file, ?int $limit): void {
        // 1. Inicializar modelos
        // 2. Leer Excel con XlsImporter
        // 3. Aplicar límite si existe
        // 4. Iniciar transacción
        // 5. Procesar filas con progress bar
        // 6. Commit o rollback según modo
    }

    private function processRow(int $rowIndex, array $row): void {
        // Transformar datos
        // Validar mínimos
        // Validar duplicados
        // Insertar
    }
}

Ver ejemplo completo en: Commands/Import/ProveedorImportCommand.php


Crear Importador con Dependencias

Para importadores que requieren datos previos (ej: Productos requiere Líneas + Rubros):

Pasos adicionales:

  1. Validar dependencias al inicio de runImport()
  2. Cachear datos de FK para performance
  3. Validar FK en processRow() contra caché

Pattern de validación de dependencias:

php
protected function runImport(string $file, ?int $limit): void {
    // 1. Validar dependencias ANTES de procesar
    $this->validateDependencies();

    // 2. Cargar caché de FK
    $this->loadLineasCache();
    $this->loadRubrosCache();

    // ... resto del proceso
}

private function validateDependencies(): void {
    $lineas = $this->lineaModel->getAll();
    if (empty($lineas)) {
        throw new RuntimeException(
            "❌ No hay líneas. Ejecute primero: import:linea"
        );
    }
}

Beneficio de caché: O(1) lookup en lugar de query por fila.


Comando Orquestador

Para ejecutar múltiples importadores en secuencia (ej: Módulo Compras completo):

Pattern:

php
class ComprasFullImportCommand extends AbstractImportCommand {
    protected function execute(InputInterface $input, OutputInterface $output): int {
        $sequence = [
            ['import:proveedor', 'proveedores.xlsx'],
            ['import:linea', 'lineas.xlsx'],
            ['import:rubro', 'rubros.xlsx'],
            ['import:producto', 'productos.xlsx'],
        ];

        foreach ($sequence as [$commandName, $fileName]) {
            $command = $this->getApplication()->find($commandName);
            $exitCode = $command->run(new ArrayInput([...]), $output);

            if ($exitCode !== 0) {
                return Command::FAILURE;
            }
        }

        return Command::SUCCESS;
    }
}

Performance

Caché de Relaciones FK

Problema: Query por cada fila (1000 filas = 1000 queries).

Solución: Pre-cargar todas las relaciones en memoria.

Implementación:

php
private array $localidadesCache = [];

protected function runImport(...) {
    // Cargar caché ANTES del loop
    $localidades = $this->localidadModel->getAll();
    foreach ($localidades as $loc) {
        $this->localidadesCache[$loc['codigo_postal']] = $loc['id'];
    }

    foreach ($rows as $row) {
        // Lookup O(1)
        $localidadId = $this->localidadesCache[$row['cpos']] ?? null;
    }
}

Beneficio: O(1) en lugar de query por fila.


Límite para Pruebas

Usar --limit en desarrollo:

bash
# Probar con 10 filas (segundos)
php bin/import import:proveedor -f data.xlsx --dry-run --limit=10

# Probar con 100 filas
php bin/import import:proveedor -f data.xlsx --dry-run --limit=100

Beneficios:

  • Feedback rápido
  • Detectar errores sin procesar todo
  • Iteración rápida en desarrollo

Transacciones Grandes

Consideración: Una transacción para toda la importación puede ser lenta en archivos grandes.

Mitigación: Commit cada N filas (batches).

Trade-off: Si falla en batch 3, batches 1-2 ya están persistidos (no es atómico).


Testing

Estrategia Recomendada

SIEMPRE ejecutar en este orden:

bash
# 1. Dry-run con límite pequeño
php bin/import import:proveedor -f data.xlsx --dry-run --limit=10

# 2. Revisar estadísticas (inserted, skipped, errors)

# 3. Si OK: dry-run completo
php bin/import import:proveedor -f data.xlsx --dry-run

# 4. Revisar estadísticas finales

# 5. Si OK: ejecutar definitivo
php bin/import import:proveedor -f data.xlsx

Por qué dry-run primero: Detecta errores sin afectar BD.


Validaciones Pre-Ejecución

Checklist:

  • [ ] Archivo Excel existe y tiene permisos de lectura
  • [ ] Primera fila contiene headers correctos
  • [ ] Database y schema existen en PostgreSQL
  • [ ] Conexión a BD funciona
  • [ ] Si hay dependencias FK, tablas referenciadas tienen datos

Validaciones Post-Ejecución

Queries de verificación:

sql
-- Verificar total insertados
SELECT COUNT(*) FROM proveedores WHERE deleted_at IS NULL;

-- Verificar campos críticos no nulos
SELECT COUNT(*) FROM proveedores WHERE nombre IS NULL;

-- Verificar duplicados
SELECT cuit, COUNT(*)
FROM proveedores
WHERE deleted_at IS NULL AND cuit IS NOT NULL
GROUP BY cuit
HAVING COUNT(*) > 1;

Seguridad

Confirmación Interactiva

Si NO es --dry-run, solicita confirmación:

⚠️  MODO PRODUCCIÓN: Los cambios se guardarán permanentemente

¿Desea continuar? (yes/no) [no]:

Solo acepta "yes" explícito para continuar.

Por qué: Evita ejecuciones accidentales en producción.


Sanitización

Todas las inserciones usan prepared statements: Ver implementación en Models.

No hay riesgo de SQL Injection: Todos los valores van por parámetros.


Troubleshooting

Error: "Archivo no existe"

Solución: Verificar ruta absoluta.

bash
ls -la /tmp/proveedores.xlsx

Error: "Error conectando a la base de datos"

Solución:

  1. Verificar PostgreSQL corriendo: sudo systemctl status postgresql
  2. Verificar database existe: psql -U postgres -l | grep bautista
  3. Verificar credentials en constants.php

Warnings: "Localidad no encontrada"

Causa: Tabla localidades no tiene ese código postal o nombre.

Soluciones:

  1. Importar localidades primero
  2. Insertar manualmente localidades faltantes
  3. Aceptar warning (campo localidad queda NULL)

Errores Masivos: "Nombre vacío"

Causa: Headers de Excel no coinciden con código (columna esperada no existe).

Solución: Verificar primera fila de Excel coincide con código (ej: cnom, ccui, etc).


Performance Lento

Diagnóstico:

bash
time php bin/import import:proveedor -f data.xlsx --dry-run --limit=100

Si tarda mucho:

  • Implementar caché de FK
  • Deshabilitar validaciones (--skip-validation)
  • Verificar indexes en tablas de FK

Roadmap

Features Pendientes

  • [ ] Soporte CSV
  • [ ] Validación pre-import (sin insertar)
  • [ ] Report HTML con estadísticas
  • [ ] Resume desde fila N (si falla)
  • [ ] Logging a archivo
  • [ ] Modo update (en lugar de insert)

Refactorings Deseables

  • [ ] Extraer traits: DateParser, CuitCleaner
  • [ ] Service Layer: Extraer lógica de búsqueda FK
  • [ ] Unit Tests: Tests para componentes base
  • [ ] Integration Tests: Tests E2E con Excel fixtures

Referencias

Librerías

Documentación Relacionada

  • Bautista Backend Architecture: /var/www/Bautista/server/CLAUDE.md
  • Database Migrations: /var/www/Bautista/server/migrations/CLAUDE.md
  • Multi-Tenancy: Ver sección ConnectionManager en server/CLAUDE.md

Comandos

bash
# Ver comandos disponibles
php bin/import list

# Ver ayuda de comando específico
php bin/import import:proveedor --help

# Versión
php bin/import --version

Conclusión

El sistema de importación XLSX provee infraestructura robusta, extensible y segura para migrar datos legacy FoxPro a PostgreSQL multi-tenant.

Puntos clave:

  • Dry-run mode elimina riesgos
  • Template Method permite crear importadores en minutos
  • Logging estructurado facilita debugging
  • Multi-tenancy aware
  • Performance optimizable con caché

Next steps para desarrolladores:

  1. Ejecutar import:proveedor en modo dry-run para familiarizarse
  2. Crear nuevo importador siguiendo patrón de ProveedorImportCommand
  3. Contribuir con mejoras del Roadmap

Autor: Sistema Bautista Development Team Última actualización: 2026-02-03 Versión: 1.0.0