- Add /cfdi/emisores and /cfdi/receptores API endpoints - Search by RFC or nombre with ILIKE - Show suggestions dropdown while typing (min 2 chars) - Click suggestion to select and populate filter input - Show loading state while searching Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import type { Request, Response, NextFunction } from 'express';
|
|
import * as cfdiService from '../services/cfdi.service.js';
|
|
import { AppError } from '../middlewares/error.middleware.js';
|
|
import type { CfdiFilters } from '@horux/shared';
|
|
|
|
export async function getCfdis(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
const filters: CfdiFilters = {
|
|
tipo: req.query.tipo as any,
|
|
estado: req.query.estado as any,
|
|
fechaInicio: req.query.fechaInicio as string,
|
|
fechaFin: req.query.fechaFin as string,
|
|
rfc: req.query.rfc as string,
|
|
emisor: req.query.emisor as string,
|
|
receptor: req.query.receptor as string,
|
|
search: req.query.search as string,
|
|
page: parseInt(req.query.page as string) || 1,
|
|
limit: parseInt(req.query.limit as string) || 20,
|
|
};
|
|
|
|
const result = await cfdiService.getCfdis(req.tenantSchema, filters);
|
|
res.json(result);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function getCfdiById(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
const cfdi = await cfdiService.getCfdiById(req.tenantSchema, String(req.params.id));
|
|
|
|
if (!cfdi) {
|
|
return next(new AppError(404, 'CFDI no encontrado'));
|
|
}
|
|
|
|
res.json(cfdi);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function getXml(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
const xml = await cfdiService.getXmlById(req.tenantSchema, String(req.params.id));
|
|
|
|
if (!xml) {
|
|
return next(new AppError(404, 'XML no encontrado para este CFDI'));
|
|
}
|
|
|
|
res.set('Content-Type', 'application/xml');
|
|
res.set('Content-Disposition', `attachment; filename="cfdi-${req.params.id}.xml"`);
|
|
res.send(xml);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function getEmisores(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
const search = (req.query.search as string) || '';
|
|
if (search.length < 2) {
|
|
return res.json([]);
|
|
}
|
|
|
|
const emisores = await cfdiService.getEmisores(req.tenantSchema, search);
|
|
res.json(emisores);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function getReceptores(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
const search = (req.query.search as string) || '';
|
|
if (search.length < 2) {
|
|
return res.json([]);
|
|
}
|
|
|
|
const receptores = await cfdiService.getReceptores(req.tenantSchema, search);
|
|
res.json(receptores);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function getResumen(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
const año = parseInt(req.query.año as string) || new Date().getFullYear();
|
|
const mes = parseInt(req.query.mes as string) || new Date().getMonth() + 1;
|
|
|
|
const resumen = await cfdiService.getResumenCfdis(req.tenantSchema, año, mes);
|
|
res.json(resumen);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function createCfdi(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
// Only admin and contador can create CFDIs
|
|
if (!['admin', 'contador'].includes(req.user!.role)) {
|
|
return next(new AppError(403, 'No tienes permisos para agregar CFDIs'));
|
|
}
|
|
|
|
const cfdi = await cfdiService.createCfdi(req.tenantSchema, req.body);
|
|
res.status(201).json(cfdi);
|
|
} catch (error: any) {
|
|
if (error.message?.includes('duplicate')) {
|
|
return next(new AppError(409, 'Este CFDI ya existe (UUID duplicado)'));
|
|
}
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
export async function createManyCfdis(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
if (!['admin', 'contador'].includes(req.user!.role)) {
|
|
return next(new AppError(403, 'No tienes permisos para agregar CFDIs'));
|
|
}
|
|
|
|
if (!Array.isArray(req.body.cfdis)) {
|
|
return next(new AppError(400, 'Se requiere un array de CFDIs'));
|
|
}
|
|
|
|
const batchInfo = {
|
|
batchNumber: req.body.batchNumber || 1,
|
|
totalBatches: req.body.totalBatches || 1,
|
|
totalFiles: req.body.totalFiles || req.body.cfdis.length
|
|
};
|
|
|
|
console.log(`[CFDI Bulk] Lote ${batchInfo.batchNumber}/${batchInfo.totalBatches} - ${req.body.cfdis.length} CFDIs para schema ${req.tenantSchema}`);
|
|
|
|
const result = await cfdiService.createManyCfdisBatch(req.tenantSchema, req.body.cfdis);
|
|
|
|
res.status(201).json({
|
|
message: `Lote ${batchInfo.batchNumber} procesado`,
|
|
batchNumber: batchInfo.batchNumber,
|
|
totalBatches: batchInfo.totalBatches,
|
|
inserted: result.inserted,
|
|
duplicates: result.duplicates,
|
|
errors: result.errors,
|
|
errorMessages: result.errorMessages.slice(0, 5) // Limit error messages
|
|
});
|
|
} catch (error: any) {
|
|
console.error('[CFDI Bulk Error]', error.message, error.stack);
|
|
next(new AppError(400, error.message || 'Error al procesar CFDIs'));
|
|
}
|
|
}
|
|
|
|
export async function deleteCfdi(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.tenantSchema) {
|
|
return next(new AppError(400, 'Schema no configurado'));
|
|
}
|
|
|
|
if (!['admin', 'contador'].includes(req.user!.role)) {
|
|
return next(new AppError(403, 'No tienes permisos para eliminar CFDIs'));
|
|
}
|
|
|
|
await cfdiService.deleteCfdi(req.tenantSchema, String(req.params.id));
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|