Skip to content

Plataforma técnica · Orchestrator

Ciclo Contable Service

Orchestrator Ciclo Contable Cierre Mensual

CicloContableService compone un snapshot del estado del ciclo contable para un período. Tiene un único método público y no muta estado.

async getEstado(
ctx: ServiceContext,
anio: number,
mes: number | null,
): Promise<EstadoCicloContable>

mes === null bifurca a getEstadoAnual (privado, 7 pasos). Cualquier otro valor invoca la vista mensual (11 pasos).

flowchart TB
  IN["getEstado(ctx, anio, mes)"]
  POOL["getPool(ctx.tenantDb)"]
  BIF{"mes === null?"}
  MEN["Vista mensual<br/>Promise.all(10 queries)"]
  AN["Vista anual<br/>Promise.all(7 fuentes)"]
  RULES["Reglas estado por paso<br/>(pendiente | parcial | completo)"]
  COMP["Compose PasoCiclo[]"]
  OUT["EstadoCicloContable"]

  IN --> POOL --> BIF
  BIF -- no --> MEN --> RULES
  BIF -- sí --> AN --> RULES
  RULES --> COMP --> OUT

Promise.all ejecuta las 10 queries del repositorio en paralelo. El paso costo_ventas se deriva del mismo dato que ventas (sin query extra).

OrdenidDetalleFuente principal
1operacionesDetalleOperacionesoperaciones_sii.{compras,ventas,boletas} vs compras_ventas_detalle
2clasificacionDetalleClasificacioninventario.proveedores_clasificacion LEFT JOIN sobre compras del período
3comprasDetalleCompras (con desglose existencias/activo_fijo/gasto/sin_clasificar)compras_ventas_detalle con tipo_concepto = 'NETO'
4ventasDetalleVentascompras_ventas_detalle filtrado por VENTAS_CONTABILIZABLES_SQL
5costo_ventasDetalleAnualCostoVentas (reusado)inventario.movimientos_inventario + saldo_stock
6remuneracionesDetalleRemuneracionesremuneraciones.{honorarios,contratos,liquidaciones}
7declaracionesDetalleDeclaracionesdeclaraciones.declaraciones_f29 (último del período)
8stockDetalleStockinventario.saldo_stock
9depreciacionDetalleDepreciacionMensualactivo_fijo.{activos,depreciacion} con CTE de activos vigentes
10conciliacionDetalleConciliacioncompras_ventas_detalle (lado libro) + financieros.movimientos_bancarios (lado banco)
11cierre_balanceDetalleCierreBalanceMensualadministracion.saldo_cuentas_cierre ó reportes.balance_guardado + checkCierreBlocker

Sin orden estricto en UI; se evalúan en paralelo. Dos delegaciones externas: ReportesService.getPpmActualizacionAnual y ReportesRepository.getEstadoResultadosAcumuladosAnual.

idDetalleFuente
depreciacionDetalleDepreciacionAnual (incluye activos_pendientes_nombres[] top 10)activo_fijo.* con CTE de vigentes por fecha_fin efectiva
provision_gastosDetalleProvisionGastosinventario.provision_ajustes_cierre LATERAL JOIN sobre compras devengadas
incobrablesDetalleIncobrablesfinancieros.conciliacion_bancaria con diferencia negativa
costo_ventasDetalleAnualCostoVentasAgregado anual de movimientos_inventario + saldo_stock
ppmDetallePPMReportesService.getPpmActualizacionAnual
balances_reportesDetalleBalancesReportesreportes.balance_guardado filtrado por año
resultados_acumuladosDetalleResultadosAcumuladosReportesRepository.getEstadoResultadosAcumuladosAnual

La mayoría sigue el patrón general (total === 0 → pendiente, pendientes === 0 → completo, sino parcial). Las excepciones se documentan abajo.

const f29Existe = f29_estado !== null;
const f29SinPago = f29Existe && Number(f29_total_a_pagar ?? 0) === 0;
if (f29_estado === "DECLARADO" || f29_estado === "RECTIFICADO" || f29SinPago)
estado = "completo";
else if (f29Existe) estado = "parcial";
else estado = "parcial"; // declaración es obligatoria → nunca pendiente

Particularidad: nunca pendiente. Si no existe registro F29 para el período, el estado es parcial (obligación abierta) en vez de pendiente (no aplica). La diferencia es semántica: la declaración mensual siempre aplica.

Sólo cierre_balance expone bloqueado_por en el contrato público. Se calcula desde ReportesRepository.checkCierreBlocker(pool, anio, mes), que verifica si el período anterior tiene cierre incompleto.

estado actualbloqueado_por_periodo_previobloqueado_por reportado
completotrue o falsenull (un cierre ya consumado no se “desbloquea”)
parcial o pendientetruemotivo_bloqueo (string del blocker)
parcial o pendientefalsenull
DelegaciónParaPor qué
ReportesService.getPpmActualizacionAnual(ctx, anio)Vista anual paso ppmEl cálculo PPM con actualización por UTM ya vive en reportes; replicarlo en cicloContable sería duplicar lógica.
ReportesRepository.getEstadoResultadosAcumuladosAnual(pool, anio)Vista anual paso resultados_acumuladosEl estado financiero/tributario de cierre lo gestiona reportes.
ReportesRepository.checkCierreBlocker(pool, anio, mes)Vista mensual paso cierre_balanceMisma lógica de bloqueo que usa el endpoint de cierre directo.

Método estático del repositorio que computa los pasos completados sin construir el detalle. Se usa para emitir eventos de cierre desde otros servicios sin pagar el costo de los detalles:

async findCierreEventPayload(
pool: Pool | PoolClient,
anio: number,
mes: number,
): Promise<CierreEventPayload>
interface CierreEventPayload {
anio: number;
mes: number;
pasosCompletados: string[];
}

Las reglas de “completo” replican exactamente las de getEstado — si divergen, el estado reportado a la UI y los eventos quedarían desincronizados. Los tests CicloContableService.unit.test.ts y CicloContableRepository.unit.test.ts cubren ambas rutas con los mismos fixtures.

CicloContableService no lanza errores propios. Propaga los del repositorio:

OrigenCausa
pool.queryErrores SQL no capturados por el repositorio (cualquier código distinto de 42P01, 3F000, 42703, 42P16).
ReportesService.getPpmActualizacionAnualErrores aguas abajo en reportes.
ReportesRepository.*Idem.

No hay ValidationError: el contrato HTTP valida anio y mes en la ruta vía express-validator antes de llegar al servicio.

ArchivoCubre
CicloContableService.unit.test.tsReglas de estado por paso con fixtures mock del repositorio.
CicloContableRepository.unit.test.tsQueries SQL contra Postgres real (test DB) y fallback 42P01.
CierreBalanceMensual.unit.test.tsDoble fuente de cierre (saldos vs balances) y precedencia.

Decisión explícita: cicloContable es agregador de estado, no contabilizador. Generar asientos desde acá rompería tres invariantes:

  1. Idempotencia de la UI: refrescar el panel no debería materializar nada.
  2. Trazabilidad: cada asiento debe nacer en su servicio de dominio (compras, depreciación, F29) con su outbox y su transacción.
  3. Tenant-tolerance: el servicio retorna pendiente cuando un esquema no existe. Si además contabilizara, fallaría duro en tenants parciales.

La contabilización vive en los dominios fuente. El ciclo sólo cuenta.