feat(api): add impuestos API endpoints (IVA/ISR mensual y resumen)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-01-22 02:23:54 +00:00
parent a81d8437ce
commit 9d49f8a833
4 changed files with 239 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ 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'; import { cfdiRoutes } from './routes/cfdi.routes.js';
import { impuestosRoutes } from './routes/impuestos.routes.js';
const app = express(); const app = express();
@@ -29,6 +30,7 @@ app.get('/health', (req, res) => {
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); app.use('/api/cfdi', cfdiRoutes);
app.use('/api/impuestos', impuestosRoutes);
// Error handling // Error handling
app.use(errorMiddleware); app.use(errorMiddleware);

View File

@@ -0,0 +1,63 @@
import type { Request, Response, NextFunction } from 'express';
import * as impuestosService from '../services/impuestos.service.js';
import { AppError } from '../middlewares/error.middleware.js';
export async function getIvaMensual(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 data = await impuestosService.getIvaMensual(req.tenantSchema, año);
res.json(data);
} catch (error) {
next(error);
}
}
export async function getResumenIva(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 impuestosService.getResumenIva(req.tenantSchema, año, mes);
res.json(resumen);
} catch (error) {
next(error);
}
}
export async function getIsrMensual(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 data = await impuestosService.getIsrMensual(req.tenantSchema, año);
res.json(data);
} catch (error) {
next(error);
}
}
export async function getResumenIsr(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 impuestosService.getResumenIsr(req.tenantSchema, año, mes);
res.json(resumen);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,16 @@
import { Router } from 'express';
import { authenticate } from '../middlewares/auth.middleware.js';
import { tenantMiddleware } from '../middlewares/tenant.middleware.js';
import * as impuestosController from '../controllers/impuestos.controller.js';
const router = Router();
router.use(authenticate);
router.use(tenantMiddleware);
router.get('/iva/mensual', impuestosController.getIvaMensual);
router.get('/iva/resumen', impuestosController.getResumenIva);
router.get('/isr/mensual', impuestosController.getIsrMensual);
router.get('/isr/resumen', impuestosController.getResumenIsr);
export { router as impuestosRoutes };

View File

@@ -0,0 +1,158 @@
import { prisma } from '../config/database.js';
import type { IvaMensual, IsrMensual, ResumenIva, ResumenIsr } from '@horux/shared';
export async function getIvaMensual(schema: string, año: number): Promise<IvaMensual[]> {
const data = await prisma.$queryRawUnsafe<IvaMensual[]>(`
SELECT
id, año, mes,
iva_trasladado as "ivaTrasladado",
iva_acreditable as "ivaAcreditable",
COALESCE(iva_retenido, 0) as "ivaRetenido",
resultado, acumulado, estado,
fecha_declaracion as "fechaDeclaracion"
FROM "${schema}".iva_mensual
WHERE año = $1
ORDER BY mes
`, año);
return data.map(row => ({
...row,
ivaTrasladado: Number(row.ivaTrasladado),
ivaAcreditable: Number(row.ivaAcreditable),
ivaRetenido: Number(row.ivaRetenido),
resultado: Number(row.resultado),
acumulado: Number(row.acumulado),
}));
}
export async function getResumenIva(schema: string, año: number, mes: number): Promise<ResumenIva> {
// Get from iva_mensual if exists
const existing = await prisma.$queryRawUnsafe<any[]>(`
SELECT * FROM "${schema}".iva_mensual WHERE año = $1 AND mes = $2
`, año, mes);
if (existing && existing.length > 0) {
const record = existing[0];
const [acumuladoResult] = await prisma.$queryRawUnsafe<[{ total: number }]>(`
SELECT COALESCE(SUM(resultado), 0) as total
FROM "${schema}".iva_mensual
WHERE año = $1 AND mes <= $2
`, año, mes);
return {
trasladado: Number(record.iva_trasladado || 0),
acreditable: Number(record.iva_acreditable || 0),
retenido: Number(record.iva_retenido || 0),
resultado: Number(record.resultado || 0),
acumuladoAnual: Number(acumuladoResult?.total || 0),
};
}
// Calculate from CFDIs if no iva_mensual record
const [calcResult] = await prisma.$queryRawUnsafe<[{
trasladado: number;
acreditable: number;
retenido: number;
}]>(`
SELECT
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN iva ELSE 0 END), 0) as trasladado,
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN iva ELSE 0 END), 0) as acreditable,
COALESCE(SUM(iva_retenido), 0) as retenido
FROM "${schema}".cfdis
WHERE estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) = $2
`, año, mes);
const trasladado = Number(calcResult?.trasladado || 0);
const acreditable = Number(calcResult?.acreditable || 0);
const retenido = Number(calcResult?.retenido || 0);
const resultado = trasladado - acreditable - retenido;
return {
trasladado,
acreditable,
retenido,
resultado,
acumuladoAnual: resultado,
};
}
export async function getIsrMensual(schema: string, año: number): Promise<IsrMensual[]> {
// Check if isr_mensual table exists
try {
const data = await prisma.$queryRawUnsafe<IsrMensual[]>(`
SELECT
id, año, mes,
ingresos_acumulados as "ingresosAcumulados",
deducciones,
base_gravable as "baseGravable",
isr_causado as "isrCausado",
isr_retenido as "isrRetenido",
isr_a_pagar as "isrAPagar",
estado,
fecha_declaracion as "fechaDeclaracion"
FROM "${schema}".isr_mensual
WHERE año = $1
ORDER BY mes
`, año);
return data.map(row => ({
...row,
ingresosAcumulados: Number(row.ingresosAcumulados),
deducciones: Number(row.deducciones),
baseGravable: Number(row.baseGravable),
isrCausado: Number(row.isrCausado),
isrRetenido: Number(row.isrRetenido),
isrAPagar: Number(row.isrAPagar),
}));
} catch {
// Table doesn't exist, return empty array
return [];
}
}
export async function getResumenIsr(schema: string, año: number, mes: number): Promise<ResumenIsr> {
// Calculate from CFDIs
const [ingresos] = await prisma.$queryRawUnsafe<[{ total: number }]>(`
SELECT COALESCE(SUM(total), 0) as total
FROM "${schema}".cfdis
WHERE tipo = 'ingreso' AND estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) <= $2
`, año, mes);
const [egresos] = await prisma.$queryRawUnsafe<[{ total: number }]>(`
SELECT COALESCE(SUM(total), 0) as total
FROM "${schema}".cfdis
WHERE tipo = 'egreso' AND estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) <= $2
`, año, mes);
const [retenido] = await prisma.$queryRawUnsafe<[{ total: number }]>(`
SELECT COALESCE(SUM(isr_retenido), 0) as total
FROM "${schema}".cfdis
WHERE estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) <= $2
`, año, mes);
const ingresosAcumulados = Number(ingresos?.total || 0);
const deducciones = Number(egresos?.total || 0);
const baseGravable = Math.max(0, ingresosAcumulados - deducciones);
// Simplified ISR calculation (actual calculation would use SAT tables)
const isrCausado = baseGravable * 0.30; // 30% simplified rate
const isrRetenido = Number(retenido?.total || 0);
const isrAPagar = Math.max(0, isrCausado - isrRetenido);
return {
ingresosAcumulados,
deducciones,
baseGravable,
isrCausado,
isrRetenido,
isrAPagar,
};
}