feat(cfdi): descarga masiva de XMLs como ZIP, limite 1,000

This commit is contained in:
Horux Dev
2026-05-24 21:19:56 +00:00
parent 80e2c099d9
commit 5c940847af
5 changed files with 131 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
import type { Request, Response, NextFunction } from 'express';
import * as cfdiService from '../services/cfdi.service.js';
import { AppError } from '../middlewares/error.middleware.js';
import AdmZip from 'adm-zip';
import { GRUPO_PF_EMPRESARIAL, GRUPO_PM_OTROS } from '../services/dashboard.service.js';
import { getRegimenesIgnoradosClaves } from '../services/regimen.service.js';
import { resolveContribuyenteContext } from '../utils/contribuyente-context.js';
@@ -75,6 +76,45 @@ export async function getXml(req: Request, res: Response, next: NextFunction) {
}
}
export async function downloadXmlsZip(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const ids = req.body.ids as number[];
if (!Array.isArray(ids) || ids.length === 0) {
return next(new AppError(400, 'Se requiere un array de IDs'));
}
if (ids.length > 1000) {
return next(new AppError(400, 'Máximo 1,000 CFDIs por descarga'));
}
const cfdis = await cfdiService.getXmlsByIds(req.tenantPool, ids);
const zip = new AdmZip();
let added = 0;
for (const cfdi of cfdis) {
if (cfdi.xml) {
const filename = `${cfdi.uuid || cfdi.id}.xml`;
zip.addFile(filename, Buffer.from(cfdi.xml, 'utf8'));
added++;
}
}
if (added === 0) {
return next(new AppError(404, 'No se encontraron XMLs para los CFDIs seleccionados'));
}
const zipBuffer = zip.toBuffer();
res.set('Content-Type', 'application/zip');
res.set('Content-Disposition', `attachment; filename="cfdis-${Date.now()}.zip"`);
res.send(zipBuffer);
} catch (error) {
next(error);
}
}
export async function listConceptos(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantPool) return next(new AppError(400, 'Tenant no configurado'));

View File

@@ -23,6 +23,7 @@ router.get('/conceptos', cfdiController.listConceptos);
router.get('/:id', cfdiController.getCfdiById);
router.get('/:id/conceptos', cfdiController.getConceptos);
router.get('/:id/xml', cfdiController.getXml);
router.post('/download-xmls', cfdiController.downloadXmlsZip);
router.post('/', checkCfdiLimit, cfdiController.createCfdi);
// Bulk upload: 10/hora — procesa hasta 50MB, pesado en parseo + inserts
router.post('/bulk', strictLimit, express.json({ limit: '50mb' }), checkCfdiLimit, cfdiController.createManyCfdis);

View File

@@ -357,6 +357,13 @@ export async function getXmlById(pool: Pool, id: string): Promise<string | null>
return rows[0]?.xml_original || null;
}
export async function getXmlsByIds(pool: Pool, ids: number[]): Promise<{ id: number; uuid: string; xml: string | null }[]> {
const { rows } = await pool.query(`
SELECT id, uuid, xml_original FROM cfdis WHERE id = ANY($1)
`, [ids]);
return rows.map((r: any) => ({ id: r.id, uuid: r.uuid, xml: r.xml_original || null }));
}
export interface CreateCfdiData {
uuid: string;
type: 'EMITIDO' | 'RECIBIDO';