feat(sat): add API endpoints for FIEL and SAT sync (Phase 7)
- Add FIEL controller with upload, status, and delete endpoints - Add SAT controller with sync start, status, history, and retry - Add admin endpoints for cron job info and manual execution - Register new routes in app.ts - All endpoints protected with authentication middleware Endpoints added: - POST /api/fiel/upload - GET /api/fiel/status - DELETE /api/fiel - POST /api/sat/sync - GET /api/sat/sync/status - GET /api/sat/sync/history - GET /api/sat/sync/:id - POST /api/sat/sync/:id/retry - GET /api/sat/cron - POST /api/sat/cron/run Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import { calendarioRoutes } from './routes/calendario.routes.js';
|
|||||||
import { reportesRoutes } from './routes/reportes.routes.js';
|
import { reportesRoutes } from './routes/reportes.routes.js';
|
||||||
import { usuariosRoutes } from './routes/usuarios.routes.js';
|
import { usuariosRoutes } from './routes/usuarios.routes.js';
|
||||||
import { tenantsRoutes } from './routes/tenants.routes.js';
|
import { tenantsRoutes } from './routes/tenants.routes.js';
|
||||||
|
import fielRoutes from './routes/fiel.routes.js';
|
||||||
|
import satRoutes from './routes/sat.routes.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -43,6 +45,8 @@ app.use('/api/calendario', calendarioRoutes);
|
|||||||
app.use('/api/reportes', reportesRoutes);
|
app.use('/api/reportes', reportesRoutes);
|
||||||
app.use('/api/usuarios', usuariosRoutes);
|
app.use('/api/usuarios', usuariosRoutes);
|
||||||
app.use('/api/tenants', tenantsRoutes);
|
app.use('/api/tenants', tenantsRoutes);
|
||||||
|
app.use('/api/fiel', fielRoutes);
|
||||||
|
app.use('/api/sat', satRoutes);
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
app.use(errorMiddleware);
|
app.use(errorMiddleware);
|
||||||
|
|||||||
82
apps/api/src/controllers/fiel.controller.ts
Normal file
82
apps/api/src/controllers/fiel.controller.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { uploadFiel, getFielStatus, deleteFiel } from '../services/fiel.service.js';
|
||||||
|
import type { FielUploadRequest } from '@horux/shared';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sube y configura las credenciales FIEL
|
||||||
|
*/
|
||||||
|
export async function upload(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tenantId = req.headers['x-tenant-id'] as string;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Tenant ID requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cerFile, keyFile, password } = req.body as FielUploadRequest;
|
||||||
|
|
||||||
|
if (!cerFile || !keyFile || !password) {
|
||||||
|
res.status(400).json({ error: 'cerFile, keyFile y password son requeridos' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await uploadFiel(tenantId, cerFile, keyFile, password);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({ error: result.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: result.message,
|
||||||
|
status: result.status,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[FIEL Controller] Error en upload:', error);
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el estado de la FIEL configurada
|
||||||
|
*/
|
||||||
|
export async function status(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tenantId = req.headers['x-tenant-id'] as string;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Tenant ID requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fielStatus = await getFielStatus(tenantId);
|
||||||
|
res.json(fielStatus);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[FIEL Controller] Error en status:', error);
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina las credenciales FIEL
|
||||||
|
*/
|
||||||
|
export async function remove(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tenantId = req.headers['x-tenant-id'] as string;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Tenant ID requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await deleteFiel(tenantId);
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
res.status(404).json({ error: 'No hay FIEL configurada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'FIEL eliminada correctamente' });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[FIEL Controller] Error en remove:', error);
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
177
apps/api/src/controllers/sat.controller.ts
Normal file
177
apps/api/src/controllers/sat.controller.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import {
|
||||||
|
startSync,
|
||||||
|
getSyncStatus,
|
||||||
|
getSyncHistory,
|
||||||
|
retryJob,
|
||||||
|
} from '../services/sat/sat.service.js';
|
||||||
|
import { getJobInfo, runSatSyncJobManually } from '../jobs/sat-sync.job.js';
|
||||||
|
import type { StartSyncRequest } from '@horux/shared';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia una sincronización manual
|
||||||
|
*/
|
||||||
|
export async function start(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tenantId = req.headers['x-tenant-id'] as string;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Tenant ID requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, dateFrom, dateTo } = req.body as StartSyncRequest;
|
||||||
|
|
||||||
|
const jobId = await startSync(
|
||||||
|
tenantId,
|
||||||
|
type || 'daily',
|
||||||
|
dateFrom ? new Date(dateFrom) : undefined,
|
||||||
|
dateTo ? new Date(dateTo) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
jobId,
|
||||||
|
message: 'Sincronización iniciada',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[SAT Controller] Error en start:', error);
|
||||||
|
|
||||||
|
if (error.message.includes('FIEL') || error.message.includes('sincronización en curso')) {
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el estado actual de sincronización
|
||||||
|
*/
|
||||||
|
export async function status(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tenantId = req.headers['x-tenant-id'] as string;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Tenant ID requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncStatus = await getSyncStatus(tenantId);
|
||||||
|
res.json(syncStatus);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[SAT Controller] Error en status:', error);
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el historial de sincronizaciones
|
||||||
|
*/
|
||||||
|
export async function history(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tenantId = req.headers['x-tenant-id'] as string;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Tenant ID requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = parseInt(req.query.page as string) || 1;
|
||||||
|
const limit = parseInt(req.query.limit as string) || 10;
|
||||||
|
|
||||||
|
const result = await getSyncHistory(tenantId, page, limit);
|
||||||
|
res.json({
|
||||||
|
...result,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[SAT Controller] Error en history:', error);
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene detalle de un job específico
|
||||||
|
*/
|
||||||
|
export async function jobDetail(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tenantId = req.headers['x-tenant-id'] as string;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Tenant ID requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const { jobs } = await getSyncHistory(tenantId, 1, 100);
|
||||||
|
const job = jobs.find(j => j.id === id);
|
||||||
|
|
||||||
|
if (!job) {
|
||||||
|
res.status(404).json({ error: 'Job no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(job);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[SAT Controller] Error en jobDetail:', error);
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reintenta un job fallido
|
||||||
|
*/
|
||||||
|
export async function retry(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tenantId = req.headers['x-tenant-id'] as string;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Tenant ID requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const newJobId = await retryJob(id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
jobId: newJobId,
|
||||||
|
message: 'Job reintentado',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[SAT Controller] Error en retry:', error);
|
||||||
|
|
||||||
|
if (error.message.includes('no encontrado') || error.message.includes('Solo se pueden')) {
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene información del job programado (solo admin)
|
||||||
|
*/
|
||||||
|
export async function cronInfo(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const info = getJobInfo();
|
||||||
|
res.json(info);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[SAT Controller] Error en cronInfo:', error);
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejecuta el job de sincronización manualmente (solo admin)
|
||||||
|
*/
|
||||||
|
export async function runCron(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Ejecutar en background
|
||||||
|
runSatSyncJobManually().catch(err =>
|
||||||
|
console.error('[SAT Controller] Error ejecutando cron manual:', err)
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ message: 'Job de sincronización iniciado' });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[SAT Controller] Error en runCron:', error);
|
||||||
|
res.status(500).json({ error: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
}
|
||||||
19
apps/api/src/routes/fiel.routes.ts
Normal file
19
apps/api/src/routes/fiel.routes.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import * as fielController from '../controllers/fiel.controller.js';
|
||||||
|
import { authenticate } from '../middlewares/auth.middleware.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Todas las rutas requieren autenticación
|
||||||
|
router.use(authenticate);
|
||||||
|
|
||||||
|
// POST /api/fiel/upload - Subir credenciales FIEL
|
||||||
|
router.post('/upload', fielController.upload);
|
||||||
|
|
||||||
|
// GET /api/fiel/status - Obtener estado de la FIEL
|
||||||
|
router.get('/status', fielController.status);
|
||||||
|
|
||||||
|
// DELETE /api/fiel - Eliminar credenciales FIEL
|
||||||
|
router.delete('/', fielController.remove);
|
||||||
|
|
||||||
|
export default router;
|
||||||
31
apps/api/src/routes/sat.routes.ts
Normal file
31
apps/api/src/routes/sat.routes.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import * as satController from '../controllers/sat.controller.js';
|
||||||
|
import { authenticate } from '../middlewares/auth.middleware.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Todas las rutas requieren autenticación
|
||||||
|
router.use(authenticate);
|
||||||
|
|
||||||
|
// POST /api/sat/sync - Iniciar sincronización manual
|
||||||
|
router.post('/sync', satController.start);
|
||||||
|
|
||||||
|
// GET /api/sat/sync/status - Estado actual de sincronización
|
||||||
|
router.get('/sync/status', satController.status);
|
||||||
|
|
||||||
|
// GET /api/sat/sync/history - Historial de sincronizaciones
|
||||||
|
router.get('/sync/history', satController.history);
|
||||||
|
|
||||||
|
// GET /api/sat/sync/:id - Detalle de un job
|
||||||
|
router.get('/sync/:id', satController.jobDetail);
|
||||||
|
|
||||||
|
// POST /api/sat/sync/:id/retry - Reintentar job fallido
|
||||||
|
router.post('/sync/:id/retry', satController.retry);
|
||||||
|
|
||||||
|
// GET /api/sat/cron - Información del job programado (admin)
|
||||||
|
router.get('/cron', satController.cronInfo);
|
||||||
|
|
||||||
|
// POST /api/sat/cron/run - Ejecutar job manualmente (admin)
|
||||||
|
router.post('/cron/run', satController.runCron);
|
||||||
|
|
||||||
|
export default router;
|
||||||
Reference in New Issue
Block a user