From e50e7100f16157d9efcfcd67615ac336a73c3864 Mon Sep 17 00:00:00 2001 From: Consultoria AS Date: Sun, 25 Jan 2026 00:56:47 +0000 Subject: [PATCH] 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 --- apps/api/src/app.ts | 4 + apps/api/src/controllers/fiel.controller.ts | 82 +++++++++ apps/api/src/controllers/sat.controller.ts | 177 ++++++++++++++++++++ apps/api/src/routes/fiel.routes.ts | 19 +++ apps/api/src/routes/sat.routes.ts | 31 ++++ 5 files changed, 313 insertions(+) create mode 100644 apps/api/src/controllers/fiel.controller.ts create mode 100644 apps/api/src/controllers/sat.controller.ts create mode 100644 apps/api/src/routes/fiel.routes.ts create mode 100644 apps/api/src/routes/sat.routes.ts diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 958935c..bc51eac 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -13,6 +13,8 @@ import { calendarioRoutes } from './routes/calendario.routes.js'; import { reportesRoutes } from './routes/reportes.routes.js'; import { usuariosRoutes } from './routes/usuarios.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(); @@ -43,6 +45,8 @@ app.use('/api/calendario', calendarioRoutes); app.use('/api/reportes', reportesRoutes); app.use('/api/usuarios', usuariosRoutes); app.use('/api/tenants', tenantsRoutes); +app.use('/api/fiel', fielRoutes); +app.use('/api/sat', satRoutes); // Error handling app.use(errorMiddleware); diff --git a/apps/api/src/controllers/fiel.controller.ts b/apps/api/src/controllers/fiel.controller.ts new file mode 100644 index 0000000..5efa5ba --- /dev/null +++ b/apps/api/src/controllers/fiel.controller.ts @@ -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 { + 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 { + 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 { + 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' }); + } +} diff --git a/apps/api/src/controllers/sat.controller.ts b/apps/api/src/controllers/sat.controller.ts new file mode 100644 index 0000000..2047460 --- /dev/null +++ b/apps/api/src/controllers/sat.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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' }); + } +} diff --git a/apps/api/src/routes/fiel.routes.ts b/apps/api/src/routes/fiel.routes.ts new file mode 100644 index 0000000..c5f9ffb --- /dev/null +++ b/apps/api/src/routes/fiel.routes.ts @@ -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; diff --git a/apps/api/src/routes/sat.routes.ts b/apps/api/src/routes/sat.routes.ts new file mode 100644 index 0000000..06075da --- /dev/null +++ b/apps/api/src/routes/sat.routes.ts @@ -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;