feat(api): add CFDI API endpoints (list, detail, resumen)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { env } from './config/env.js';
|
|||||||
import { errorMiddleware } from './middlewares/error.middleware.js';
|
import { errorMiddleware } from './middlewares/error.middleware.js';
|
||||||
import { authRoutes } from './routes/auth.routes.js';
|
import { authRoutes } from './routes/auth.routes.js';
|
||||||
import { dashboardRoutes } from './routes/dashboard.routes.js';
|
import { dashboardRoutes } from './routes/dashboard.routes.js';
|
||||||
|
import { cfdiRoutes } from './routes/cfdi.routes.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ app.get('/health', (req, res) => {
|
|||||||
// API Routes
|
// API Routes
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/dashboard', dashboardRoutes);
|
app.use('/api/dashboard', dashboardRoutes);
|
||||||
|
app.use('/api/cfdi', cfdiRoutes);
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
app.use(errorMiddleware);
|
app.use(errorMiddleware);
|
||||||
|
|||||||
62
apps/api/src/controllers/cfdi.controller.ts
Normal file
62
apps/api/src/controllers/cfdi.controller.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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,
|
||||||
|
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, req.params.id);
|
||||||
|
|
||||||
|
if (!cfdi) {
|
||||||
|
return next(new AppError(404, 'CFDI no encontrado'));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(cfdi);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/api/src/routes/cfdi.routes.ts
Normal file
15
apps/api/src/routes/cfdi.routes.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { authenticate } from '../middlewares/auth.middleware.js';
|
||||||
|
import { tenantMiddleware } from '../middlewares/tenant.middleware.js';
|
||||||
|
import * as cfdiController from '../controllers/cfdi.controller.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use(authenticate);
|
||||||
|
router.use(tenantMiddleware);
|
||||||
|
|
||||||
|
router.get('/', cfdiController.getCfdis);
|
||||||
|
router.get('/resumen', cfdiController.getResumen);
|
||||||
|
router.get('/:id', cfdiController.getCfdiById);
|
||||||
|
|
||||||
|
export { router as cfdiRoutes };
|
||||||
128
apps/api/src/services/cfdi.service.ts
Normal file
128
apps/api/src/services/cfdi.service.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { prisma } from '../config/database.js';
|
||||||
|
import type { Cfdi, CfdiFilters, CfdiListResponse } from '@horux/shared';
|
||||||
|
|
||||||
|
export async function getCfdis(schema: string, filters: CfdiFilters): Promise<CfdiListResponse> {
|
||||||
|
const page = filters.page || 1;
|
||||||
|
const limit = filters.limit || 20;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
let whereClause = 'WHERE 1=1';
|
||||||
|
const params: any[] = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
if (filters.tipo) {
|
||||||
|
whereClause += ` AND tipo = $${paramIndex++}`;
|
||||||
|
params.push(filters.tipo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.estado) {
|
||||||
|
whereClause += ` AND estado = $${paramIndex++}`;
|
||||||
|
params.push(filters.estado);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.fechaInicio) {
|
||||||
|
whereClause += ` AND fecha_emision >= $${paramIndex++}`;
|
||||||
|
params.push(filters.fechaInicio);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.fechaFin) {
|
||||||
|
whereClause += ` AND fecha_emision <= $${paramIndex++}`;
|
||||||
|
params.push(filters.fechaFin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.rfc) {
|
||||||
|
whereClause += ` AND (rfc_emisor ILIKE $${paramIndex} OR rfc_receptor ILIKE $${paramIndex++})`;
|
||||||
|
params.push(`%${filters.rfc}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.search) {
|
||||||
|
whereClause += ` AND (uuid_fiscal ILIKE $${paramIndex} OR nombre_emisor ILIKE $${paramIndex} OR nombre_receptor ILIKE $${paramIndex++})`;
|
||||||
|
params.push(`%${filters.search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const countResult = await prisma.$queryRawUnsafe<[{ count: number }]>(`
|
||||||
|
SELECT COUNT(*) as count FROM "${schema}".cfdis ${whereClause}
|
||||||
|
`, ...params);
|
||||||
|
|
||||||
|
const total = Number(countResult[0]?.count || 0);
|
||||||
|
|
||||||
|
params.push(limit, offset);
|
||||||
|
const data = await prisma.$queryRawUnsafe<Cfdi[]>(`
|
||||||
|
SELECT
|
||||||
|
id, uuid_fiscal as "uuidFiscal", tipo, serie, folio,
|
||||||
|
fecha_emision as "fechaEmision", fecha_timbrado as "fechaTimbrado",
|
||||||
|
rfc_emisor as "rfcEmisor", nombre_emisor as "nombreEmisor",
|
||||||
|
rfc_receptor as "rfcReceptor", nombre_receptor as "nombreReceptor",
|
||||||
|
subtotal, descuento, iva, isr_retenido as "isrRetenido",
|
||||||
|
iva_retenido as "ivaRetenido", total, moneda,
|
||||||
|
tipo_cambio as "tipoCambio", metodo_pago as "metodoPago",
|
||||||
|
forma_pago as "formaPago", uso_cfdi as "usoCfdi",
|
||||||
|
estado, xml_url as "xmlUrl", pdf_url as "pdfUrl",
|
||||||
|
created_at as "createdAt"
|
||||||
|
FROM "${schema}".cfdis
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY fecha_emision DESC
|
||||||
|
LIMIT $${paramIndex++} OFFSET $${paramIndex}
|
||||||
|
`, ...params);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCfdiById(schema: string, id: string): Promise<Cfdi | null> {
|
||||||
|
const result = await prisma.$queryRawUnsafe<Cfdi[]>(`
|
||||||
|
SELECT
|
||||||
|
id, uuid_fiscal as "uuidFiscal", tipo, serie, folio,
|
||||||
|
fecha_emision as "fechaEmision", fecha_timbrado as "fechaTimbrado",
|
||||||
|
rfc_emisor as "rfcEmisor", nombre_emisor as "nombreEmisor",
|
||||||
|
rfc_receptor as "rfcReceptor", nombre_receptor as "nombreReceptor",
|
||||||
|
subtotal, descuento, iva, isr_retenido as "isrRetenido",
|
||||||
|
iva_retenido as "ivaRetenido", total, moneda,
|
||||||
|
tipo_cambio as "tipoCambio", metodo_pago as "metodoPago",
|
||||||
|
forma_pago as "formaPago", uso_cfdi as "usoCfdi",
|
||||||
|
estado, xml_url as "xmlUrl", pdf_url as "pdfUrl",
|
||||||
|
created_at as "createdAt"
|
||||||
|
FROM "${schema}".cfdis
|
||||||
|
WHERE id = $1
|
||||||
|
`, id);
|
||||||
|
|
||||||
|
return result[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResumenCfdis(schema: string, año: number, mes: number) {
|
||||||
|
const result = await prisma.$queryRawUnsafe<[{
|
||||||
|
total_ingresos: number;
|
||||||
|
total_egresos: number;
|
||||||
|
count_ingresos: number;
|
||||||
|
count_egresos: number;
|
||||||
|
iva_trasladado: number;
|
||||||
|
iva_acreditable: number;
|
||||||
|
}]>(`
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN total ELSE 0 END), 0) as total_ingresos,
|
||||||
|
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN total ELSE 0 END), 0) as total_egresos,
|
||||||
|
COUNT(CASE WHEN tipo = 'ingreso' THEN 1 END) as count_ingresos,
|
||||||
|
COUNT(CASE WHEN tipo = 'egreso' THEN 1 END) as count_egresos,
|
||||||
|
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN iva ELSE 0 END), 0) as iva_trasladado,
|
||||||
|
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN iva ELSE 0 END), 0) as iva_acreditable
|
||||||
|
FROM "${schema}".cfdis
|
||||||
|
WHERE estado = 'vigente'
|
||||||
|
AND EXTRACT(YEAR FROM fecha_emision) = $1
|
||||||
|
AND EXTRACT(MONTH FROM fecha_emision) = $2
|
||||||
|
`, año, mes);
|
||||||
|
|
||||||
|
const r = result[0];
|
||||||
|
return {
|
||||||
|
totalIngresos: Number(r?.total_ingresos || 0),
|
||||||
|
totalEgresos: Number(r?.total_egresos || 0),
|
||||||
|
countIngresos: Number(r?.count_ingresos || 0),
|
||||||
|
countEgresos: Number(r?.count_egresos || 0),
|
||||||
|
ivaTrasladado: Number(r?.iva_trasladado || 0),
|
||||||
|
ivaAcreditable: Number(r?.iva_acreditable || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user