Skip to content
GitHub

Arquitectura Hexagonal

Arquitectura Hexagonal del Sistema Contable

Section titled “Arquitectura Hexagonal del Sistema Contable”

Mapa de la arquitectura hexagonal del sistema contable que muestra el flujo desde el frontend Sevastopol a través del proxy BFF hasta el backend Orchestrator, incluyendo autenticación JWT, gestión multi-tenant con pools de conexión PostgreSQL, y la capa de dominio con servicios de negocio. Destacan el patrón de proxy inverso [1b], el sistema RBAC [2c], la gestión de pools por tenant [3b], y el motor de cálculo de nómina [5c].

El sistema sigue los principios de arquitectura hexagonal, aislando la lógica de negocio central de las preocupaciones externas a través de puertos y adaptadores bien definidos.

El dominio central (Servicios, Motores, Modelos) no tiene dependencias directas de la infraestructura:

  • Sin importaciones de tipos express en la lógica de dominio
  • Sin consultas directas a base de datos en los motores de cálculo
  • Sin operaciones del sistema de archivos en las reglas de negocio
flowchart

%% =========================
%% Sistemas Externos
%% =========================
subgraph EX["Sistemas Externos"]
  WEB["Navegador Web"]
  DB["Base de Datos PostgreSQL"]
  FS["Sistema de Archivos<br/>(Almacenamiento PDF)"]
end

%% =========================
%% Adaptadores
%% =========================
subgraph AD["Adaptadores"]
  FE["Adaptador Frontend (Proxy)<br/>Sevastopol"]
  API["Adaptador API (Rutas)<br/>Express"]

  DB_AD["Adaptador Base de Datos<br/>(Patrón Repository)"]
  FILE_AD["Adaptador de Archivos<br/>(PdfService)"]
end

%% =========================
%% Puertos
%% =========================
subgraph PORTS["Puertos (Interfaces)"]
  HTTP_PORT["Puerto de Petición HTTP"]
  DATA_PORT["Puerto de Acceso a Datos"]
  STORAGE_PORT["Puerto de Almacenamiento"]
end

%% =========================
%% Dominio Central
%% =========================
subgraph DOMAIN["Dominio Central"]
  SERVICES["Servicios de Dominio<br/>PayrollService<br/>EmployeeService<br/>ContractService"]

  CALC["Motores de Cálculo<br/>PayrollEngine<br/>TaxCalculator<br/>SocialLawsCalculator"]

  MODELS["Modelos de Dominio<br/>Employee<br/>Contract<br/>Payroll"]
end

%% =========================
%% Flujos
%% =========================

WEB -->|HTTP| FE
FE -->|Proxy| API
API -->|Transformar| HTTP_PORT
HTTP_PORT --> SERVICES

SERVICES -->|Usa| CALC
SERVICES -->|Usa| MODELS

SERVICES -->|Consultar / Persistir| DATA_PORT
SERVICES -->|Generar PDF| STORAGE_PORT

DATA_PORT --> DB_AD
STORAGE_PORT --> FILE_AD

DB_AD --> DB
FILE_AD --> FS

1 Flujo de Petición del Frontend al Backend

Section titled “1 Flujo de Petición del Frontend al Backend”

El patrón BFF (Backend-for-Frontend) que protege la lógica de negocio en Sevastopol.

Sevastopol Frontend
└── 1a Proxy API en Sevastopol
└── Proxy utils
└── createProxy() genera handlres
└── 1b Reenvío a Orchestrator
Orchestrator Backend
└── App Factory
| │
| └── 1c Registro de Ruta en backend
└── Routes Handler
└── authenticateToken() middleware
└── 1d Middleware de Autenticación

2 Autenticación y Autorización Multi-Tenant

Section titled “2 Autenticación y Autorización Multi-Tenant”

El sistema de seguridad basado en JWT y RBAC que protege los endpoints.

Middleware de Autenticación
└── 2a Extracción de Token desde Cookie
└── 2b Verificación del JWT
└── Decodificación de user payload
Sistema RBAC
└── 2c Verificación RBAC
└── 2d Configuración de Permisos
└── Mapeo de rutas permitidas
Flujo de Autorización
└── authenticateToken() middleware
└── autorizeRoute() validation
└── requireRole() helper

El sistema de pools de conexión qeu aísla datos por tenant.

Configuración Inicial de DB
└── buildPoolConfug()
| │
| └── Parámetros por pool type
| │
| └── Configuración de conexión
└── 3a Pool Central de Conexiones
Gestión de Pools por Tenant
└── 3b Obtención de Pool por tenant
| │
| └── Verificar cache existente
| │
| └── 3c Creación de Pool Específico
│ | │
│ | └── buildPoolConfig(database)
│ | │
│ | └── 3c Creación de Pool Específico
│ │
│ └── 3d Cache de Pools
└── Retornar pool al solicitante
Sistema de Cache
└── Map<string, Pool> tenantPools
└── Reutilización de conexiones

La implementación del patrón Service con lógica de negocio encapsulada.

Ruta HTTP Express
└── 4a Instanciacion ded Servicio
└── 4b Llamada a Servicio de Negocio
Servicio de Dominio
└── getAllTenants(): Promise<Tenant[]>
| │
| └── 4c Servicio de Dominio
└── Repositorio de Datos
└── 4d Acceso a Datos

5 Procesamiento de Nómina con Cálculos Complejos

Section titled “5 Procesamiento de Nómina con Cálculos Complejos”

El motor de cálculo de nómina que desmuestra la separación de responsabilidades.

5a Transacción de Negocio
└── Obtener Contexto del Contrato
| │
| └── Datos del empleado
| │
| └── Asistencia y horas extras
└── Mapeo a Input de Cálculo
│ |
│ └── Conversión de UF a CLP
└── 5c Motor de Cálculo Puro
| │
| └── Cálculo de imponibles
| │
| └── Descuentos legales (AFP, salud)
| │
| └── Cálculo de impuestos
| │
| └── Líquido final
└── 5d Persistencia de Resultados
└── Guardar liquidación
└── Actualizar estados
└── Generar PDF (opcional)
Manejo de Errores
└── Rollback automático
└── Logging de auditoría

El middleware de Astro que protege las páginas HTML y endpoints API.

6a Middleware de Astro
└── 6b Verificación de Rutas Protegidas
| │
| └── /dashboard,/settings,/registry
| │
| └── Extraer token de cookie 'sid'
└── 6c Validación con Backend
│ |
│ └── POST /api/auth/validate
│ |
│ └── Headers: Cookie + Content-Type
│ |
│ └── Respuesta OK/No autorizada
└── 6d Redirección su no Autorizado
Aplicación de headers de seguridad
└── CSP, X-Frame-Options
└── Headers a todas las respuestas

El frontend crea un proxy que reenvía todas las peticiones al backend

export const { GET, POST, PUT, DELETE } = createProxy("/api/tenant");

Se reenvía la petición con cookies y headers al backend principal

const orchestratorRes = await fetch(targetUrl, {
method: request.method,
headers: getProxyHeaders(request),
body: body,
credentials: "include",
});

El backend registra la ruta del tenant en Express

app.use("/api/tenant", tenantRouter);

Se aplica autenticación JWT antes de procesar la petición

router.get('/', authenticateToken, async (req: AuthenticatedRequest, res) => {...})

Se extrae el JWT de la cookie ‘sid’ para autenticación

if (!token && req.headers.cookie) {
const parsed = cookie.parse(req.headers.cookie);
token = parsed.sid;
}

Se verifica y decodifica el token con la clave secreta

const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;

Se verifica si el rol tiene acceso a la ruta específica

if (!rbac.canAccess(role, route)) {...}

Se definen las rutas accesibles por cada rol de usuario

this.routes.set('SUPER_ADMIN', [...])

Pool para base de datos central con metadata de tenants

export const centralPool = new Pool(buildPoolConfig(undefined, "central"));

Función que retorna pool específico para cada tenant.

export function getTenantPool(database: string): Pool {...};

Se crea nuevo pool con configuaración optimizada para tenant

const pool = new Pool(buildPoolConfig(database, "tenant"));

Se almacena el pool en cache para reutilizarlo

tenantPools.set(database, pool);

Se crea instacia del servicio de dominio en la ruta

const tenantService = new TenantService();

El controller delega la lógica al servicio de dominio

const tenants = await tenantService.getAllTenants();

El servicio encapsula la lógica y delega persistencia al repositorio.

async getAllTenants(): Promise<Tenant[]> {...};

El repositorio ejecuta la consulta SQL contra la base de datos.

const { rows } = await centralPool.query<Tenant>(...);

El servicio inicia una transacción para generar nómina.

return this.withTransaction(ctx, async (client) => {...});

Se delega a motor funcional sin efectos secundarios.

const result = PayrollEngine.calculate(input);

Se guardan los resultadios calculados en la base de datos.

const persistedId = await PayrollRepository.savePayroll(...);

se define middleware oara todas las peticiones del frontend

export const onRequest = defineMiddleware(async (ctx, next) => {...});

Se identifican rutas que requieren autenticación

if (
url.pathname.startsWith('/dashboard') ||
url.pathname.startsWith('/settings') ||
url.pathname.startsWith('/registry')
)

Se valida la sesión contra el backend orchestrator

const validateRes = await fetch('http://localhost:8000/api/auth/validate', {...});

Se redirige al login si la validación falla

if (!validateRes.ok) {
return ctx.redirect("/", 302);
}