Plataforma técnica · Orchestrator
Balance Service
Orchestrator Reportes Balance
BalanceService es el entry point del motor balance. Extiende ReportesServiceSupport que aporta las funciones de construcción (buildBalanceClasificado, buildEstadoResultados, buildRenta). Este service expone dos responsabilidades:
- Cálculo on-demand — leer las líneas agregadas y proyectar el reporte completo.
- Lifecycle del reporte guardado — persistir como BORRADOR, aprobar, reversar, eliminar.
Métodos
Section titled “Métodos”| Método | Tipo | Propósito |
|---|---|---|
calcular(ctx, anio, tipo, {mes}) | Lectura | Cómputo en memoria del ReporteAnual completo. No persiste. |
guardar(ctx, anio, tipo, reporte) | Escritura | Inserta el reporte como BORRADOR en reportes.balance_guardado. |
listar(ctx) | Lectura | Lista resumida de todos los balances guardados (sin detalle). |
getById(ctx, id) | Lectura | Detalle completo de un balance guardado. |
aprobar(ctx, id) | Transición | BORRADOR → APROBADO. |
reversarAprobacion(ctx, id) | Transición | APROBADO → BORRADOR. Falla si el estado no era APROBADO. |
eliminar(ctx, id) | Destrucción | Borra. Falla si el estado no es BORRADOR. |
calcular — el motor en acción
Section titled “calcular — el motor en acción”flowchart TB
IN["calcular(anio, tipo, {mes})"]
VAL["Validate<br/>(anio 2000-2100, mes 1-12)"]
POOL["getPool(ctx.tenantDb)"]
PAR["Promise.all"]
Q1["calcularLineasBalance(anio, mes)<br/>→ fn_balance_lineas(anio, mes)"]
Q2["calcularRenta(anio, mes)<br/>→ vista de renta"]
MAP["if tipo == IFRS:<br/>mapearNombresIfrs"]
BC["buildBalanceClasificado(lineas)"]
ER["buildEstadoResultados(lineas)"]
RT["buildRenta(rentaRaw)"]
OUT["ReporteAnual"]
IN --> VAL --> POOL --> PAR
PAR --> Q1 --> MAP --> BC
MAP --> ER
PAR --> Q2 --> RT
BC & ER & RT --> OUT async calcular( ctx: ServiceContext, anio: number, tipo: "TRIBUTARIO" | "IFRS", periodo?: { mes?: number },): Promise<ServiceResult<ReporteAnual>>Validaciones:
anioentero en[2000, 2100]oValidationError("Año inválido")mesentero en[1, 12]. Default12si no se pasa (balance anual completo).
Output (ReporteAnual):
interface ReporteAnual { anio: number; mes: number; modo_periodo: "ANUAL_ACUMULADO"; // siempre acumulado desde enero tipo_presentacion: "TRIBUTARIO" | "IFRS"; lineas_balance: BalanceLinea[]; // raw nivel 1-4 con debe/haber/activo/pasivo/... balance_clasificado: BalanceClasificado; // activos cte/no-cte, pasivos, patrimonio, cuadra estado_resultados: EstadoResultados; // margen bruto → resultado del ejercicio renta: BalanceTributario; // base imponible + impuesto estimado}Diferencia entre tipos
Section titled “Diferencia entre tipos”tipo | Qué cambia |
|---|---|
TRIBUTARIO | Usa nombres del plan_contable tal cual. |
IFRS | mapearNombresIfrs reemplaza nombre por nombre_ifrs (si existe) en cada línea. La clasificación, agregación y totales no cambian — solo la etiqueta. |
La presentación IFRS no separa cuentas distintas: el plan contable es único y cada cuenta tiene un nombre_ifrs opcional. Para distinciones de fondo (criterios de reconocimiento), el modelo asume que las categorías ya generaron las líneas correctamente.
Lifecycle del reporte guardado
Section titled “Lifecycle del reporte guardado”stateDiagram-v2 [*] --> BORRADOR : guardar(reporte) BORRADOR --> APROBADO : aprobar(id) APROBADO --> BORRADOR : reversarAprobacion(id) BORRADOR --> [*] : eliminar(id) APROBADO --> APROBADO : (no se puede eliminar)
| Método | Guard |
|---|---|
guardar | Sin guard. Cualquier reporte se persiste como BORRADOR. |
aprobar | Sin guard explícito; se asume que el caller validó. |
reversarAprobacion | Falla con ValidationError("Solo se pueden reversar balances en estado APROBADO") si el repo retorna false. |
eliminar | Falla con ValidationError("Solo se pueden eliminar balances en estado BORRADOR") si el repo retorna false. |
Los guards reales viven en SQL del CierrePeriodoRepository: las queries de update y delete tienen WHERE estado = 'APROBADO' o WHERE estado = 'BORRADOR'. Si la condición no se cumple, la query afecta 0 filas y el repo retorna false — el service lo traduce a error de validación.
Persistencia
Section titled “Persistencia”guardar inserta en reportes.balance_guardado:
INSERT INTO reportes.balance_guardado (anio, tipo_presentacion, estado, total_activos, total_pasivos, total_patrimonio, base_imponible, ppm, resultado_ejercicio, detalle, calculado_por)VALUES ($1, $2, 'BORRADOR', $3, $4, $5, $6, $7, $8, $9::jsonb, $10)RETURNING *- Columnas top-level (
total_*,base_imponible,ppm,resultado_ejercicio) son indexables para listar resúmenes sin parsear el JSON. detalle JSONBguarda elReporteAnualcompleto, incluidolineas_balance[]con todas las cuentas nivel 1-4.
listar solo lee las columnas indexables + extrae mes y modo_periodo del JSON. getById deserializa detalle completo.
Errores
Section titled “Errores”| Error | Causa |
|---|---|
ValidationError("Año inválido") | `anio < 2000 |
ValidationError("Mes inválido (1-12)") | mes fuera de rango |
ValidationError("Falta configuración contable de FACT-COMP-NET...") | Propagado de BalanceRepository; el concepto FACT-COMP-NET no está parametrizado en operaciones_sii.config_conceptos_contables |
ValidationError("Solo se pueden reversar balances en estado APROBADO") | reversarAprobacion sobre un balance que no estaba APROBADO |
ValidationError("Solo se pueden eliminar balances en estado BORRADOR") | eliminar sobre un balance que no estaba en BORRADOR |
NotFoundError (vía assertExists) | getById con id inexistente |
Por qué BalanceService no abre transacciones
Section titled “Por qué BalanceService no abre transacciones”calcular es read-only puro. guardar es un único INSERT, aprobar/reversarAprobacion/eliminar son únicos UPDATE/DELETE — la atomicidad del statement basta. No hay encolado de eventos en este service.
El único lugar del dominio que usa withTransaction con outbox es CierrePeriodoService.cerrarPeriodo, porque allí sí hay efecto cross-dominio (ciclo:cierre).
Convención del campo cuadra
Section titled “Convención del campo cuadra”buildBalanceClasificado calcula:
cuadra: Math.round(Math.abs(total_activos - total_pasivos_patrimonio)) === 0,diferencia: total_activos - total_pasivos_patrimonio,cuadra: false no es un error técnico, es una señal de auditoría. El motor entrega el reporte igual y deja que el caller (UI o caller de cierre) decida si bloquear el avance. Causas habituales: líneas contables incompletas, categorías mal parametrizadas, IVA F29 no contabilizado, depreciación pendiente, costo de ventas sin asentar.