refactor: migrate all tenant services and controllers to pool-based queries

Replace Prisma raw queries with pg.Pool for all tenant-scoped services:
cfdi, dashboard, impuestos, alertas, calendario, reportes, export, and SAT.
Controllers now pass req.tenantPool instead of req.tenantSchema.
Fixes SQL injection in calendario.service.ts (parameterized interval).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-03-15 23:29:20 +00:00
parent 7eaeefa09d
commit b064f15404
15 changed files with 359 additions and 412 deletions

View File

@@ -1,10 +1,10 @@
import { Request, Response, NextFunction } from 'express';
import type { Request, Response, NextFunction } from 'express';
import * as alertasService from '../services/alertas.service.js';
export async function getAlertas(req: Request, res: Response, next: NextFunction) {
try {
const { leida, resuelta, prioridad } = req.query;
const alertas = await alertasService.getAlertas(req.tenantSchema!, {
const alertas = await alertasService.getAlertas(req.tenantPool!, {
leida: leida === 'true' ? true : leida === 'false' ? false : undefined,
resuelta: resuelta === 'true' ? true : resuelta === 'false' ? false : undefined,
prioridad: prioridad as string,
@@ -17,7 +17,7 @@ export async function getAlertas(req: Request, res: Response, next: NextFunction
export async function getAlerta(req: Request, res: Response, next: NextFunction) {
try {
const alerta = await alertasService.getAlertaById(req.tenantSchema!, parseInt(String(req.params.id)));
const alerta = await alertasService.getAlertaById(req.tenantPool!, parseInt(String(req.params.id)));
if (!alerta) {
return res.status(404).json({ message: 'Alerta no encontrada' });
}
@@ -29,7 +29,7 @@ export async function getAlerta(req: Request, res: Response, next: NextFunction)
export async function createAlerta(req: Request, res: Response, next: NextFunction) {
try {
const alerta = await alertasService.createAlerta(req.tenantSchema!, req.body);
const alerta = await alertasService.createAlerta(req.tenantPool!, req.body);
res.status(201).json(alerta);
} catch (error) {
next(error);
@@ -38,7 +38,7 @@ export async function createAlerta(req: Request, res: Response, next: NextFuncti
export async function updateAlerta(req: Request, res: Response, next: NextFunction) {
try {
const alerta = await alertasService.updateAlerta(req.tenantSchema!, parseInt(String(req.params.id)), req.body);
const alerta = await alertasService.updateAlerta(req.tenantPool!, parseInt(String(req.params.id)), req.body);
res.json(alerta);
} catch (error) {
next(error);
@@ -47,7 +47,7 @@ export async function updateAlerta(req: Request, res: Response, next: NextFuncti
export async function deleteAlerta(req: Request, res: Response, next: NextFunction) {
try {
await alertasService.deleteAlerta(req.tenantSchema!, parseInt(String(req.params.id)));
await alertasService.deleteAlerta(req.tenantPool!, parseInt(String(req.params.id)));
res.status(204).send();
} catch (error) {
next(error);
@@ -56,7 +56,7 @@ export async function deleteAlerta(req: Request, res: Response, next: NextFuncti
export async function getStats(req: Request, res: Response, next: NextFunction) {
try {
const stats = await alertasService.getStats(req.tenantSchema!);
const stats = await alertasService.getStats(req.tenantPool!);
res.json(stats);
} catch (error) {
next(error);
@@ -65,7 +65,7 @@ export async function getStats(req: Request, res: Response, next: NextFunction)
export async function markAllAsRead(req: Request, res: Response, next: NextFunction) {
try {
await alertasService.markAllAsRead(req.tenantSchema!);
await alertasService.markAllAsRead(req.tenantPool!);
res.json({ success: true });
} catch (error) {
next(error);

View File

@@ -1,4 +1,4 @@
import { Request, Response, NextFunction } from 'express';
import type { Request, Response, NextFunction } from 'express';
import * as calendarioService from '../services/calendario.service.js';
export async function getEventos(req: Request, res: Response, next: NextFunction) {
@@ -7,7 +7,7 @@ export async function getEventos(req: Request, res: Response, next: NextFunction
const añoNum = parseInt(año as string) || new Date().getFullYear();
const mesNum = mes ? parseInt(mes as string) : undefined;
const eventos = await calendarioService.getEventos(req.tenantSchema!, añoNum, mesNum);
const eventos = await calendarioService.getEventos(req.tenantPool!, añoNum, mesNum);
res.json(eventos);
} catch (error) {
next(error);
@@ -17,7 +17,7 @@ export async function getEventos(req: Request, res: Response, next: NextFunction
export async function getProximos(req: Request, res: Response, next: NextFunction) {
try {
const dias = parseInt(req.query.dias as string) || 30;
const eventos = await calendarioService.getProximosEventos(req.tenantSchema!, dias);
const eventos = await calendarioService.getProximosEventos(req.tenantPool!, dias);
res.json(eventos);
} catch (error) {
next(error);
@@ -26,7 +26,7 @@ export async function getProximos(req: Request, res: Response, next: NextFunctio
export async function createEvento(req: Request, res: Response, next: NextFunction) {
try {
const evento = await calendarioService.createEvento(req.tenantSchema!, req.body);
const evento = await calendarioService.createEvento(req.tenantPool!, req.body);
res.status(201).json(evento);
} catch (error) {
next(error);
@@ -35,7 +35,7 @@ export async function createEvento(req: Request, res: Response, next: NextFuncti
export async function updateEvento(req: Request, res: Response, next: NextFunction) {
try {
const evento = await calendarioService.updateEvento(req.tenantSchema!, parseInt(String(req.params.id)), req.body);
const evento = await calendarioService.updateEvento(req.tenantPool!, parseInt(String(req.params.id)), req.body);
res.json(evento);
} catch (error) {
next(error);
@@ -44,7 +44,7 @@ export async function updateEvento(req: Request, res: Response, next: NextFuncti
export async function deleteEvento(req: Request, res: Response, next: NextFunction) {
try {
await calendarioService.deleteEvento(req.tenantSchema!, parseInt(String(req.params.id)));
await calendarioService.deleteEvento(req.tenantPool!, parseInt(String(req.params.id)));
res.status(204).send();
} catch (error) {
next(error);

View File

@@ -5,8 +5,8 @@ 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'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const filters: CfdiFilters = {
@@ -22,7 +22,7 @@ export async function getCfdis(req: Request, res: Response, next: NextFunction)
limit: parseInt(req.query.limit as string) || 20,
};
const result = await cfdiService.getCfdis(req.tenantSchema, filters);
const result = await cfdiService.getCfdis(req.tenantPool, filters);
res.json(result);
} catch (error) {
next(error);
@@ -31,11 +31,11 @@ export async function getCfdis(req: Request, res: Response, next: NextFunction)
export async function getCfdiById(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const cfdi = await cfdiService.getCfdiById(req.tenantSchema, String(req.params.id));
const cfdi = await cfdiService.getCfdiById(req.tenantPool, String(req.params.id));
if (!cfdi) {
return next(new AppError(404, 'CFDI no encontrado'));
@@ -49,11 +49,11 @@ export async function getCfdiById(req: Request, res: Response, next: NextFunctio
export async function getXml(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const xml = await cfdiService.getXmlById(req.tenantSchema, String(req.params.id));
const xml = await cfdiService.getXmlById(req.tenantPool, String(req.params.id));
if (!xml) {
return next(new AppError(404, 'XML no encontrado para este CFDI'));
@@ -69,8 +69,8 @@ export async function getXml(req: Request, res: Response, next: NextFunction) {
export async function getEmisores(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const search = (req.query.search as string) || '';
@@ -78,7 +78,7 @@ export async function getEmisores(req: Request, res: Response, next: NextFunctio
return res.json([]);
}
const emisores = await cfdiService.getEmisores(req.tenantSchema, search);
const emisores = await cfdiService.getEmisores(req.tenantPool, search);
res.json(emisores);
} catch (error) {
next(error);
@@ -87,8 +87,8 @@ export async function getEmisores(req: Request, res: Response, next: NextFunctio
export async function getReceptores(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const search = (req.query.search as string) || '';
@@ -96,7 +96,7 @@ export async function getReceptores(req: Request, res: Response, next: NextFunct
return res.json([]);
}
const receptores = await cfdiService.getReceptores(req.tenantSchema, search);
const receptores = await cfdiService.getReceptores(req.tenantPool, search);
res.json(receptores);
} catch (error) {
next(error);
@@ -105,14 +105,14 @@ export async function getReceptores(req: Request, res: Response, next: NextFunct
export async function getResumen(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant 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);
const resumen = await cfdiService.getResumenCfdis(req.tenantPool, año, mes);
res.json(resumen);
} catch (error) {
next(error);
@@ -121,16 +121,15 @@ export async function getResumen(req: Request, res: Response, next: NextFunction
export async function createCfdi(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant 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);
const cfdi = await cfdiService.createCfdi(req.tenantPool, req.body);
res.status(201).json(cfdi);
} catch (error: any) {
if (error.message?.includes('duplicate')) {
@@ -142,8 +141,8 @@ export async function createCfdi(req: Request, res: Response, next: NextFunction
export async function createManyCfdis(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
if (!['admin', 'contador'].includes(req.user!.role)) {
@@ -160,9 +159,9 @@ export async function createManyCfdis(req: Request, res: Response, next: NextFun
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}`);
console.log(`[CFDI Bulk] Lote ${batchInfo.batchNumber}/${batchInfo.totalBatches} - ${req.body.cfdis.length} CFDIs`);
const result = await cfdiService.createManyCfdisBatch(req.tenantSchema, req.body.cfdis);
const result = await cfdiService.createManyCfdisBatch(req.tenantPool, req.body.cfdis);
res.status(201).json({
message: `Lote ${batchInfo.batchNumber} procesado`,
@@ -171,7 +170,7 @@ export async function createManyCfdis(req: Request, res: Response, next: NextFun
inserted: result.inserted,
duplicates: result.duplicates,
errors: result.errors,
errorMessages: result.errorMessages.slice(0, 5) // Limit error messages
errorMessages: result.errorMessages.slice(0, 5)
});
} catch (error: any) {
console.error('[CFDI Bulk Error]', error.message, error.stack);
@@ -181,15 +180,15 @@ export async function createManyCfdis(req: Request, res: Response, next: NextFun
export async function deleteCfdi(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant 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));
await cfdiService.deleteCfdi(req.tenantPool, String(req.params.id));
res.status(204).send();
} catch (error) {
next(error);

View File

@@ -4,14 +4,14 @@ import { AppError } from '../middlewares/error.middleware.js';
export async function getKpis(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant 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 kpis = await dashboardService.getKpis(req.tenantSchema, año, mes);
const kpis = await dashboardService.getKpis(req.tenantPool, año, mes);
res.json(kpis);
} catch (error) {
next(error);
@@ -20,13 +20,13 @@ export async function getKpis(req: Request, res: Response, next: NextFunction) {
export async function getIngresosEgresos(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const año = parseInt(req.query.año as string) || new Date().getFullYear();
const data = await dashboardService.getIngresosEgresos(req.tenantSchema, año);
const data = await dashboardService.getIngresosEgresos(req.tenantPool, año);
res.json(data);
} catch (error) {
next(error);
@@ -35,14 +35,14 @@ export async function getIngresosEgresos(req: Request, res: Response, next: Next
export async function getResumenFiscal(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant 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 dashboardService.getResumenFiscal(req.tenantSchema, año, mes);
const resumen = await dashboardService.getResumenFiscal(req.tenantPool, año, mes);
res.json(resumen);
} catch (error) {
next(error);
@@ -51,13 +51,13 @@ export async function getResumenFiscal(req: Request, res: Response, next: NextFu
export async function getAlertas(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const limit = parseInt(req.query.limit as string) || 5;
const alertas = await dashboardService.getAlertas(req.tenantSchema, limit);
const alertas = await dashboardService.getAlertas(req.tenantPool, limit);
res.json(alertas);
} catch (error) {
next(error);

View File

@@ -1,10 +1,10 @@
import { Request, Response, NextFunction } from 'express';
import type { Request, Response, NextFunction } from 'express';
import * as exportService from '../services/export.service.js';
export async function exportCfdis(req: Request, res: Response, next: NextFunction) {
try {
const { tipo, estado, fechaInicio, fechaFin } = req.query;
const buffer = await exportService.exportCfdisToExcel(req.tenantSchema!, {
const buffer = await exportService.exportCfdisToExcel(req.tenantPool!, {
tipo: tipo as string,
estado: estado as string,
fechaInicio: fechaInicio as string,
@@ -27,7 +27,7 @@ export async function exportReporte(req: Request, res: Response, next: NextFunct
const fin = (fechaFin as string) || now.toISOString().split('T')[0];
const buffer = await exportService.exportReporteToExcel(
req.tenantSchema!,
req.tenantPool!,
tipo as 'estado-resultados' | 'flujo-efectivo',
inicio,
fin

View File

@@ -4,12 +4,12 @@ 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'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const año = parseInt(req.query.año as string) || new Date().getFullYear();
const data = await impuestosService.getIvaMensual(req.tenantSchema, año);
const data = await impuestosService.getIvaMensual(req.tenantPool, año);
res.json(data);
} catch (error) {
next(error);
@@ -18,14 +18,14 @@ export async function getIvaMensual(req: Request, res: Response, next: NextFunct
export async function getResumenIva(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant 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);
const resumen = await impuestosService.getResumenIva(req.tenantPool, año, mes);
res.json(resumen);
} catch (error) {
next(error);
@@ -34,12 +34,12 @@ export async function getResumenIva(req: Request, res: Response, next: NextFunct
export async function getIsrMensual(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
const año = parseInt(req.query.año as string) || new Date().getFullYear();
const data = await impuestosService.getIsrMensual(req.tenantSchema, año);
const data = await impuestosService.getIsrMensual(req.tenantPool, año);
res.json(data);
} catch (error) {
next(error);
@@ -48,14 +48,14 @@ export async function getIsrMensual(req: Request, res: Response, next: NextFunct
export async function getResumenIsr(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantSchema) {
return next(new AppError(400, 'Schema no configurado'));
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant 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);
const resumen = await impuestosService.getResumenIsr(req.tenantPool, año, mes);
res.json(resumen);
} catch (error) {
next(error);

View File

@@ -1,4 +1,4 @@
import { Request, Response, NextFunction } from 'express';
import type { Request, Response, NextFunction } from 'express';
import * as reportesService from '../services/reportes.service.js';
export async function getEstadoResultados(req: Request, res: Response, next: NextFunction) {
@@ -8,8 +8,7 @@ export async function getEstadoResultados(req: Request, res: Response, next: Nex
const inicio = (fechaInicio as string) || `${now.getFullYear()}-01-01`;
const fin = (fechaFin as string) || now.toISOString().split('T')[0];
console.log('[reportes] getEstadoResultados - schema:', req.tenantSchema, 'inicio:', inicio, 'fin:', fin);
const data = await reportesService.getEstadoResultados(req.tenantSchema!, inicio, fin);
const data = await reportesService.getEstadoResultados(req.tenantPool!, inicio, fin);
res.json(data);
} catch (error) {
console.error('[reportes] Error en getEstadoResultados:', error);
@@ -24,7 +23,7 @@ export async function getFlujoEfectivo(req: Request, res: Response, next: NextFu
const inicio = (fechaInicio as string) || `${now.getFullYear()}-01-01`;
const fin = (fechaFin as string) || now.toISOString().split('T')[0];
const data = await reportesService.getFlujoEfectivo(req.tenantSchema!, inicio, fin);
const data = await reportesService.getFlujoEfectivo(req.tenantPool!, inicio, fin);
res.json(data);
} catch (error) {
next(error);
@@ -34,7 +33,7 @@ export async function getFlujoEfectivo(req: Request, res: Response, next: NextFu
export async function getComparativo(req: Request, res: Response, next: NextFunction) {
try {
const año = parseInt(req.query.año as string) || new Date().getFullYear();
const data = await reportesService.getComparativo(req.tenantSchema!, año);
const data = await reportesService.getComparativo(req.tenantPool!, año);
res.json(data);
} catch (error) {
next(error);
@@ -49,7 +48,7 @@ export async function getConcentradoRfc(req: Request, res: Response, next: NextF
const fin = (fechaFin as string) || now.toISOString().split('T')[0];
const tipoRfc = (tipo as 'cliente' | 'proveedor') || 'cliente';
const data = await reportesService.getConcentradoRfc(req.tenantSchema!, inicio, fin, tipoRfc);
const data = await reportesService.getConcentradoRfc(req.tenantPool!, inicio, fin, tipoRfc);
res.json(data);
} catch (error) {
next(error);

View File

@@ -1,8 +1,8 @@
import { prisma } from '../config/database.js';
import type { Pool } from 'pg';
import type { AlertaFull, AlertaCreate, AlertaUpdate, AlertasStats } from '@horux/shared';
export async function getAlertas(
schema: string,
pool: Pool,
filters: { leida?: boolean; resuelta?: boolean; prioridad?: string }
): Promise<AlertaFull[]> {
let whereClause = 'WHERE 1=1';
@@ -22,43 +22,43 @@ export async function getAlertas(
params.push(filters.prioridad);
}
const alertas = await prisma.$queryRawUnsafe<AlertaFull[]>(`
const { rows } = await pool.query(`
SELECT id, tipo, titulo, mensaje, prioridad,
fecha_vencimiento as "fechaVencimiento",
leida, resuelta, created_at as "createdAt"
FROM "${schema}".alertas
FROM alertas
${whereClause}
ORDER BY
CASE prioridad WHEN 'alta' THEN 1 WHEN 'media' THEN 2 ELSE 3 END,
created_at DESC
`, ...params);
`, params);
return alertas;
return rows;
}
export async function getAlertaById(schema: string, id: number): Promise<AlertaFull | null> {
const [alerta] = await prisma.$queryRawUnsafe<AlertaFull[]>(`
export async function getAlertaById(pool: Pool, id: number): Promise<AlertaFull | null> {
const { rows } = await pool.query(`
SELECT id, tipo, titulo, mensaje, prioridad,
fecha_vencimiento as "fechaVencimiento",
leida, resuelta, created_at as "createdAt"
FROM "${schema}".alertas
FROM alertas
WHERE id = $1
`, id);
return alerta || null;
`, [id]);
return rows[0] || null;
}
export async function createAlerta(schema: string, data: AlertaCreate): Promise<AlertaFull> {
const [alerta] = await prisma.$queryRawUnsafe<AlertaFull[]>(`
INSERT INTO "${schema}".alertas (tipo, titulo, mensaje, prioridad, fecha_vencimiento)
export async function createAlerta(pool: Pool, data: AlertaCreate): Promise<AlertaFull> {
const { rows } = await pool.query(`
INSERT INTO alertas (tipo, titulo, mensaje, prioridad, fecha_vencimiento)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, tipo, titulo, mensaje, prioridad,
fecha_vencimiento as "fechaVencimiento",
leida, resuelta, created_at as "createdAt"
`, data.tipo, data.titulo, data.mensaje, data.prioridad, data.fechaVencimiento || null);
return alerta;
`, [data.tipo, data.titulo, data.mensaje, data.prioridad, data.fechaVencimiento || null]);
return rows[0];
}
export async function updateAlerta(schema: string, id: number, data: AlertaUpdate): Promise<AlertaFull> {
export async function updateAlerta(pool: Pool, id: number, data: AlertaUpdate): Promise<AlertaFull> {
const sets: string[] = [];
const params: any[] = [];
let paramIndex = 1;
@@ -74,35 +74,35 @@ export async function updateAlerta(schema: string, id: number, data: AlertaUpdat
params.push(id);
const [alerta] = await prisma.$queryRawUnsafe<AlertaFull[]>(`
UPDATE "${schema}".alertas
const { rows } = await pool.query(`
UPDATE alertas
SET ${sets.join(', ')}
WHERE id = $${paramIndex}
RETURNING id, tipo, titulo, mensaje, prioridad,
fecha_vencimiento as "fechaVencimiento",
leida, resuelta, created_at as "createdAt"
`, ...params);
`, params);
return alerta;
return rows[0];
}
export async function deleteAlerta(schema: string, id: number): Promise<void> {
await prisma.$queryRawUnsafe(`DELETE FROM "${schema}".alertas WHERE id = $1`, id);
export async function deleteAlerta(pool: Pool, id: number): Promise<void> {
await pool.query(`DELETE FROM alertas WHERE id = $1`, [id]);
}
export async function getStats(schema: string): Promise<AlertasStats> {
const [stats] = await prisma.$queryRawUnsafe<AlertasStats[]>(`
export async function getStats(pool: Pool): Promise<AlertasStats> {
const { rows: [stats] } = await pool.query(`
SELECT
COUNT(*)::int as total,
COUNT(CASE WHEN leida = false THEN 1 END)::int as "noLeidas",
COUNT(CASE WHEN prioridad = 'alta' AND resuelta = false THEN 1 END)::int as alta,
COUNT(CASE WHEN prioridad = 'media' AND resuelta = false THEN 1 END)::int as media,
COUNT(CASE WHEN prioridad = 'baja' AND resuelta = false THEN 1 END)::int as baja
FROM "${schema}".alertas
FROM alertas
`);
return stats;
}
export async function markAllAsRead(schema: string): Promise<void> {
await prisma.$queryRawUnsafe(`UPDATE "${schema}".alertas SET leida = true WHERE leida = false`);
export async function markAllAsRead(pool: Pool): Promise<void> {
await pool.query(`UPDATE alertas SET leida = true WHERE leida = false`);
}

View File

@@ -1,8 +1,8 @@
import { prisma } from '../config/database.js';
import type { Pool } from 'pg';
import type { EventoFiscal, EventoCreate, EventoUpdate } from '@horux/shared';
export async function getEventos(
schema: string,
pool: Pool,
año: number,
mes?: number
): Promise<EventoFiscal[]> {
@@ -14,49 +14,49 @@ export async function getEventos(
params.push(mes);
}
const eventos = await prisma.$queryRawUnsafe<EventoFiscal[]>(`
const { rows } = await pool.query(`
SELECT id, titulo, descripcion, tipo,
fecha_limite as "fechaLimite",
recurrencia, completado, notas,
created_at as "createdAt"
FROM "${schema}".calendario_fiscal
FROM calendario_fiscal
${whereClause}
ORDER BY fecha_limite ASC
`, ...params);
`, params);
return eventos;
return rows;
}
export async function getProximosEventos(schema: string, dias = 30): Promise<EventoFiscal[]> {
const eventos = await prisma.$queryRawUnsafe<EventoFiscal[]>(`
export async function getProximosEventos(pool: Pool, dias = 30): Promise<EventoFiscal[]> {
const { rows } = await pool.query(`
SELECT id, titulo, descripcion, tipo,
fecha_limite as "fechaLimite",
recurrencia, completado, notas,
created_at as "createdAt"
FROM "${schema}".calendario_fiscal
FROM calendario_fiscal
WHERE completado = false
AND fecha_limite BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '${dias} days'
AND fecha_limite BETWEEN CURRENT_DATE AND CURRENT_DATE + $1 * INTERVAL '1 day'
ORDER BY fecha_limite ASC
`);
`, [dias]);
return eventos;
return rows;
}
export async function createEvento(schema: string, data: EventoCreate): Promise<EventoFiscal> {
const [evento] = await prisma.$queryRawUnsafe<EventoFiscal[]>(`
INSERT INTO "${schema}".calendario_fiscal
export async function createEvento(pool: Pool, data: EventoCreate): Promise<EventoFiscal> {
const { rows } = await pool.query(`
INSERT INTO calendario_fiscal
(titulo, descripcion, tipo, fecha_limite, recurrencia, notas)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, titulo, descripcion, tipo,
fecha_limite as "fechaLimite",
recurrencia, completado, notas,
created_at as "createdAt"
`, data.titulo, data.descripcion, data.tipo, data.fechaLimite, data.recurrencia, data.notas || null);
`, [data.titulo, data.descripcion, data.tipo, data.fechaLimite, data.recurrencia, data.notas || null]);
return evento;
return rows[0];
}
export async function updateEvento(schema: string, id: number, data: EventoUpdate): Promise<EventoFiscal> {
export async function updateEvento(pool: Pool, id: number, data: EventoUpdate): Promise<EventoFiscal> {
const sets: string[] = [];
const params: any[] = [];
let paramIndex = 1;
@@ -84,19 +84,19 @@ export async function updateEvento(schema: string, id: number, data: EventoUpdat
params.push(id);
const [evento] = await prisma.$queryRawUnsafe<EventoFiscal[]>(`
UPDATE "${schema}".calendario_fiscal
const { rows } = await pool.query(`
UPDATE calendario_fiscal
SET ${sets.join(', ')}
WHERE id = $${paramIndex}
RETURNING id, titulo, descripcion, tipo,
fecha_limite as "fechaLimite",
recurrencia, completado, notas,
created_at as "createdAt"
`, ...params);
`, params);
return evento;
return rows[0];
}
export async function deleteEvento(schema: string, id: number): Promise<void> {
await prisma.$queryRawUnsafe(`DELETE FROM "${schema}".calendario_fiscal WHERE id = $1`, id);
export async function deleteEvento(pool: Pool, id: number): Promise<void> {
await pool.query(`DELETE FROM calendario_fiscal WHERE id = $1`, [id]);
}

View File

@@ -1,7 +1,7 @@
import { prisma } from '../config/database.js';
import type { Pool } from 'pg';
import type { Cfdi, CfdiFilters, CfdiListResponse } from '@horux/shared';
export async function getCfdis(schema: string, filters: CfdiFilters): Promise<CfdiListResponse> {
export async function getCfdis(pool: Pool, filters: CfdiFilters): Promise<CfdiListResponse> {
const page = filters.page || 1;
const limit = filters.limit || 20;
const offset = (page - 1) * limit;
@@ -50,9 +50,8 @@ export async function getCfdis(schema: string, filters: CfdiFilters): Promise<Cf
params.push(`%${filters.search}%`);
}
// Combinar COUNT con la query principal usando window function
params.push(limit, offset);
const dataWithCount = await prisma.$queryRawUnsafe<(Cfdi & { total_count: number })[]>(`
const { rows: dataWithCount } = await pool.query(`
SELECT
id, uuid_fiscal as "uuidFiscal", tipo, serie, folio,
fecha_emision as "fechaEmision", fecha_timbrado as "fechaTimbrado",
@@ -65,14 +64,14 @@ export async function getCfdis(schema: string, filters: CfdiFilters): Promise<Cf
estado, xml_url as "xmlUrl", pdf_url as "pdfUrl",
created_at as "createdAt",
COUNT(*) OVER() as total_count
FROM "${schema}".cfdis
FROM cfdis
${whereClause}
ORDER BY fecha_emision DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex}
`, ...params);
`, params);
const total = Number(dataWithCount[0]?.total_count || 0);
const data = dataWithCount.map(({ total_count, ...cfdi }) => cfdi) as Cfdi[];
const data = dataWithCount.map(({ total_count, ...cfdi }: any) => cfdi) as Cfdi[];
return {
data,
@@ -83,8 +82,8 @@ export async function getCfdis(schema: string, filters: CfdiFilters): Promise<Cf
};
}
export async function getCfdiById(schema: string, id: string): Promise<Cfdi | null> {
const result = await prisma.$queryRawUnsafe<Cfdi[]>(`
export async function getCfdiById(pool: Pool, id: string): Promise<Cfdi | null> {
const { rows } = await pool.query(`
SELECT
id, uuid_fiscal as "uuidFiscal", tipo, serie, folio,
fecha_emision as "fechaEmision", fecha_timbrado as "fechaTimbrado",
@@ -97,19 +96,19 @@ export async function getCfdiById(schema: string, id: string): Promise<Cfdi | nu
estado, xml_url as "xmlUrl", pdf_url as "pdfUrl",
xml_original as "xmlOriginal",
created_at as "createdAt"
FROM "${schema}".cfdis
FROM cfdis
WHERE id = $1::uuid
`, id);
`, [id]);
return result[0] || null;
return rows[0] || null;
}
export async function getXmlById(schema: string, id: string): Promise<string | null> {
const result = await prisma.$queryRawUnsafe<[{ xml_original: string | null }]>(`
SELECT xml_original FROM "${schema}".cfdis WHERE id = $1::uuid
`, id);
export async function getXmlById(pool: Pool, id: string): Promise<string | null> {
const { rows } = await pool.query(`
SELECT xml_original FROM cfdis WHERE id = $1::uuid
`, [id]);
return result[0]?.xml_original || null;
return rows[0]?.xml_original || null;
}
export interface CreateCfdiData {
@@ -139,18 +138,15 @@ export interface CreateCfdiData {
pdfUrl?: string;
}
export async function createCfdi(schema: string, data: CreateCfdiData): Promise<Cfdi> {
// Validate required fields
export async function createCfdi(pool: Pool, data: CreateCfdiData): Promise<Cfdi> {
if (!data.uuidFiscal) throw new Error('UUID Fiscal es requerido');
if (!data.fechaEmision) throw new Error('Fecha de emisión es requerida');
if (!data.rfcEmisor) throw new Error('RFC Emisor es requerido');
if (!data.rfcReceptor) throw new Error('RFC Receptor es requerido');
// Parse dates safely - handle YYYY-MM-DD format explicitly
let fechaEmision: Date;
let fechaTimbrado: Date;
// If date is in YYYY-MM-DD format, add time to avoid timezone issues
const dateStr = typeof data.fechaEmision === 'string' && data.fechaEmision.match(/^\d{4}-\d{2}-\d{2}$/)
? `${data.fechaEmision}T12:00:00`
: data.fechaEmision;
@@ -173,8 +169,8 @@ export async function createCfdi(schema: string, data: CreateCfdiData): Promise<
throw new Error(`Fecha de timbrado inválida: ${data.fechaTimbrado}`);
}
const result = await prisma.$queryRawUnsafe<Cfdi[]>(`
INSERT INTO "${schema}".cfdis (
const { rows } = await pool.query(`
INSERT INTO cfdis (
uuid_fiscal, tipo, serie, folio, fecha_emision, fecha_timbrado,
rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor,
subtotal, descuento, iva, isr_retenido, iva_retenido, total,
@@ -191,7 +187,7 @@ export async function createCfdi(schema: string, data: CreateCfdiData): Promise<
forma_pago as "formaPago", uso_cfdi as "usoCfdi",
estado, xml_url as "xmlUrl", pdf_url as "pdfUrl",
created_at as "createdAt"
`,
`, [
data.uuidFiscal,
data.tipo || 'ingreso',
data.serie || null,
@@ -216,9 +212,9 @@ export async function createCfdi(schema: string, data: CreateCfdiData): Promise<
data.estado || 'vigente',
data.xmlUrl || null,
data.pdfUrl || null
);
]);
return result[0];
return rows[0];
}
export interface BatchInsertResult {
@@ -228,14 +224,12 @@ export interface BatchInsertResult {
errorMessages: string[];
}
// Optimized batch insert using multi-row INSERT
export async function createManyCfdis(schema: string, cfdis: CreateCfdiData[]): Promise<number> {
const result = await createManyCfdisBatch(schema, cfdis);
export async function createManyCfdis(pool: Pool, cfdis: CreateCfdiData[]): Promise<number> {
const result = await createManyCfdisBatch(pool, cfdis);
return result.inserted;
}
// New optimized batch insert with detailed results
export async function createManyCfdisBatch(schema: string, cfdis: CreateCfdiData[]): Promise<BatchInsertResult> {
export async function createManyCfdisBatch(pool: Pool, cfdis: CreateCfdiData[]): Promise<BatchInsertResult> {
const result: BatchInsertResult = {
inserted: 0,
duplicates: 0,
@@ -245,19 +239,17 @@ export async function createManyCfdisBatch(schema: string, cfdis: CreateCfdiData
if (cfdis.length === 0) return result;
// Process in batches of 500 for optimal performance
const BATCH_SIZE = 500;
for (let batchStart = 0; batchStart < cfdis.length; batchStart += BATCH_SIZE) {
const batch = cfdis.slice(batchStart, batchStart + BATCH_SIZE);
try {
const batchResult = await insertBatch(schema, batch);
const batchResult = await insertBatch(pool, batch);
result.inserted += batchResult.inserted;
result.duplicates += batchResult.duplicates;
} catch (error: any) {
// If batch fails, try individual inserts for this batch
const individualResult = await insertIndividually(schema, batch);
const individualResult = await insertIndividually(pool, batch);
result.inserted += individualResult.inserted;
result.duplicates += individualResult.duplicates;
result.errors += individualResult.errors;
@@ -268,17 +260,14 @@ export async function createManyCfdisBatch(schema: string, cfdis: CreateCfdiData
return result;
}
// Insert a batch using multi-row INSERT with ON CONFLICT
async function insertBatch(schema: string, cfdis: CreateCfdiData[]): Promise<{ inserted: number; duplicates: number }> {
async function insertBatch(pool: Pool, cfdis: CreateCfdiData[]): Promise<{ inserted: number; duplicates: number }> {
if (cfdis.length === 0) return { inserted: 0, duplicates: 0 };
// Build the VALUES part of the query
const values: any[] = [];
const valuePlaceholders: string[] = [];
let paramIndex = 1;
for (const cfdi of cfdis) {
// Parse dates
const fechaEmision = parseDate(cfdi.fechaEmision);
const fechaTimbrado = cfdi.fechaTimbrado ? parseDate(cfdi.fechaTimbrado) : fechaEmision;
@@ -322,9 +311,8 @@ async function insertBatch(schema: string, cfdis: CreateCfdiData[]): Promise<{ i
return { inserted: 0, duplicates: 0 };
}
// Use ON CONFLICT to handle duplicates gracefully
const query = `
INSERT INTO "${schema}".cfdis (
INSERT INTO cfdis (
uuid_fiscal, tipo, serie, folio, fecha_emision, fecha_timbrado,
rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor,
subtotal, descuento, iva, isr_retenido, iva_retenido, total,
@@ -333,15 +321,12 @@ async function insertBatch(schema: string, cfdis: CreateCfdiData[]): Promise<{ i
ON CONFLICT (uuid_fiscal) DO NOTHING
`;
await prisma.$executeRawUnsafe(query, ...values);
await pool.query(query, values);
// We can't know exactly how many were inserted vs duplicates with DO NOTHING
// Return optimistic count, duplicates will be 0 (they're silently skipped)
return { inserted: valuePlaceholders.length, duplicates: 0 };
}
// Fallback: insert individually when batch fails
async function insertIndividually(schema: string, cfdis: CreateCfdiData[]): Promise<BatchInsertResult> {
async function insertIndividually(pool: Pool, cfdis: CreateCfdiData[]): Promise<BatchInsertResult> {
const result: BatchInsertResult = {
inserted: 0,
duplicates: 0,
@@ -351,7 +336,7 @@ async function insertIndividually(schema: string, cfdis: CreateCfdiData[]): Prom
for (const cfdi of cfdis) {
try {
await createCfdi(schema, cfdi);
await createCfdi(pool, cfdi);
result.inserted++;
} catch (error: any) {
const errorMsg = error.message || 'Error desconocido';
@@ -369,11 +354,9 @@ async function insertIndividually(schema: string, cfdis: CreateCfdiData[]): Prom
return result;
}
// Helper to parse dates safely
function parseDate(dateStr: string): Date | null {
if (!dateStr) return null;
// If date is in YYYY-MM-DD format, add time to avoid timezone issues
const normalized = dateStr.match(/^\d{4}-\d{2}-\d{2}$/)
? `${dateStr}T12:00:00`
: dateStr;
@@ -382,41 +365,34 @@ function parseDate(dateStr: string): Date | null {
return isNaN(date.getTime()) ? null : date;
}
export async function deleteCfdi(schema: string, id: string): Promise<void> {
await prisma.$queryRawUnsafe(`DELETE FROM "${schema}".cfdis WHERE id = $1`, id);
export async function deleteCfdi(pool: Pool, id: string): Promise<void> {
await pool.query(`DELETE FROM cfdis WHERE id = $1`, [id]);
}
export async function getEmisores(schema: string, search: string, limit: number = 10): Promise<{ rfc: string; nombre: string }[]> {
const result = await prisma.$queryRawUnsafe<{ rfc: string; nombre: string }[]>(`
export async function getEmisores(pool: Pool, search: string, limit: number = 10): Promise<{ rfc: string; nombre: string }[]> {
const { rows } = await pool.query(`
SELECT DISTINCT rfc_emisor as rfc, nombre_emisor as nombre
FROM "${schema}".cfdis
FROM cfdis
WHERE rfc_emisor ILIKE $1 OR nombre_emisor ILIKE $1
ORDER BY nombre_emisor
LIMIT $2
`, `%${search}%`, limit);
return result;
`, [`%${search}%`, limit]);
return rows;
}
export async function getReceptores(schema: string, search: string, limit: number = 10): Promise<{ rfc: string; nombre: string }[]> {
const result = await prisma.$queryRawUnsafe<{ rfc: string; nombre: string }[]>(`
export async function getReceptores(pool: Pool, search: string, limit: number = 10): Promise<{ rfc: string; nombre: string }[]> {
const { rows } = await pool.query(`
SELECT DISTINCT rfc_receptor as rfc, nombre_receptor as nombre
FROM "${schema}".cfdis
FROM cfdis
WHERE rfc_receptor ILIKE $1 OR nombre_receptor ILIKE $1
ORDER BY nombre_receptor
LIMIT $2
`, `%${search}%`, limit);
return result;
`, [`%${search}%`, limit]);
return rows;
}
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;
}]>(`
export async function getResumenCfdis(pool: Pool, año: number, mes: number) {
const { rows } = await pool.query(`
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,
@@ -424,13 +400,13 @@ export async function getResumenCfdis(schema: string, año: number, mes: number)
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
FROM cfdis
WHERE estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) = $2
`, año, mes);
`, [año, mes]);
const r = result[0];
const r = rows[0];
return {
totalIngresos: Number(r?.total_ingresos || 0),
totalEgresos: Number(r?.total_egresos || 0),

View File

@@ -1,44 +1,44 @@
import { prisma } from '../config/database.js';
import type { Pool } from 'pg';
import type { KpiData, IngresosEgresosData, ResumenFiscal, Alerta } from '@horux/shared';
export async function getKpis(schema: string, año: number, mes: number): Promise<KpiData> {
const [ingresos] = await prisma.$queryRawUnsafe<[{ total: number }]>(`
export async function getKpis(pool: Pool, año: number, mes: number): Promise<KpiData> {
const { rows: [ingresos] } = await pool.query(`
SELECT COALESCE(SUM(total), 0) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'ingreso'
AND estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) = $2
`, año, mes);
`, [año, mes]);
const [egresos] = await prisma.$queryRawUnsafe<[{ total: number }]>(`
const { rows: [egresos] } = await pool.query(`
SELECT COALESCE(SUM(total), 0) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'egreso'
AND estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) = $2
`, año, mes);
`, [año, mes]);
const [ivaData] = await prisma.$queryRawUnsafe<[{ trasladado: number; acreditable: number }]>(`
const { rows: [ivaData] } = await pool.query(`
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
FROM "${schema}".cfdis
FROM cfdis
WHERE estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) = $2
`, año, mes);
`, [año, mes]);
const [counts] = await prisma.$queryRawUnsafe<[{ emitidos: number; recibidos: number }]>(`
const { rows: [counts] } = await pool.query(`
SELECT
COUNT(CASE WHEN tipo = 'ingreso' THEN 1 END) as emitidos,
COUNT(CASE WHEN tipo = 'egreso' THEN 1 END) as recibidos
FROM "${schema}".cfdis
FROM cfdis
WHERE estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) = $2
`, año, mes);
`, [año, mes]);
const ingresosVal = Number(ingresos?.total || 0);
const egresosVal = Number(egresos?.total || 0);
@@ -57,23 +57,23 @@ export async function getKpis(schema: string, año: number, mes: number): Promis
};
}
export async function getIngresosEgresos(schema: string, año: number): Promise<IngresosEgresosData[]> {
const data = await prisma.$queryRawUnsafe<{ mes: number; ingresos: number; egresos: number }[]>(`
export async function getIngresosEgresos(pool: Pool, año: number): Promise<IngresosEgresosData[]> {
const { rows: data } = await pool.query(`
SELECT
EXTRACT(MONTH FROM fecha_emision)::int as mes,
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN total ELSE 0 END), 0) as ingresos,
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN total ELSE 0 END), 0) as egresos
FROM "${schema}".cfdis
FROM cfdis
WHERE estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
GROUP BY EXTRACT(MONTH FROM fecha_emision)
ORDER BY mes
`, año);
`, [año]);
const meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];
return meses.map((mes, index) => {
const found = data.find(d => d.mes === index + 1);
const found = data.find((d: any) => d.mes === index + 1);
return {
mes,
ingresos: Number(found?.ingresos || 0),
@@ -82,16 +82,17 @@ export async function getIngresosEgresos(schema: string, año: number): Promise<
});
}
export async function getResumenFiscal(schema: string, año: number, mes: number): Promise<ResumenFiscal> {
const [ivaResult] = await prisma.$queryRawUnsafe<[{ resultado: number; acumulado: number }]>(`
SELECT resultado, acumulado FROM "${schema}".iva_mensual
export async function getResumenFiscal(pool: Pool, año: number, mes: number): Promise<ResumenFiscal> {
const { rows: ivaRows } = await pool.query(`
SELECT resultado, acumulado FROM iva_mensual
WHERE año = $1 AND mes = $2
`, año, mes) || [{ resultado: 0, acumulado: 0 }];
`, [año, mes]);
const ivaResult = ivaRows[0] || { resultado: 0, acumulado: 0 };
const [pendientes] = await prisma.$queryRawUnsafe<[{ count: number }]>(`
SELECT COUNT(*) as count FROM "${schema}".iva_mensual
const { rows: [pendientes] } = await pool.query(`
SELECT COUNT(*) as count FROM iva_mensual
WHERE año = $1 AND estado = 'pendiente'
`, año);
`, [año]);
const resultado = Number(ivaResult?.resultado || 0);
const acumulado = Number(ivaResult?.acumulado || 0);
@@ -108,19 +109,19 @@ export async function getResumenFiscal(schema: string, año: number, mes: number
};
}
export async function getAlertas(schema: string, limit = 5): Promise<Alerta[]> {
const alertas = await prisma.$queryRawUnsafe<Alerta[]>(`
export async function getAlertas(pool: Pool, limit = 5): Promise<Alerta[]> {
const { rows } = await pool.query(`
SELECT id, tipo, titulo, mensaje, prioridad,
fecha_vencimiento as "fechaVencimiento",
leida, resuelta,
created_at as "createdAt"
FROM "${schema}".alertas
FROM alertas
WHERE resuelta = false
ORDER BY
CASE prioridad WHEN 'alta' THEN 1 WHEN 'media' THEN 2 ELSE 3 END,
created_at DESC
LIMIT $1
`, limit);
`, [limit]);
return alertas;
return rows;
}

View File

@@ -1,8 +1,8 @@
import ExcelJS from 'exceljs';
import { prisma } from '../config/database.js';
import type { Pool } from 'pg';
export async function exportCfdisToExcel(
schema: string,
pool: Pool,
filters: { tipo?: string; estado?: string; fechaInicio?: string; fechaFin?: string }
): Promise<Buffer> {
let whereClause = 'WHERE 1=1';
@@ -26,15 +26,15 @@ export async function exportCfdisToExcel(
params.push(filters.fechaFin);
}
const cfdis = await prisma.$queryRawUnsafe<any[]>(`
const { rows: cfdis } = await pool.query(`
SELECT uuid_fiscal, tipo, serie, folio, fecha_emision, fecha_timbrado,
rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor,
subtotal, descuento, iva, isr_retenido, iva_retenido, total,
moneda, metodo_pago, forma_pago, uso_cfdi, estado
FROM "${schema}".cfdis
FROM cfdis
${whereClause}
ORDER BY fecha_emision DESC
`, ...params);
`, params);
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('CFDIs');
@@ -63,7 +63,7 @@ export async function exportCfdisToExcel(
};
sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } };
cfdis.forEach(cfdi => {
cfdis.forEach((cfdi: any) => {
sheet.addRow({
...cfdi,
fecha_emision: new Date(cfdi.fecha_emision).toLocaleDateString('es-MX'),
@@ -78,7 +78,7 @@ export async function exportCfdisToExcel(
}
export async function exportReporteToExcel(
schema: string,
pool: Pool,
tipo: 'estado-resultados' | 'flujo-efectivo',
fechaInicio: string,
fechaFin: string
@@ -87,13 +87,13 @@ export async function exportReporteToExcel(
const sheet = workbook.addWorksheet(tipo === 'estado-resultados' ? 'Estado de Resultados' : 'Flujo de Efectivo');
if (tipo === 'estado-resultados') {
const [totales] = await prisma.$queryRawUnsafe<[{ ingresos: number; egresos: number }]>(`
const { rows: [totales] } = await pool.query(`
SELECT
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN subtotal ELSE 0 END), 0) as ingresos,
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN subtotal ELSE 0 END), 0) as egresos
FROM "${schema}".cfdis
FROM cfdis
WHERE estado = 'vigente' AND fecha_emision BETWEEN $1 AND $2
`, fechaInicio, fechaFin);
`, [fechaInicio, fechaFin]);
sheet.columns = [
{ header: 'Concepto', key: 'concepto', width: 40 },

View File

@@ -1,8 +1,8 @@
import { prisma } from '../config/database.js';
import type { Pool } from 'pg';
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[]>(`
export async function getIvaMensual(pool: Pool, año: number): Promise<IvaMensual[]> {
const { rows: data } = await pool.query(`
SELECT
id, año, mes,
iva_trasladado as "ivaTrasladado",
@@ -10,12 +10,12 @@ export async function getIvaMensual(schema: string, año: number): Promise<IvaMe
COALESCE(iva_retenido, 0) as "ivaRetenido",
resultado, acumulado, estado,
fecha_declaracion as "fechaDeclaracion"
FROM "${schema}".iva_mensual
FROM iva_mensual
WHERE año = $1
ORDER BY mes
`, año);
`, [año]);
return data.map(row => ({
return data.map((row: any) => ({
...row,
ivaTrasladado: Number(row.ivaTrasladado),
ivaAcreditable: Number(row.ivaAcreditable),
@@ -25,19 +25,18 @@ export async function getIvaMensual(schema: string, año: number): Promise<IvaMe
}));
}
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);
export async function getResumenIva(pool: Pool, año: number, mes: number): Promise<ResumenIva> {
const { rows: existing } = await pool.query(`
SELECT * FROM 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 }]>(`
const { rows: [acumuladoResult] } = await pool.query(`
SELECT COALESCE(SUM(resultado), 0) as total
FROM "${schema}".iva_mensual
FROM iva_mensual
WHERE año = $1 AND mes <= $2
`, año, mes);
`, [año, mes]);
return {
trasladado: Number(record.iva_trasladado || 0),
@@ -48,21 +47,16 @@ export async function getResumenIva(schema: string, año: number, mes: number):
};
}
// Calculate from CFDIs if no iva_mensual record
const [calcResult] = await prisma.$queryRawUnsafe<[{
trasladado: number;
acreditable: number;
retenido: number;
}]>(`
const { rows: [calcResult] } = await pool.query(`
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
FROM cfdis
WHERE estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) = $2
`, año, mes);
`, [año, mes]);
const trasladado = Number(calcResult?.trasladado || 0);
const acreditable = Number(calcResult?.acreditable || 0);
@@ -78,10 +72,9 @@ export async function getResumenIva(schema: string, año: number, mes: number):
};
}
export async function getIsrMensual(schema: string, año: number): Promise<IsrMensual[]> {
// Check if isr_mensual table exists
export async function getIsrMensual(pool: Pool, año: number): Promise<IsrMensual[]> {
try {
const data = await prisma.$queryRawUnsafe<IsrMensual[]>(`
const { rows: data } = await pool.query(`
SELECT
id, año, mes,
ingresos_acumulados as "ingresosAcumulados",
@@ -92,12 +85,12 @@ export async function getIsrMensual(schema: string, año: number): Promise<IsrMe
isr_a_pagar as "isrAPagar",
estado,
fecha_declaracion as "fechaDeclaracion"
FROM "${schema}".isr_mensual
FROM isr_mensual
WHERE año = $1
ORDER BY mes
`, año);
`, [año]);
return data.map(row => ({
return data.map((row: any) => ({
...row,
ingresosAcumulados: Number(row.ingresosAcumulados),
deducciones: Number(row.deducciones),
@@ -107,43 +100,40 @@ export async function getIsrMensual(schema: string, año: number): Promise<IsrMe
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 }]>(`
export async function getResumenIsr(pool: Pool, año: number, mes: number): Promise<ResumenIsr> {
const { rows: [ingresos] } = await pool.query(`
SELECT COALESCE(SUM(total), 0) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'ingreso' AND estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) <= $2
`, año, mes);
`, [año, mes]);
const [egresos] = await prisma.$queryRawUnsafe<[{ total: number }]>(`
const { rows: [egresos] } = await pool.query(`
SELECT COALESCE(SUM(total), 0) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'egreso' AND estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) <= $2
`, año, mes);
`, [año, mes]);
const [retenido] = await prisma.$queryRawUnsafe<[{ total: number }]>(`
const { rows: [retenido] } = await pool.query(`
SELECT COALESCE(SUM(isr_retenido), 0) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE estado = 'vigente'
AND EXTRACT(YEAR FROM fecha_emision) = $1
AND EXTRACT(MONTH FROM fecha_emision) <= $2
`, año, mes);
`, [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 isrCausado = baseGravable * 0.30;
const isrRetenido = Number(retenido?.total || 0);
const isrAPagar = Math.max(0, isrCausado - isrRetenido);

View File

@@ -1,7 +1,6 @@
import { prisma } from '../config/database.js';
import type { Pool } from 'pg';
import type { EstadoResultados, FlujoEfectivo, ComparativoPeriodos, ConcentradoRfc } from '@horux/shared';
// Helper to convert Prisma Decimal/BigInt to number
function toNumber(value: unknown): number {
if (value === null || value === undefined) return 0;
if (typeof value === 'number') return value;
@@ -14,37 +13,37 @@ function toNumber(value: unknown): number {
}
export async function getEstadoResultados(
schema: string,
pool: Pool,
fechaInicio: string,
fechaFin: string
): Promise<EstadoResultados> {
const ingresos = await prisma.$queryRawUnsafe<{ rfc: string; nombre: string; total: unknown }[]>(`
const { rows: ingresos } = await pool.query(`
SELECT rfc_receptor as rfc, nombre_receptor as nombre, SUM(subtotal) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'ingreso' AND estado = 'vigente'
AND fecha_emision BETWEEN $1::date AND $2::date
GROUP BY rfc_receptor, nombre_receptor
ORDER BY total DESC LIMIT 10
`, fechaInicio, fechaFin);
`, [fechaInicio, fechaFin]);
const egresos = await prisma.$queryRawUnsafe<{ rfc: string; nombre: string; total: unknown }[]>(`
const { rows: egresos } = await pool.query(`
SELECT rfc_emisor as rfc, nombre_emisor as nombre, SUM(subtotal) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'egreso' AND estado = 'vigente'
AND fecha_emision BETWEEN $1::date AND $2::date
GROUP BY rfc_emisor, nombre_emisor
ORDER BY total DESC LIMIT 10
`, fechaInicio, fechaFin);
`, [fechaInicio, fechaFin]);
const totalesResult = await prisma.$queryRawUnsafe<{ ingresos: unknown; egresos: unknown; iva: unknown }[]>(`
const { rows: totalesResult } = await pool.query(`
SELECT
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN subtotal ELSE 0 END), 0) as ingresos,
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN subtotal ELSE 0 END), 0) as egresos,
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN iva ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN iva ELSE 0 END), 0) as iva
FROM "${schema}".cfdis
FROM cfdis
WHERE estado = 'vigente' AND fecha_emision BETWEEN $1::date AND $2::date
`, fechaInicio, fechaFin);
`, [fechaInicio, fechaFin]);
const totales = totalesResult[0];
const totalIngresos = toNumber(totales?.ingresos);
@@ -54,8 +53,8 @@ export async function getEstadoResultados(
return {
periodo: { inicio: fechaInicio, fin: fechaFin },
ingresos: ingresos.map(i => ({ concepto: i.nombre, monto: toNumber(i.total) })),
egresos: egresos.map(e => ({ concepto: e.nombre, monto: toNumber(e.total) })),
ingresos: ingresos.map((i: any) => ({ concepto: i.nombre, monto: toNumber(i.total) })),
egresos: egresos.map((e: any) => ({ concepto: e.nombre, monto: toNumber(e.total) })),
totalIngresos,
totalEgresos,
utilidadBruta,
@@ -65,36 +64,36 @@ export async function getEstadoResultados(
}
export async function getFlujoEfectivo(
schema: string,
pool: Pool,
fechaInicio: string,
fechaFin: string
): Promise<FlujoEfectivo> {
const entradas = await prisma.$queryRawUnsafe<{ mes: string; total: unknown }[]>(`
const { rows: entradas } = await pool.query(`
SELECT TO_CHAR(fecha_emision, 'YYYY-MM') as mes, SUM(total) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'ingreso' AND estado = 'vigente'
AND fecha_emision BETWEEN $1::date AND $2::date
GROUP BY TO_CHAR(fecha_emision, 'YYYY-MM')
ORDER BY mes
`, fechaInicio, fechaFin);
`, [fechaInicio, fechaFin]);
const salidas = await prisma.$queryRawUnsafe<{ mes: string; total: unknown }[]>(`
const { rows: salidas } = await pool.query(`
SELECT TO_CHAR(fecha_emision, 'YYYY-MM') as mes, SUM(total) as total
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'egreso' AND estado = 'vigente'
AND fecha_emision BETWEEN $1::date AND $2::date
GROUP BY TO_CHAR(fecha_emision, 'YYYY-MM')
ORDER BY mes
`, fechaInicio, fechaFin);
`, [fechaInicio, fechaFin]);
const totalEntradas = entradas.reduce((sum, e) => sum + toNumber(e.total), 0);
const totalSalidas = salidas.reduce((sum, s) => sum + toNumber(s.total), 0);
const totalEntradas = entradas.reduce((sum: number, e: any) => sum + toNumber(e.total), 0);
const totalSalidas = salidas.reduce((sum: number, s: any) => sum + toNumber(s.total), 0);
return {
periodo: { inicio: fechaInicio, fin: fechaFin },
saldoInicial: 0,
entradas: entradas.map(e => ({ concepto: e.mes, monto: toNumber(e.total) })),
salidas: salidas.map(s => ({ concepto: s.mes, monto: toNumber(s.total) })),
entradas: entradas.map((e: any) => ({ concepto: e.mes, monto: toNumber(e.total) })),
salidas: salidas.map((s: any) => ({ concepto: s.mes, monto: toNumber(s.total) })),
totalEntradas,
totalSalidas,
flujoNeto: totalEntradas - totalSalidas,
@@ -103,36 +102,36 @@ export async function getFlujoEfectivo(
}
export async function getComparativo(
schema: string,
pool: Pool,
año: number
): Promise<ComparativoPeriodos> {
const actual = await prisma.$queryRawUnsafe<{ mes: number; ingresos: unknown; egresos: unknown }[]>(`
const { rows: actual } = await pool.query(`
SELECT EXTRACT(MONTH FROM fecha_emision)::int as mes,
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN total ELSE 0 END), 0) as ingresos,
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN total ELSE 0 END), 0) as egresos
FROM "${schema}".cfdis
FROM cfdis
WHERE estado = 'vigente' AND EXTRACT(YEAR FROM fecha_emision) = $1
GROUP BY mes ORDER BY mes
`, año);
`, [año]);
const anterior = await prisma.$queryRawUnsafe<{ mes: number; ingresos: unknown; egresos: unknown }[]>(`
const { rows: anterior } = await pool.query(`
SELECT EXTRACT(MONTH FROM fecha_emision)::int as mes,
COALESCE(SUM(CASE WHEN tipo = 'ingreso' THEN total ELSE 0 END), 0) as ingresos,
COALESCE(SUM(CASE WHEN tipo = 'egreso' THEN total ELSE 0 END), 0) as egresos
FROM "${schema}".cfdis
FROM cfdis
WHERE estado = 'vigente' AND EXTRACT(YEAR FROM fecha_emision) = $1
GROUP BY mes ORDER BY mes
`, año - 1);
`, [año - 1]);
const meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];
const ingresos = meses.map((_, i) => toNumber(actual.find(a => a.mes === i + 1)?.ingresos));
const egresos = meses.map((_, i) => toNumber(actual.find(a => a.mes === i + 1)?.egresos));
const ingresos = meses.map((_, i) => toNumber(actual.find((a: any) => a.mes === i + 1)?.ingresos));
const egresos = meses.map((_, i) => toNumber(actual.find((a: any) => a.mes === i + 1)?.egresos));
const utilidad = ingresos.map((ing, i) => ing - egresos[i]);
const totalActualIng = ingresos.reduce((a, b) => a + b, 0);
const totalAnteriorIng = anterior.reduce((a, b) => a + toNumber(b.ingresos), 0);
const totalAnteriorIng = anterior.reduce((a: number, b: any) => a + toNumber(b.ingresos), 0);
const totalActualEgr = egresos.reduce((a, b) => a + b, 0);
const totalAnteriorEgr = anterior.reduce((a, b) => a + toNumber(b.egresos), 0);
const totalAnteriorEgr = anterior.reduce((a: number, b: any) => a + toNumber(b.egresos), 0);
return {
periodos: meses,
@@ -146,25 +145,25 @@ export async function getComparativo(
}
export async function getConcentradoRfc(
schema: string,
pool: Pool,
fechaInicio: string,
fechaFin: string,
tipo: 'cliente' | 'proveedor'
): Promise<ConcentradoRfc[]> {
if (tipo === 'cliente') {
const data = await prisma.$queryRawUnsafe<{ rfc: string; nombre: string; tipo: string; totalFacturado: unknown; totalIva: unknown; cantidadCfdis: number }[]>(`
const { rows: data } = await pool.query(`
SELECT rfc_receptor as rfc, nombre_receptor as nombre,
'cliente' as tipo,
SUM(total) as "totalFacturado",
SUM(iva) as "totalIva",
COUNT(*)::int as "cantidadCfdis"
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'ingreso' AND estado = 'vigente'
AND fecha_emision BETWEEN $1::date AND $2::date
GROUP BY rfc_receptor, nombre_receptor
ORDER BY "totalFacturado" DESC
`, fechaInicio, fechaFin);
return data.map(d => ({
`, [fechaInicio, fechaFin]);
return data.map((d: any) => ({
rfc: d.rfc,
nombre: d.nombre,
tipo: 'cliente' as const,
@@ -173,19 +172,19 @@ export async function getConcentradoRfc(
cantidadCfdis: d.cantidadCfdis
}));
} else {
const data = await prisma.$queryRawUnsafe<{ rfc: string; nombre: string; tipo: string; totalFacturado: unknown; totalIva: unknown; cantidadCfdis: number }[]>(`
const { rows: data } = await pool.query(`
SELECT rfc_emisor as rfc, nombre_emisor as nombre,
'proveedor' as tipo,
SUM(total) as "totalFacturado",
SUM(iva) as "totalIva",
COUNT(*)::int as "cantidadCfdis"
FROM "${schema}".cfdis
FROM cfdis
WHERE tipo = 'egreso' AND estado = 'vigente'
AND fecha_emision BETWEEN $1::date AND $2::date
GROUP BY rfc_emisor, nombre_emisor
ORDER BY "totalFacturado" DESC
`, fechaInicio, fechaFin);
return data.map(d => ({
`, [fechaInicio, fechaFin]);
return data.map((d: any) => ({
rfc: d.rfc,
nombre: d.nombre,
tipo: 'proveedor' as const,

View File

@@ -1,4 +1,4 @@
import { prisma } from '../../config/database.js';
import { prisma, tenantDb } from '../../config/database.js';
import { getDecryptedFiel } from '../fiel.service.js';
import {
createSatService,
@@ -10,6 +10,7 @@ import {
import { processPackage, type CfdiParsed } from './sat-parser.service.js';
import type { SatSyncJob, CfdiSyncType, SatSyncType } from '@horux/shared';
import type { Service } from '@nodecfdi/sat-ws-descarga-masiva';
import type { Pool } from 'pg';
const POLL_INTERVAL_MS = 30000; // 30 segundos
const MAX_POLL_ATTEMPTS = 60; // 30 minutos máximo
@@ -20,7 +21,7 @@ interface SyncContext {
service: Service;
rfc: string;
tenantId: string;
databaseName: string;
pool: Pool;
}
/**
@@ -54,7 +55,7 @@ async function updateJobProgress(
* Guarda los CFDIs en la base de datos del tenant
*/
async function saveCfdis(
databaseName: string,
pool: Pool,
cfdis: CfdiParsed[],
jobId: string
): Promise<{ inserted: number; updated: number }> {
@@ -63,16 +64,14 @@ async function saveCfdis(
for (const cfdi of cfdis) {
try {
// Usar raw query para el esquema del tenant
const existing = await prisma.$queryRawUnsafe<{ id: string }[]>(
`SELECT id FROM "${databaseName}".cfdis WHERE uuid_fiscal = $1`,
cfdi.uuidFiscal
const { rows: existing } = await pool.query(
`SELECT id FROM cfdis WHERE uuid_fiscal = $1`,
[cfdi.uuidFiscal]
);
if (existing.length > 0) {
// Actualizar CFDI existente
await prisma.$executeRawUnsafe(
`UPDATE "${databaseName}".cfdis SET
await pool.query(
`UPDATE cfdis SET
tipo = $2,
serie = $3,
folio = $4,
@@ -99,36 +98,37 @@ async function saveCfdis(
sat_sync_job_id = $24::uuid,
updated_at = NOW()
WHERE uuid_fiscal = $1`,
cfdi.uuidFiscal,
cfdi.tipo,
cfdi.serie,
cfdi.folio,
cfdi.fechaEmision,
cfdi.fechaTimbrado,
cfdi.rfcEmisor,
cfdi.nombreEmisor,
cfdi.rfcReceptor,
cfdi.nombreReceptor,
cfdi.subtotal,
cfdi.descuento,
cfdi.iva,
cfdi.isrRetenido,
cfdi.ivaRetenido,
cfdi.total,
cfdi.moneda,
cfdi.tipoCambio,
cfdi.metodoPago,
cfdi.formaPago,
cfdi.usoCfdi,
cfdi.estado,
cfdi.xmlOriginal,
jobId
[
cfdi.uuidFiscal,
cfdi.tipo,
cfdi.serie,
cfdi.folio,
cfdi.fechaEmision,
cfdi.fechaTimbrado,
cfdi.rfcEmisor,
cfdi.nombreEmisor,
cfdi.rfcReceptor,
cfdi.nombreReceptor,
cfdi.subtotal,
cfdi.descuento,
cfdi.iva,
cfdi.isrRetenido,
cfdi.ivaRetenido,
cfdi.total,
cfdi.moneda,
cfdi.tipoCambio,
cfdi.metodoPago,
cfdi.formaPago,
cfdi.usoCfdi,
cfdi.estado,
cfdi.xmlOriginal,
jobId
]
);
updated++;
} else {
// Insertar nuevo CFDI
await prisma.$executeRawUnsafe(
`INSERT INTO "${databaseName}".cfdis (
await pool.query(
`INSERT INTO cfdis (
id, uuid_fiscal, tipo, serie, folio, fecha_emision, fecha_timbrado,
rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor,
subtotal, descuento, iva, isr_retenido, iva_retenido, total,
@@ -139,30 +139,32 @@ async function saveCfdis(
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22,
$23, 'sat', $24::uuid, NOW(), NOW()
)`,
cfdi.uuidFiscal,
cfdi.tipo,
cfdi.serie,
cfdi.folio,
cfdi.fechaEmision,
cfdi.fechaTimbrado,
cfdi.rfcEmisor,
cfdi.nombreEmisor,
cfdi.rfcReceptor,
cfdi.nombreReceptor,
cfdi.subtotal,
cfdi.descuento,
cfdi.iva,
cfdi.isrRetenido,
cfdi.ivaRetenido,
cfdi.total,
cfdi.moneda,
cfdi.tipoCambio,
cfdi.metodoPago,
cfdi.formaPago,
cfdi.usoCfdi,
cfdi.estado,
cfdi.xmlOriginal,
jobId
[
cfdi.uuidFiscal,
cfdi.tipo,
cfdi.serie,
cfdi.folio,
cfdi.fechaEmision,
cfdi.fechaTimbrado,
cfdi.rfcEmisor,
cfdi.nombreEmisor,
cfdi.rfcReceptor,
cfdi.nombreReceptor,
cfdi.subtotal,
cfdi.descuento,
cfdi.iva,
cfdi.isrRetenido,
cfdi.ivaRetenido,
cfdi.total,
cfdi.moneda,
cfdi.tipoCambio,
cfdi.metodoPago,
cfdi.formaPago,
cfdi.usoCfdi,
cfdi.estado,
cfdi.xmlOriginal,
jobId
]
);
inserted++;
}
@@ -186,11 +188,9 @@ async function processDateRange(
): Promise<{ found: number; downloaded: number; inserted: number; updated: number }> {
console.log(`[SAT] Procesando ${tipoCfdi} desde ${fechaInicio.toISOString()} hasta ${fechaFin.toISOString()}`);
// 1. Solicitar descarga
const queryResult = await querySat(ctx.service, fechaInicio, fechaFin, tipoCfdi);
if (!queryResult.success) {
// Código 5004 = No hay CFDIs en el rango
if (queryResult.statusCode === '5004') {
console.log('[SAT] No se encontraron CFDIs en el rango');
return { found: 0, downloaded: 0, inserted: 0, updated: 0 };
@@ -203,7 +203,6 @@ async function processDateRange(
await updateJobProgress(jobId, { satRequestId: requestId });
// 2. Esperar y verificar solicitud
let verifyResult;
let attempts = 0;
@@ -227,7 +226,6 @@ async function processDateRange(
throw new Error('Timeout esperando respuesta del SAT');
}
// 3. Descargar paquetes
const packageIds = verifyResult.packageIds;
await updateJobProgress(jobId, {
satPackageIds: packageIds,
@@ -249,17 +247,15 @@ async function processDateRange(
continue;
}
// 4. Procesar paquete (el contenido viene en base64)
const cfdis = processPackage(downloadResult.packageContent);
totalDownloaded += cfdis.length;
console.log(`[SAT] Procesando ${cfdis.length} CFDIs del paquete`);
const { inserted, updated } = await saveCfdis(ctx.databaseName, cfdis, jobId);
const { inserted, updated } = await saveCfdis(ctx.pool, cfdis, jobId);
totalInserted += inserted;
totalUpdated += updated;
// Actualizar progreso
const progress = Math.round(((i + 1) / packageIds.length) * 100);
await updateJobProgress(jobId, {
cfdisDownloaded: totalDownloaded,
@@ -287,7 +283,6 @@ async function processInitialSync(
customDateTo?: Date
): Promise<void> {
const ahora = new Date();
// Usar fechas personalizadas si se proporcionan, sino calcular desde YEARS_TO_SYNC
const inicioHistorico = customDateFrom || new Date(ahora.getFullYear() - YEARS_TO_SYNC, ahora.getMonth(), 1);
const fechaFin = customDateTo || ahora;
@@ -296,14 +291,12 @@ async function processInitialSync(
let totalInserted = 0;
let totalUpdated = 0;
// Procesar por meses para evitar límites del SAT
let currentDate = new Date(inicioHistorico);
while (currentDate < fechaFin) {
const monthEnd = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0, 23, 59, 59);
const rangeEnd = monthEnd > fechaFin ? fechaFin : monthEnd;
// Procesar emitidos
try {
const emitidos = await processDateRange(ctx, jobId, currentDate, rangeEnd, 'emitidos');
totalFound += emitidos.found;
@@ -314,7 +307,6 @@ async function processInitialSync(
console.error(`[SAT] Error procesando emitidos ${currentDate.toISOString()}:`, error.message);
}
// Procesar recibidos
try {
const recibidos = await processDateRange(ctx, jobId, currentDate, rangeEnd, 'recibidos');
totalFound += recibidos.found;
@@ -325,10 +317,8 @@ async function processInitialSync(
console.error(`[SAT] Error procesando recibidos ${currentDate.toISOString()}:`, error.message);
}
// Siguiente mes
currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
// Pequeña pausa entre meses para no saturar el SAT
await new Promise(resolve => setTimeout(resolve, 5000));
}
@@ -352,7 +342,6 @@ async function processDailySync(ctx: SyncContext, jobId: string): Promise<void>
let totalInserted = 0;
let totalUpdated = 0;
// Procesar emitidos del mes
try {
const emitidos = await processDateRange(ctx, jobId, inicioMes, ahora, 'emitidos');
totalFound += emitidos.found;
@@ -363,7 +352,6 @@ async function processDailySync(ctx: SyncContext, jobId: string): Promise<void>
console.error('[SAT] Error procesando emitidos:', error.message);
}
// Procesar recibidos del mes
try {
const recibidos = await processDateRange(ctx, jobId, inicioMes, ahora, 'recibidos');
totalFound += recibidos.found;
@@ -391,7 +379,6 @@ export async function startSync(
dateFrom?: Date,
dateTo?: Date
): Promise<string> {
// Obtener credenciales FIEL
const decryptedFiel = await getDecryptedFiel(tenantId);
if (!decryptedFiel) {
throw new Error('No hay FIEL configurada o está vencida');
@@ -403,10 +390,8 @@ export async function startSync(
password: decryptedFiel.password,
};
// Crear servicio SAT
const service = createSatService(fielData);
// Obtener datos del tenant
const tenant = await prisma.tenant.findUnique({
where: { id: tenantId },
select: { databaseName: true },
@@ -416,7 +401,6 @@ export async function startSync(
throw new Error('Tenant no encontrado');
}
// Verificar que no haya sync activo
const activeSync = await prisma.satSyncJob.findFirst({
where: {
tenantId,
@@ -428,7 +412,6 @@ export async function startSync(
throw new Error('Ya hay una sincronización en curso');
}
// Crear job
const now = new Date();
const job = await prisma.satSyncJob.create({
data: {
@@ -446,7 +429,7 @@ export async function startSync(
service,
rfc: decryptedFiel.rfc,
tenantId,
databaseName: tenant.databaseName,
pool: tenantDb.getPool(tenantId, tenant.databaseName),
};
// Ejecutar sincronización en background