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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user