Skip to content

Plataforma técnica · Orchestrator

Orchestrator (Backend)

Orchestrator

Orchestrator es la API REST del ecosistema Nostromo. Construido en Node.js + Express + TypeScript, expone los endpoints que consume Sevastopol y centraliza autenticación, autorización, resolución de tenant y reglas de negocio.

Esta página cubre lo que vive dentro del Orchestrator: stack, capas, multi-tenancy, contratos de API y catálogo de dominios. Para la visión integral del ecosistema ver Arquitectura del Sistema; para levantar el entorno ver Setup Local.

Solo dependencias conceptuales — las versiones vigentes viven en orchestrator/package.json.

CapaPaquetes
HTTPexpress, cors, helmet, morgan, cookie-parser, express-rate-limit
Validaciónexpress-validator
Persistenciapg (node-postgres), mongodb (sistema de agentes y tareas)
Authjsonwebtoken, bcryptjs
Documentospuppeteer, docxtemplater, pizzip, qrcode
Diagnósticosysteminformation
Utilidadesdotenv, date-fns
Testing (dev)jest, supertest, tsconfig-paths
Lenguaje (dev)typescript

Runtime: Node.js 20+. Gestor: pnpm (workspace Accounting/).


orchestrator/.env mínimo para desarrollo:

Terminal window
NODE_ENV=development
PORT=8000
# PostgreSQL (Mother)
PGHOST=localhost
PGPORT=5432
PGUSER=postgres
PGPASSWORD=$DB_PASSWORD
COMMAND_DB=nostromo_command
COMMON_DB=nostromo_common
# Auth
JWT_SECRET=$JWT_SECRET
JWT_EXPIRES_IN=8h
# Storage
LIQUIDACIONES_DIR=./storage/liquidaciones

Los orígenes CORS están hardcodeados en app.ts (no se leen de env): localhost:4320, localhost:4321, localhost:4322.

En producción NODE_ENV=production activa el redirect HTTP→HTTPS y el header Strict-Transport-Security (HSTS).

El detalle completo y los pasos de creación de bases viven en Setup Local.


  • Directoryorchestrator/
    • Directorysrc/
      • app.ts createApp() factoría Express
      • server.ts — arranque HTTP
      • Directorylib/
        • db.ts centralPool, commonPool, getTenantPool()
        • rbac.ts — control de acceso por rol
        • tenantResolver.ts getDatabaseNameForUser()
        • accessControl.ts — verificaciones de permisos
        • audit.ts auditMiddleware (log de mutaciones)
      • Directorymiddleware/
        • auth.ts authenticateToken, AuthenticatedRequest
        • errorHandler.ts AppError, errorHandler, notFoundHandler, asyncHandler
        • metrics.ts metricsMiddleware, metricsHandler
        • rateLimiter.ts — limitador por rol
        • validation.ts — helpers para express-validator
      • Directorydomain/ — un subdirectorio por contexto (DDD)
      • Directoryroutes/ — un subdirectorio por dominio HTTP
      • Directorycontrollers/ — controllers híbridos (ej. EtlController)
      • Directoryservices/agent/ — sistema de agentes y registry
      • Directorytest/ *.unit.test.ts · *.integration.test.ts · *.e2e.test.ts
      • Directoryscripts/ — utilidades CLI (scan-manual-cuentas, seed-manual-cuentas)
    • Directoryassets/
    • Directorystorage/liquidaciones/ — PDFs generados
    • package.json
    • tsconfig.json
    • jest.config.js

Alias TypeScript (tsconfig.json): @/lib/*, @/middleware/*, @/domain/*, @/routes/*.


createApp() retorna una instancia de Express ya configurada, sin iniciar el servidor — esto permite importarla en tests sin abrir puertos. server.ts se limita a createApp().listen(PORT).

El orden importa: seguridad → CORS → logging → parsing → tracing → métricas → audit → rutas → 404 → error handler.

flowchart LR
  REQ["Petición HTTP"] --> TP["trust proxy = 1"]
  TP --> HTTPS["HTTPS redirect + HSTS<br/>(prod)"]
  HTTPS --> HEL["helmet()<br/>+ CSP estricta"]
  HEL --> CRS["cors() + cookies"]
  CRS --> MOR["morgan('combined')"]
  MOR --> EJS["express.json (2mb)"]
  EJS --> CKP["cookieParser"]
  CKP --> CC["Cache-Control no-store<br/>(/api/*)"]
  CC --> RID["requestIdMiddleware"]
  RID --> MET["metricsMiddleware"]
  MET --> AUD["auditMiddleware"]
  AUD --> RT["Rutas (/api/*, /health, /metrics)"]
  RT --> AUTH["authenticateToken<br/>(por router protegido)"]
  AUTH --> HDL["Handler"]
  HDL --> NF["notFoundHandler"]
  NF --> ERR["errorHandler global"]
  ERR --> RES["Respuesta HTTP"]
MiddlewarePropósito
trust proxy = 1Resolver IP real detrás de Nginx/Cloudflare (requerido por el rate limiter).
HTTPS redirect + HSTSEn NODE_ENV=production: 301 a https:// y header Strict-Transport-Security.
helmet({ contentSecurityPolicy })CSP estricta: defaultSrc 'self', scriptSrc 'self', frameAncestors 'none'.
cors({ credentials })Acepta localhost:4320–4322 con cookies.
morgan('combined')Log estilo Apache por petición a stdout.
express.json({ limit })Parsear JSON hasta 2 MB.
cookieParser()Habilita req.cookies (cookie sid).
Cache-Control no-storeAplicado a todo /api/* — evita 304 sin body en clientes fetch.
requestIdMiddlewareAsigna requestId (UUID) y lo expone para tracing y errorHandler.
metricsMiddlewareCuenta peticiones y latencias; excluye /health y /metrics.
auditMiddlewareLoguea operaciones mutantes; excluye /health, /metrics, /api/auth/login.
authenticateTokenValida JWT y resuelve req.user (aplicado por router protegido).
notFoundHandler404 con errorCode: NOT_FOUND y requestId.
errorHandlerCaptura AppError y desconocidos, formato JSON consistente, oculta stack en prod.

Configurado en app.ts (actualmente deshabilitado en desarrollo, listo para producción):

RolPeticiones / 15 min
SUPER_ADMIN5000
ADMIN2000
Autenticado1000
Anónimo500

Salta /health. Respuesta de límite excedido sigue el shape estándar de error.

Terminal window
curl http://localhost:8000/health
# → {"status":"OK","service":"orchestrator"}

Ruta pública. No consulta la base de datos: solo confirma que el proceso responde. El rate limiter y el audit la excluyen.

Terminal window
curl http://localhost:8000/metrics

Endpoint con métricas en formato Prometheus generadas por metricsMiddleware. Pensado para sondas de monitoring externo.

/files/liquidaciones/<archivo>.pdf sirve los PDFs generados desde LIQUIDACIONES_DIR con Cache-Control: public, max-age=31536000, immutable.


Modelo database-per-tenant. Tres tipos de pool en lib/db.ts:

flowchart LR
  subgraph PM["Pools"]
    CP["centralPool"]
    CMP["commonPool"]
    GET["getTenantPool(db)"]
    MAP["Map<string, Pool><br/>(tenantPools)"]
    GET --> MAP
  end
  subgraph DB["Bases de Datos"]
    CMD[("nostromo_command")]
    COM[("nostromo_common")]
    T1[("nostromo_60004317")]
    T2[("nostromo_70001234")]
  end
  CP --> CMD
  CMP --> COM
  MAP --> T1
  MAP --> T2
PoolTipoBaseContenido
centralPoolSingletonnostromo_commandUsuarios, sesiones, tenants registrados, monitoreo.
commonPoolSingletonnostromo_commonParámetros compartidos (monedas, AFP, IUSC, AFC).
tenantPoolsCacheadosnostromo_<rut_sin_dv>Negocio aislado por empresa.

getTenantPool(dbName) reutiliza la conexión si ya existe en el Map; si no, crea un new Pool(config) y lo cachea.

sequenceDiagram
  autonumber
  participant R as Router
  participant A as authenticateToken
  participant T as tenantResolver
  participant P as getTenantPool
  participant DB as PostgreSQL

  R->>A: Validar cookie sid
  A-->>R: req.user = { userId, role, ... }
  R->>T: getDatabaseNameForUser(user, req)
  T->>T: Verificar permisos RBAC
  T-->>R: nombre de tenantDb
  R->>P: getTenantPool(tenantDb)
  alt Pool cacheado
    P-->>R: Pool existente
  else Cache miss
    P->>DB: new Pool(config)
    P-->>R: Pool nuevo
  end
  R->>DB: pool.query(sql, params)
  DB-->>R: Resultado

El nombre de tenant del usuario se persiste en nostromo_command.users.tenant_db. Sin ese vínculo, getDatabaseNameForUser devuelve 403 Forbidden.


El Orchestrator separa cada petición en cuatro capas con responsabilidades disjuntas. El dominio Remuneraciones es el caso más completo:

flowchart LR
  subgraph R["Route"]
    ER["express.Router()"]
    GET["GET /"]
    POSTG["POST /generate"]
    POSTP["POST /preview"]
  end
  subgraph S["Service"]
    GP["generatePayroll()"]
    PP["previewPayroll()"]
    MAP["mapContextToInput()"]
  end
  subgraph E["Engine (puro)"]
    ENG["PayrollEngine.calculate()"]
    BASE["BaseSalaryCalculator"]
    GRAT["GratificationCalculator"]
    SOC["SocialLawsCalculator"]
    TAX["TaxCalculator"]
    PRO["ProrrataCalculator"]
  end
  subgraph REP["Repository"]
    CTX["getPayrollContext()"]
    SAVE["savePayroll()"]
    LIST["list()"]
    FBID["findById()"]
  end
  subgraph DB["PostgreSQL"]
    LIQ["remuneraciones.liquidaciones<br/>liquidaciones_detalle"]
    VLIQ["v_liquidaciones_departamento<br/>v_liquidaciones_detalle_completo"]
    PAR["parametros.*"]
  end

  ER --> GET & POSTG & POSTP
  POSTG --> GP
  POSTP --> PP
  GET --> LIST
  GP --> CTX --> MAP --> ENG
  GP --> SAVE
  ENG --> BASE & GRAT & SOC & TAX & PRO
  SAVE --> LIQ
  FBID --> LIQ
  LIST --> VLIQ
  CTX --> PAR
CapaArchivoResponsabilidadAcceso a DBTestabilidad
Routepayroll.tsParsear request, validar params, formato JSON.Solo vía Service.E2E (Supertest)
ServicePayrollService.tsOrquestar Repository → Engine → Repository.Solo vía Repository.Integration
EnginePayrollEngine.tsCálculo puro. Función estática sin efectos.Ninguno.Unit (rápido)
RepositoryPayrollRepository.tsQueries SQL y mapeo a objetos del dominio.Directo (Pool).Integration

Flujo de POST /api/remuneraciones/payroll/generate

Section titled “Flujo de POST /api/remuneraciones/payroll/generate”
  1. Route parsea contrato_id y periodo_mes, resuelve la base del tenant.
  2. Service.generatePayroll llama Repository.getPayrollContextmapContextToInputEngine.calculateRepository.savePayroll.
  3. Repository.getPayrollContext lee empleados, contratos, asistencia, indicadores, topes y tramos de impuesto en paralelo (Promise.all).
  4. Engine.calculate ejecuta los 5 calculadores en secuencia y retorna PayrollResult.
  5. Repository.savePayroll inserta cabecera en liquidaciones y líneas en liquidaciones_detalle dentro de una transacción.
  6. Route retorna JSON snake_case con id_liquidacion, total_liquido, total_haberes, total_descuentos.

Patrón vigente con asyncHandler (envuelve errores async automáticamente) y custom error classes:

import express from "express";
import { getTenantPool } from "@/lib/db";
import { authenticateToken, AuthenticatedRequest } from "@/middleware/auth";
import { asyncHandler, NotFoundError } from "@/middleware/errorHandler";
import { getDatabaseNameForUser } from "@/lib/tenantResolver";
import { PayrollRepository } from "@/domain/payroll/PayrollRepository";
const router = express.Router();
router.use(authenticateToken);
router.get(
"/",
asyncHandler(async (req: AuthenticatedRequest, res) => {
const db = await getDatabaseNameForUser(req.user, req);
const pool = getTenantPool(db);
const rows = await PayrollRepository.list(pool, parseFilters(req.query));
res.json(rows);
}),
);
router.get(
"/:id",
asyncHandler(async (req: AuthenticatedRequest, res) => {
const pool = getTenantPool(await getDatabaseNameForUser(req.user, req));
const row = await PayrollRepository.findById(pool, req.params.id);
if (!row) throw new NotFoundError("Liquidación", req.params.id);
res.json(row);
}),
);
export default router;

asyncHandler evita el try/catch en cada handler: cualquier rechazo se propaga al errorHandler global. Las custom error classes (BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, ValidationError) viven en middleware/errorHandler.ts y traducen automáticamente a su statusCode y errorCode.

LadoConvenciónEjemplo
Variables internascamelCasetotalHaberes, sueldoBase
Columnas PostgreSQLsnake_casetotal_haberes, sueldo_base
Respuestas JSONsnake_case{ "total_haberes": 1500000 }

La respuesta replica la forma de la base: evita una capa de transformación y simplifica el binding en Sevastopol.

Todas las rutas de listado soportan limit y offset como query params:

Terminal window
GET /api/employees?limit=100&offset=200

Defaults: limit=2000, offset=0. El handler hace parseInt defensivo.

Listados por período aceptan año y mes (alias anio, year, month por compatibilidad):

Terminal window
GET /api/remuneraciones/payroll?año=2026&mes=5
CódigoUsoEjemplo
200GET/PUT exitosoListar liquidaciones.
201POST exitoso (crea recurso)Crear un tenant.
204DELETE exitosoEliminar registro.
400Payload inválidoFaltan campos requeridos.
401Token faltante o inválidoSin cookie sid.
403Token válido pero sin permisosRol insuficiente o tenant no asignado.
404Recurso no existeID no encontrado.
500Error del servidorFallo de conexión a base de datos.
501Vista o feature no disponibleVista materializada no creada aún.

errorHandler produce siempre el mismo shape:

{
"success": false,
"error": "NOT_FOUND",
"message": "Liquidación con id 'abc-123' no encontrado",
"requestId": "5f1e1d2a-...",
"timestamp": "2026-05-23T12:00:00.000Z",
"path": "/api/remuneraciones/payroll/abc-123",
"method": "GET"
}

En NODE_ENV !== 'production' la respuesta agrega stack y context. En errores 5xx producción, message se reemplaza por "An unexpected error occurred" para no filtrar internals.

Errores de ValidationError incluyen además un campo errors con el detalle por campo (formato express-validator).


Cada dominio vive en src/domain/<contexto>/ con su trío Service + Repository + (opcional) Engine/Calculator. La página de detalle de cada uno explica entidades, calculadoras y endpoints.

DominioCarpetaEndpoints principales
Remuneracionesdomain/payroll/ y otros/api/remuneraciones/payroll, /api/employees, /api/contracts, /api/attendance, /api/vacations, /api/permissions, /api/honorarios, /api/finiquitos, /api/previsiones, /api/afp, /api/isapre, /api/apv_contracts, /api/isapre_contracts, /api/cargos, /api/working_day, /api/generate-pdf
Operaciones SIIdomain/operaciones//api/operaciones, /api/accounting, /api/etl — compras, ventas, boletas, ETL mensual.
Activo Fijodomain/activo-fijo//api/activo-fijo — contabilización y depreciación.
Inventariodomain/inventario//api/inventario.
Gastosdomain/gastos//api/gastos.
Financierosdomain/financieros//api/financieros — instrumentos, cuentas bancarias.
Declaracionesdomain/declaraciones//api/declaraciones/f29.
Declaraciones Juradasdomain/declaraciones_juradas//api/declaraciones-juradas/1887, /api/declaraciones-juradas/1879.
Ciclo Contabledomain/cicloContable//api/ciclo-contable — apertura, cierre, ajustes.
Reportesdomain/reportes//api/reportes.
Manual de Cuentasdomain/internal/ + más/api/manual-cuentas, /api/command/manual-cuentas, /api/internal/manual-cuentas.
Administracióndomain/{company,capital,legal-representatives,system-config,configContable}//api/admin/company, /api/admin/capital, /api/admin/representatives, /api/admin/system-config, /api/admin/config-contable, /api/admin/chart-of-accounts.
Commondomain/common//api/parameters, /api/afc, /api/impuesto_2cat.
Commanddomain/command/, domain/auth//api/auth, /api/admin, /api/tenant, /api/tenant-db, /api/sessions, /api/menu, /api/monitoring, /api/command/plan-cuentas.
Agentesservices/agent//api/agent/tasks, /api/agent/registry — orquestación de agentes AI internos.
Cachedomain/cache/Cache compartido por servicios.

Generado contra src/app.ts. Las bases listadas son las relevantes; cada handler resuelve el tenantPool cuando aplica.

PrefijoArchivoBase / Schema
/api/authroutes/command/auth.tsnostromo_command.users · sessions
/api/adminroutes/command/users.tsnostromo_command.users
/api/sessionsroutes/command/sessions.tsnostromo_command.sessions
/api/tenantroutes/command/tenant.tsnostromo_command.tenants
/api/tenant-dbroutes/command/tenant-db.tsnostromo_command.tenant_databases
/api/menuroutes/command/menu.tsMenús dinámicos
/api/monitoringroutes/command/monitoring.tsMétricas del sistema
/api/command/plan-cuentasroutes/command/plan-cuentas.tsPlan contable canónico
/api/command/manual-cuentasroutes/command/manual-cuentas.tsManual de cuentas (admin)
/api/internal/manual-cuentasroutes/internal/manual-cuentas.tsExport read-only (token interno)
/api/agent/tasksroutes/command/agent-tasks.tsSistema de agentes (MongoDB)
/api/agent/registryroutes/command/registry.tsRegistry de agentes
PrefijoArchivoBase / Schema
/api/parametersroutes/common/parameters.tsparametros.indicadores
/api/afcroutes/common/afc.tsparametros.afc
/api/impuesto_2catroutes/common/impuesto_2cat.tsparametros.impuesto_2cat
PrefijoArchivoBase / Schema
/api/operacionesroutes/operaciones/operaciones_sii.*
/api/accountingroutes/accounting.tsoperaciones_sii.*
/api/etlroutes/etlRoutes.ts (+ controllers/EtlController.ts)Pipeline ETL mensual
PrefijoArchivoBase / Schema
/api/employeesroutes/remuneraciones/employees.tsremuneraciones.empleados
/api/contractsroutes/remuneraciones/contracts.tsremuneraciones.contratos
/api/cargosroutes/remuneraciones/cargos.tsremuneraciones.cargos
/api/departmentsroutes/remuneraciones/departments.tsremuneraciones.departamentos
/api/attendanceroutes/remuneraciones/attendance.tsremuneraciones.asistencia_dia
/api/working_dayroutes/remuneraciones/working_day.tsremuneraciones.jornadas
/api/vacationsroutes/remuneraciones/vacations.tsremuneraciones.vacaciones
/api/permissionsroutes/remuneraciones/permissions.tsremuneraciones.permisos
/api/afproutes/remuneraciones/afp.tsremuneraciones.afp
/api/isapreroutes/remuneraciones/isapre.tsremuneraciones.isapre
/api/apv_contractsroutes/remuneraciones/apv_contracts.tsremuneraciones.contrato_apv
/api/isapre_contractsroutes/remuneraciones/isapre_contracts.tsremuneraciones.contrato_isapre
/api/remuneraciones/payrollroutes/remuneraciones/payroll.tsremuneraciones.liquidaciones
/api/previsionesroutes/remuneraciones/previsiones.tsAgregaciones de seguridad social
/api/honorariosroutes/remuneraciones/honorarios.tsremuneraciones.honorarios
/api/finiquitosroutes/remuneraciones/finiquitos.tsremuneraciones.finiquitos
/api/generate-pdfroutes/remuneraciones/generate-pdf.tsGeneración de PDF
PrefijoArchivoBase / Schema
/api/admin/chart-of-accountsroutes/admin/chart-of-accounts.tsadministracion.plan_contable
/api/admin/companyroutes/admin/company.tsadministracion.empresas
/api/admin/capitalroutes/admin/capital.tsadministracion.capital
/api/admin/representativesroutes/admin/representatives.tsadministracion.representantes_legales
/api/admin/system-configroutes/admin/system-config.tsadministracion.system_config
/api/admin/config-contableroutes/admin/config-contable.tsConfiguración contable
PrefijoArchivoBase / Schema
/api/activo-fijoroutes/activo-fijo/activo_fijo.*
/api/inventarioroutes/inventario/inventario.*
/api/gastosroutes/gastos/gastos.*
/api/financierosroutes/financieros/financieros.*
/api/declaraciones/f29routes/declaraciones/f29.tsdeclaraciones.f29
/api/declaraciones-juradas/1887routes/declaraciones_juradas/dj1887.tsdeclaraciones_juradas.dj1887
/api/declaraciones-juradas/1879routes/declaraciones_juradas/dj1879.tsdeclaraciones_juradas.dj1879
/api/ciclo-contableroutes/ciclo-contable/ciclo_contable.*
/api/reportesroutes/reportes/Vistas y reportes derivados
/api/manual-cuentasroutes/manual-cuentas/Manual de cuentas (lectura)
EndpointTipoPropósito
/healthSonda{"status":"OK","service":"orchestrator"}.
/metricsPrometheusMétricas internas vía metricsHandler.
/files/liquidaciones/<pdf>StaticPDFs servidos con cache inmutable.

MecanismoQué provee
morgan('combined')Log estilo Apache de cada petición HTTP a stdout.
requestIdMiddlewareUUID por petición, propagado al errorHandler y a respuestas de error.
metricsMiddlewareRecolecta contadores y latencias; expuestos en /metrics (Prometheus).
auditMiddlewareLoguea operaciones mutantes (POST/PUT/PATCH/DELETE) con user, path, params.
/healthSonda de liveness; no toca la base ni atraviesa rate limit.
/api/monitoringEndpoints internos del dominio Command (carga del sistema, uptime, sesiones).
errorHandlerCentraliza el log: 5xx → console.error con stack; 4xx → console.warn con ctx.

Los logs de stdout/stderr quedan a cargo del runtime (Docker, systemd o Cloudflare). No hay sink de logs externo configurado por defecto.


Tres tipos según sufijo de archivo:

TipoSufijoQué cubreVelocidad
Unit*.unit.test.tsFunciones puras (Engine, Calculators). Sin DB.<100ms
Integration*.integration.test.tsRepository contra DB real o transacciones.100–1000ms
E2E*.e2e.test.tsCiclo HTTP completo vía supertest.1–5s

Cobertura esperada:

  • Engines (lógica de negocio): 90%+.
  • Endpoints críticos (auth, generación de nómina): 100%.
  • Repository (queries SQL): 70%+.

Detalle de runners y configuración en Testing.


Desde la raíz del workspace Accounting/ (preferido) o pnpm --dir orchestrator ...:

ComandoPropósito
pnpm dev:orchestratorHot reload (nodemon -r tsconfig-paths/register).
pnpm --dir orchestrator buildCompilar TypeScript a dist/.
pnpm --dir orchestrator startServidor de producción (node dist/server.js).
pnpm --dir orchestrator testTodos los tests (jest).
pnpm --dir orchestrator test:unitSolo unit tests (*.unit.test.ts).
pnpm --dir orchestrator test:integrationSolo integration tests.
pnpm --dir orchestrator test:domainTests de la capa de dominio (src/domain/).
pnpm --dir orchestrator test:payrollTests del dominio Remuneraciones.
pnpm --dir orchestrator test:watchModo watch.
pnpm --dir orchestrator lintESLint sobre src/**/*.ts.
pnpm --dir orchestrator lint:fixESLint con --fix.
pnpm --dir orchestrator scan:manual-cuentasCLI: inspeccionar manual de cuentas.
pnpm --dir orchestrator seed:manual-cuentasCLI: sembrar manual de cuentas.