Plataforma técnica · Orchestrator
Ciclo Contable
Orchestrator Ciclo Contable Cierre Mensual
El dominio Ciclo Contable del Orchestrator es una vista de control read-only: agrega el estado de los dominios operativos (operaciones SII, compras, ventas, remuneraciones, declaraciones, inventario, depreciación, conciliación, balances, provisiones, PPM, incobrables, resultados acumulados) en una semántica uniforme pendiente | parcial | completo por paso.
No genera asientos, no contabiliza, no llama a withTransaction. Su responsabilidad única es componer un snapshot del estado del período para que la UI (CicloContableIsland) muestre qué falta cerrar y por dónde se rompe el flujo.
Ubicación
Section titled “Ubicación”orchestrator/src/domain/cicloContable/├── CicloContableService.ts # composición + reglas de estado por paso├── CicloContableRepository.ts # 1 query por paso, tolerante a tenants├── types.ts # EstadoPaso, PasoCiclo, DetalleXxx└── __tests__/ ├── CicloContableRepository.unit.test.ts ├── CicloContableService.unit.test.ts └── CierreBalanceMensual.unit.test.tsEndpoint HTTP único en routes/ciclo-contable/index.ts:
GET /api/ciclo-contable/estado?anio=2026&mes=3 # vista mensualGET /api/ciclo-contable/estado?anio=2026 # vista anualServicios
Section titled “Servicios”| Servicio | Responsabilidad | Doc |
|---|---|---|
CicloContableService | Entry point único getEstado(ctx, anio, mes). Bifurca en vista mensual (11 pasos) o anual (7 pasos). Computa EstadoPaso por paso aplicando reglas declarativas. | → |
CicloContableRepository | Una query SQL por paso. Tolerante a 42P01/3F000/42703 (tenants con esquemas incompletos retornan ceros en lugar de fallar). Wrappeado con wrapStaticRepository para slow-query logging (>500 ms). | — |
Modos de operación
Section titled “Modos de operación”| Modo | Trigger | Pasos | Fuente principal |
|---|---|---|---|
| Mensual | mes !== null | 11 — operaciones, clasificación, compras, ventas, costo ventas, remuneraciones, F29, stock, depreciación mensual, conciliación, cierre balance | Tablas operativas filtradas por año + mes |
| Anual | mes === null | 7 — depreciación anual, provisión gastos, incobrables, costo ventas anual, PPM, balances/reportes, resultados acumulados | Agregaciones del ejercicio + ReportesService |
Ambos modos retornan la misma estructura:
interface EstadoCicloContable { periodo: { anio: number; mes: number | null; modo: "mensual" | "anual" }; pasos: PasoCiclo[];}
interface PasoCiclo { id: string; titulo: string; subtitulo: string; estado: "pendiente" | "parcial" | "completo"; bloqueado_por?: string | null; detalle: DetalleXxx; // unión discriminada por id}Mapa de dependencias
Section titled “Mapa de dependencias”flowchart LR
subgraph SRC["Esquemas leídos"]
OPS[("operaciones_sii.*<br/>compras · ventas · boletas · detalle")]
INV[("inventario.*<br/>proveedores_clasificacion · saldo_stock · movimientos · provision_ajustes_cierre")]
REM[("remuneraciones.*<br/>honorarios · contratos · liquidaciones")]
DEC[("declaraciones.declaraciones_f29")]
AF[("activo_fijo.*<br/>activos · depreciacion")]
FIN[("financieros.*<br/>movimientos_bancarios · conciliacion_bancaria")]
ADM[("administracion.*<br/>saldo_cuentas_cierre · configuracion_empresa")]
REP[("reportes.balance_guardado")]
end
subgraph SVC["domain/cicloContable/"]
REPO["CicloContableRepository<br/>(1 query por paso)"]
SRV["CicloContableService<br/>(estado por paso + composición)"]
end
subgraph DEPS["Delegaciones"]
RPS["ReportesService<br/>(PPM, resultados acumulados)"]
RPR["ReportesRepository<br/>(checkCierreBlocker)"]
end
subgraph OUT["Consumidores"]
EP["GET /api/ciclo-contable/estado"]
ISL["CicloContableIsland"]
end
OPS & INV & REM & DEC & AF & FIN & ADM & REP --> REPO
REPO --> SRV
SRV -. PPM anual .-> RPS
SRV -. blocker cierre .-> RPR
RPS --> SRV
RPR --> SRV
SRV --> EP --> ISL Semántica de estado
Section titled “Semántica de estado”Las reglas son declarativas por paso y viven en CicloContableService. El patrón general:
if (total === 0) estado = "pendiente"; // no aplica al períodoelse if (pendientes === 0) estado = "completo"; // todo lo aplicable resueltoelse estado = "parcial"; // hay actividad pero queda trabajoEl detalle de cada regla (incluidos los pasos que no siguen este patrón — declaraciones, cierre_balance, costo_ventas, ppm, resultados_acumulados) está en CicloContableService › Reglas por paso.
Tolerancia multi-tenant
Section titled “Tolerancia multi-tenant”Varios tenants no tienen todos los esquemas (depreciación, provisiones, financieros, reportes). El repositorio captura 42P01 (undefined_table), 3F000 (invalid_schema_name), 42703 (undefined_column) y 42P16 (invalid_table_definition) y retorna ceros en lugar de propagar.
try { const { rows } = await pool.query(...); return rows[0];} catch (error) { const code = getPgErrorCode(error); if (code !== "42P01" && code !== "3F000") throw error; return { /* zeros */ };}Esto significa: un paso pendiente puede deberse a un esquema ausente, no sólo a falta de datos. La UI no distingue ambos casos; el contrato del ciclo es “ese paso no contribuye al cierre del período”.
Hook para eventos de cierre
Section titled “Hook para eventos de cierre”CicloContableRepository.findCierreEventPayload(pool, anio, mes) es un cómputo paralelo que retorna solo el listado de pasosCompletados (sin detalles). Se usa para emitir eventos de dominio cuando un cierre se considera consumado externamente:
interface CierreEventPayload { anio: number; mes: number; pasosCompletados: string[]; // ej: ["operaciones", "compras", "f29"]}No emite eventos por sí mismo; el caller decide qué hacer con el payload. Las reglas de “completo” replican exactamente las de getEstado para mantener consistencia.
Convención de orquestación
Section titled “Convención de orquestación”CicloContableService no abre transacciones. Es read-only puro. Si un consumidor necesita un snapshot consistente, debe llamar al endpoint mientras no haya escritura concurrente en los dominios fuente — el aislamiento por defecto (READ COMMITTED) acepta lecturas no repetibles entre pasos.