Skip to content

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.

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.ts

Endpoint HTTP único en routes/ciclo-contable/index.ts:

GET /api/ciclo-contable/estado?anio=2026&mes=3 # vista mensual
GET /api/ciclo-contable/estado?anio=2026 # vista anual
ServicioResponsabilidadDoc
CicloContableServiceEntry point único getEstado(ctx, anio, mes). Bifurca en vista mensual (11 pasos) o anual (7 pasos). Computa EstadoPaso por paso aplicando reglas declarativas.
CicloContableRepositoryUna 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).
ModoTriggerPasosFuente principal
Mensualmes !== null11 — operaciones, clasificación, compras, ventas, costo ventas, remuneraciones, F29, stock, depreciación mensual, conciliación, cierre balanceTablas operativas filtradas por año + mes
Anualmes === null7 — depreciación anual, provisión gastos, incobrables, costo ventas anual, PPM, balances/reportes, resultados acumuladosAgregaciones 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
}
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

Las reglas son declarativas por paso y viven en CicloContableService. El patrón general:

if (total === 0) estado = "pendiente"; // no aplica al período
else if (pendientes === 0) estado = "completo"; // todo lo aplicable resuelto
else estado = "parcial"; // hay actividad pero queda trabajo

El 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.

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”.

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.

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.