Appearance
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:
- UI profesional (progress bars, tablas, colores, prompts)
- Gestión automática de opciones y validación de parámetros
- Help integrado y documentación auto-generada
- 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:
- DRY: Lógica común una sola vez (progress bar, transacciones, estadísticas)
- Consistencia: Todos los importadores se comportan igual
- Extensibilidad: Nuevos importadores solo implementan
runImport() - 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:
- Validación completa: Ejecuta todas las validaciones (FK, constraints, triggers)
- Sin side effects: No persiste datos en BD
- Estadísticas reales: Operadores ven qué pasaría (inserted, skipped, errors)
- 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:
- Seguridad por defecto: Evita duplicados accidentales
- Flexibilidad: Permite reimportaciones con datos actualizados
- 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:
- Trazabilidad: Operador puede abrir Excel y encontrar fila exacta
- Debugging: Fácil reproducir error con
--limit=N - 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-TenantFlujo 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 finalesPatrones 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:
- Detecta formato automático (XLS, XLSX)
- Lee primera fila como headers (nombres de columnas)
- Convierte filas a arrays asociativos:
['header' => valor] - Filtra filas vacías
- Retorna:
array<int, array<string, mixed>>
Manejo de errores:
- Archivo no existe →
RuntimeException - Error al procesar →
RuntimeExceptioncon 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ón | Alias | Requerido | Descripción | Default |
|---|---|---|---|---|
--file | -f | ✅ | Ruta al archivo XLSX | - |
--database | -d | ❌ | Nombre de la base de datos | bautista |
--schema | -s | ❌ | Schema multi-tenant | suc0001 |
--dry-run | - | ❌ | Ejecutar sin guardar | false |
--skip-validation | - | ❌ | Omitir validaciones duplicados | false |
--limit | -l | ❌ | Limitar número de filas | null |
Métodos abstractos (HOOKS):
runImport(string $file, ?int $limit): voidgetResourceName(): 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-y→Y-m-d - Normalización de strings: Trim + uppercase
- Búsqueda de localidades por CP + nombre
- Validación de IVA con fallback a defecto
Validaciones:
- Nombre requerido (skip si vacío)
- Duplicados por nombre o CUIT (configurable)
- Localidad buscada, warning si no existe
- 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
--schemasetea schema PostgreSQL correcto - Usa
ConnectionManagerpara 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=suc0002Flow interno:
- Comando recibe
--schema=suc0001 - ConnectionManager setea
SET search_path = suc0001 - Queries ejecutan en schema correcto
Database Modes (Oficial vs Prueba)
Soporte para bases de datos de prueba:
- Parámetro
--databaseespecifica BD (con sufijo_ppara 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_pNO 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 → ModelArquitectura importadores:
✅ Command → Model → DatabaseRazones:
- Batch operation: No es request HTTP individual
- Performance: Evita overhead de validaciones HTTP/JWT
- Transacciones masivas: Una transacción para N inserts
- 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:
- Crear clase extendiendo
AbstractImportCommand - Implementar
getResourceName()yrunImport() - 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:
- Validar dependencias al inicio de
runImport() - Cachear datos de FK para performance
- 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=100Beneficios:
- 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.xlsxPor 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.xlsxError: "Error conectando a la base de datos"
Solución:
- Verificar PostgreSQL corriendo:
sudo systemctl status postgresql - Verificar database existe:
psql -U postgres -l | grep bautista - Verificar credentials en
constants.php
Warnings: "Localidad no encontrada"
Causa: Tabla localidades no tiene ese código postal o nombre.
Soluciones:
- Importar localidades primero
- Insertar manualmente localidades faltantes
- Aceptar warning (campo
localidadqueda 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=100Si 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
- Symfony Console: https://symfony.com/doc/current/components/console.html
- PhpSpreadsheet: https://phpspreadsheet.readthedocs.io/
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 --versionConclusió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:
- Ejecutar
import:proveedoren modo dry-run para familiarizarse - Crear nuevo importador siguiendo patrón de ProveedorImportCommand
- Contribuir con mejoras del Roadmap
Autor: Sistema Bautista Development Team Última actualización: 2026-02-03 Versión: 1.0.0