Files
HoruxDespachosNuevo/docs/sessions/2026-05-24-cfdi-bulk-xml-download-refactor.md
Horux Dev e35eae2a72 refactor(cfdi): descarga masiva de XMLs por filtros en lugar de checkboxes
- Backend: POST /cfdi/download-xmls acepta CfdiFilters, usa getXmlsByFilters con LIMIT 1000
- Frontend: eliminados checkboxes y estado selectedIds; botón Descargar XMLs usa filtros activos
- Si >1000 resultados, muestra confirm() de advertencia pero permite proceder
- Agregada documentación técnica y changelog
2026-05-24 21:40:08 +00:00

5.4 KiB
Raw Blame History

Refactor: Descarga masiva de XMLs por filtros

Fecha: 2026-05-24
Feature: CFDI — descarga masiva de XMLs refactorizada de selección por checkbox a descarga por filtros


1. Requerimiento

Cambiar el mecanismo de descarga masiva de XMLs en la página /cfdi:

  • Antes: El usuario debía seleccionar CFDIs individuales mediante checkboxes por fila y en el header de la tabla. Solo se descargaban los seleccionados.
  • Después: Un único botón "Descargar XMLs" descarga todos los CFDIs que coincidan con los filtros activos, sin necesidad de selección manual.
  • Límite: Si los filtros aplicados devuelven más de 1,000 CFDIs, se muestra una advertencia (confirm) informando que solo se descargarán los primeros 1,000, pero el usuario puede proceder.

2. Decisiones de diseño

2.1 Sin checkboxes

Eliminar toda la UI de selección (header con checkbox maestro, checkboxes por fila, barra de "X seleccionados" y botón "Limpiar"). Esto simplifica la UX y reduce el estado del componente.

2.2 Filtros como fuente de verdad

El backend ya soportaba descarga por IDs (downloadXmlsZip(ids: number[])). Se reemplazó por descarga por filtros (downloadXmlsZip(filters: CfdiFilters)). Esto aprovecha el mismo whereClause que usa getCfdis() para listar, garantizando consistencia entre lo que se ve y lo que se descarga.

2.3 Warning, no error, al superar 1,000

En lugar de bloquear la descarga cuando hay >1,000 resultados, se muestra un window.confirm() con el mensaje:

"Solo se descargarán los primeros 1,000 XMLs. ¿Continuar?"

Si el usuario acepta, el backend ejecuta la query con LIMIT 1000 y genera el ZIP.


3. Cambios implementados

3.1 Backend

Service: apps/api/src/services/cfdi.service.ts

Función existente reutilizada: getXmlsByFilters(pool, filters, limit)

  • Reusa el mismo whereClause builder de getCfdis() para garantizar consistencia.
  • SELECT: id, uuid, xml_original as xml
  • LIMIT parametrizado (default 1000).

Controller: apps/api/src/controllers/cfdi.controller.ts

Endpoint: POST /cfdi/download-xmls

  • Body: { filters: CfdiFilters }
  • Llama getXmlsByFilters(req.tenantPool, filters, 1000).
  • Genera ZIP con adm-zip.
  • Retorna application/zip con Content-Disposition: attachment; filename="cfdis-{timestamp}.zip".
  • Si ningún CFDI tiene XML (campo xml null o vacío), retorna 404.

3.2 Frontend

API client: apps/web/lib/api/cfdi.ts

// Antes
export async function downloadXmlsZip(ids: number[]): Promise<Blob>

// Después
export async function downloadXmlsZip(filters: CfdiFilters): Promise<Blob>

La función ahora envía filters en el body en lugar de un array de ids.

Página: apps/web/app/(dashboard)/cfdi/page.tsx

Estados eliminados:

  • selectedIds: Set<number>
  • downloadingXmls se mantiene solo para indicador de loading en el botón.

UI eliminada:

  • Checkbox en header de tabla (<th className="w-8">).
  • Checkbox por fila en cada <tr>.
  • Barra de "X seleccionados" con botones "Descargar XMLs" y "Limpiar" condicionales.

UI nueva/modificada:

  • Botón "Descargar XMLs" ubicado permanentemente en la barra de acciones del CardHeader.
  • Deshabilitado cuando:
    • downloadingXmls === true (ya hay una descarga en curso)
    • !data?.total (no hay resultados con los filtros actuales)

Flujo del botón:

  1. Verifica si data.total > 1000.
  2. Si sí → window.confirm() con mensaje de advertencia.
  3. Si el usuario cancela → no hace nada.
  4. Si el usuario acepta (o total ≤ 1000) → construye objeto CfdiFilters con los filtros actuales (tipo, tipoComprobante, estado, fechaInicio, fechaFin, rfc, emisor, receptor, search, contribuyenteId).
  5. Llama downloadXmlsZip(filters).
  6. Crea blob URL y dispara descarga con nombre cfdis-xml-{timestamp}.zip.
  7. Limpia URL object.

4. Estructura del ZIP

Cada archivo dentro del ZIP se nombra {uuid}.xml o {id}.xml si el UUID es null.

El contenido es el XML original tal como se almacenó en cfdis.xml_original.


5. Archivos modificados

Archivo Cambio
apps/api/src/services/cfdi.service.ts getXmlsByFilters() — reusa whereClause de getCfdis, limit 1000
apps/api/src/controllers/cfdi.controller.ts downloadXmlsZip() — acepta filtros, genera ZIP con adm-zip
apps/api/src/routes/cfdi.routes.ts Ruta POST /download-xmls registrada
apps/web/lib/api/cfdi.ts downloadXmlsZip() ahora recibe CfdiFilters
apps/web/app/(dashboard)/cfdi/page.tsx Eliminados checkboxes y selectedIds; botón de descarga usa filtros actuales

6. Deploy

cd /root/HoruxDespachosNuevo
# Builds
npm run build --filter=@horux/api
npm run build --filter=@horux/web

# PM2 reload
pm2 reload horux-api
pm2 reload horux-web

Estado: Exitoso. Builds sin errores. Procesos reiniciados.


7. Notas técnicas

  • El backend no envía el mensaje de advertencia como respuesta; el frontend lo calcula comparando data.total (del listado paginado) contra 1,000. Esto es una aproximación eficiente porque evita un COUNT(* adicional.
  • Si el usuario aplica filtros muy amplios (ej. todo un mes sin restricciones), el ZIP puede contener hasta 1,000 archivos. Cada XML típicamente pesa entre 3 KB y 50 KB, por lo que el ZIP rara vez superará los 2030 MB.
  • El endpoint requiere autenticación y tenant middleware (como todo el módulo CFDI).