Plataforma técnica · Orchestrator
Reportes Service Support
Orchestrator Reportes
ReportesServiceSupport es una clase abstracta que contiene el motor de presentación del dominio reportes. No se instancia directamente; los 5 services del dominio (BalanceService, CierrePeriodoService, PpmService, RentaService, ResultadosAcumuladosService) la extienden para compartir la misma proyección y evitar drift entre cálculos hechos en diferentes contextos.
Extiende BaseService y compone CommonDataService para acceso a parámetros (UF, UTM, factores de corrección monetaria).
export abstract class ReportesServiceSupport extends BaseService { protected readonly commonDataService: CommonDataService; constructor(serviceName: string) { ... }
// Entry point reutilizable (mismo cuerpo que BalanceService.calcular) async calcular(ctx, anio, tipo, periodo): Promise<ServiceResult<ReporteAnual>>
// El motor protected buildBalanceClasificado(lineas: BalanceLinea[]): BalanceClasificado protected buildEstadoResultados(lineas: BalanceLinea[]): EstadoResultados protected buildRenta(raw): BalanceTributario protected mapearNombresIfrs(lineas: BalanceLinea[]): BalanceLinea[]
// Helpers PPM protected buildPpmFactorMap(factoresRaw): Map<...> protected resolvePpmFactorSourceMonth(anio, declarado): number | null protected buildPpmActualizacionAnualResumen(anio, declarados, persistidos, factoresRaw) protected roundPpmAmount(value): number}Por qué clase base y no helpers sueltos
Section titled “Por qué clase base y no helpers sueltos”Tres razones convergen:
- Composición de servicios —
CierrePeriodoService.cerrarPeriodoinvocathis.calcular(...)para obtener las líneas a persistir.ResultadosAcumuladosService.generarResultadoAcumuladotambién invocathis.calcular(...)para resolver el resultado del período. Ambos heredan en lugar de inyectar. - Consistencia obligada — si la lógica de
buildBalanceClasificadovive en un helper suelto, alguien puede saltársela y proyectar el balance con su propio loop, divergiendo. Como método protegido de la base, queda fuera del público y no se duplica. - Acceso a
commonDataService— los helpers PPM necesitan factores de corrección monetaria. Vivir en la base permite que cada subclase usethis.commonDataServicesin reinicializar.
El motor: tres builders
Section titled “El motor: tres builders”flowchart LR IN["BalanceLinea[]<br/>(filas nivel 1-4)"] BC["buildBalanceClasificado"] ER["buildEstadoResultados"] RT["buildRenta"] OUT1["BalanceClasificado<br/>(presentación A=P+P)"] OUT2["EstadoResultados<br/>(EERR)"] OUT3["BalanceTributario<br/>(base imponible)"] IN --> BC --> OUT1 IN --> ER --> OUT2 IN -. usa rentaRaw .-> RT --> OUT3
buildBalanceClasificado
Section titled “buildBalanceClasificado”Filtra solo nivel 3 (subsección) y agrupa por codigo_padre (nivel 2 anclas):
| Constante | Código nivel 2 | Sección |
|---|---|---|
SEC_ACTIVO_CTE | 1100000 | Activos corrientes |
SEC_ACTIVO_NCTE | 1200000 | Activos no corrientes |
SEC_PASIVO_CTE | 2100000 | Pasivos corrientes |
SEC_PASIVO_NCTE | 2200000 | Pasivos no corrientes |
SEC_PATRIMONIO | 2300000 | Patrimonio |
Reglas de monto por sección:
| Sección | Fórmula por línea | Por qué |
|---|---|---|
| Activos | monto = activo - pasivo | Contra-cuentas (depreciación acumulada con tipo=ACTIVO + naturaleza=C) tienen activo=0, pasivo>0 → restan |
| Pasivos | monto = pasivo | Pasivos solo informan saldo acreedor |
| Patrimonio | monto = pasivo - activo | Pérdidas acumuladas (tipo=PATRIMONIO + naturaleza=D) reducen patrimonio |
El resultado del ejercicio se suma como una pseudo-línea de patrimonio:
const resultadoEjercicio = n3.reduce( (s, l) => s + Number(l.ganancias ?? 0) - Number(l.perdidas ?? 0), 0,);// Se inyecta como línea sintética con codigo "RESULTADO_EJERCICIO"Cierre: total_activos == total_pasivos + total_patrimonio → cuadra: true. Tolerancia: redondeo entero por Math.round(Math.abs(...)).
buildEstadoResultados
Section titled “buildEstadoResultados”Mismo patrón: filtra nivel 3 por anclas nivel 2 de cuentas de resultado.
| Constante | Código nivel 2 | Bucket EERR |
|---|---|---|
SEC_ING_ORD | 4100000 | Ingresos operacionales |
SEC_ING_OTROS | 4200000 | Ingresos operacionales (otros) |
SEC_COSTO | 3100000 | Costo de ventas |
SEC_ADM | 3200000 | Gastos operacionales |
SEC_VTA | 3300000 | Gastos operacionales |
SEC_OTROS_IMP | 3700000 | Gastos operacionales (otros impuestos) |
SEC_FIN | 3400000 | Resultado no operacional |
SEC_OTROS_G | 3500000 | Resultado no operacional |
SEC_IMP_RENTA | 3600000 | Impuesto a la renta |
Cascada:
total_ingresos = SUM(ingresos.ganancias)total_costo = SUM(costo_ventas.perdidas)margen_bruto = total_ingresos - total_costo
total_gastos_op = SUM(gastosOperacionales.perdidas)resultado_operacional = margen_bruto - total_gastos_op
total_no_op = SUM(gastosNoOp.perdidas)resultado_antes_impuesto = resultado_operacional - total_no_op
impuesto_renta = gastos en SEC_IMP_RENTA si > 0 :: round(resultado_antes_impuesto * TASA_IMPUESTO) si no hay cuentas :: 0 si resultado antes de impuesto es negativo
resultado_ejercicio = resultado_antes_impuesto - impuesto_rentabuildRenta
Section titled “buildRenta”Diferente a los otros: no parte de líneas del libro diario sino de un raw de RentaRepository.calcularRenta (vista SQL que suma ventas, compras, honorarios e impuestos específicos del año).
total_ingresos = ventas_netas + nc_ventastotal_compras = compras_netas + nc_comprastotal_deducciones = total_compras + honorarios + impuestos_específicosbase_imponible = total_ingresos - total_deduccionesimpuesto_estimado = max(0, base_imponible) * TASA_IMPUESTOdiferencia = impuesto_estimado - ppmSolo redondea y compone — la heavy lifting vive en SQL.
mapearNombresIfrs
Section titled “mapearNombresIfrs”return lineas.map((l) => ({ ...l, nombre: l.nombre_ifrs ?? l.nombre,}));Reemplazo 1-a-1 cuando el plan tiene nombre_ifrs definido. Si no, usa nombre (presentación tributaria).
Helpers PPM
Section titled “Helpers PPM”Encapsulan la actualización de PPM por corrección monetaria al cierre anual. Detalle del cálculo y reglas en PpmService. Los helpers en esta base:
| Helper | Rol |
|---|---|
buildPpmFactorMap(factoresRaw) | Construye Map<mesOrigen, { factor, fuenteUrl }> filtrando solo factores con mes_destino = 12 (corrección al cierre anual). |
resolvePpmFactorSourceMonth(anio, declarado) | Decide qué mes de factor aplicar a un PPM declarado: usa fecha_pago si está, sino mes + 1 (el factor aplica desde el mes siguiente a la declaración). |
roundPpmAmount(value) | Math.round(Number(value ?? 0)) — wrapping para consistencia en montos PPM. |
buildPpmActualizacionAnualResumen | Compone la tabla 12-mes con ppm_declarado, factor, ppm_actualizado, ajuste_correccion, estado por mes + estado general (PENDIENTE/PARCIAL/CONTABILIZADO). |
Método legacy: _aplicarArrastreAnualLegacy
Section titled “Método legacy: _aplicarArrastreAnualLegacy”Marcado con _ por convención (uso interno transicional). Aplica arrastre de saldo del año anterior cuando el cálculo actual no incluye apertura. Maneja excepciones críticas:
- IVA F29 (
1108002,2103003) — no se arrastra; tiene lógica propia en el cálculo del período. - Existencias (
110900*) — workaround temporal; el módulo de inventario trae su propio saldo inicial. Excepción:1109004(existencias en tránsito) sí arrastra. - PPM por recuperar/pagar (
1108001,2202001) — no descuenta el bruto del año previo en delta. - Depreciación acumulada (
1202*connaturaleza=C) — caso especial para detectar acumulado vs movimiento.
Hoy no se invoca desde el flujo principal (la actualización por arrastre se hizo desde SQL/saldo_cuentas_cierre). Queda en la clase como referencia y para tests de comportamiento histórico.
Clasificación nivel-4: nivel4Classification.ts
Section titled “Clasificación nivel-4: nivel4Classification.ts”Función pura externa que el motor usa indirectamente (a través de _aplicarArrastreAnualLegacy y del SQL fn_balance_lineas). Decide si un saldo va a activo o pasivo:
function classifyNivel4Balance({ tipo, naturaleza, debe, haber }): { saldo_deudor: number; saldo_acreedor: number; activo: number; pasivo: number;}| Tipo cuenta | Naturaleza | Saldo va a | Notas |
|---|---|---|---|
| ACTIVO | D (deudora) | activo | Caso normal: caja, banco, deudores |
| ACTIVO | C (acreedora) | pasivo | Contra-cuenta: depreciación acumulada, estimación incobrables |
| PASIVO | C (acreedora) | pasivo | Caso normal: proveedores, deudas tributarias |
| PATRIMONIO | C (acreedora) | pasivo | Caso normal: capital, utilidades retenidas |
| PATRIMONIO | D (deudora) | ambos (activo/pasivo) | Contra-cuenta: pérdidas acumuladas (2302002) |
Las contra-cuentas conocidas viven enumeradas en INVERTED_NATURE_ACCOUNTS:
| Código | Cuenta |
|---|---|
1104002 | Estimación de deudores incobrables |
1202000 | Depreciación acumulada (genérica) |
1202100 | Dep. acum. edificios |
1202300 | Dep. acum. muebles |
1202400 | Dep. acum. vehículos |
1202500 | Dep. acum. maquinaria |
1202600 | Dep. acum. equipos computacionales |
1202700 | Dep. acum. herramientas |
2302002 | Pérdidas acumuladas |
Esta lista es la fuente de verdad de cuentas con naturaleza invertida en el plan contable. Si se agrega una contra-cuenta nueva al plan, debe registrarse aquí o la clasificación queda incorrecta.
Por qué un único calcular en la base
Section titled “Por qué un único calcular en la base”BalanceService.calcular y ReportesServiceSupport.calcular tienen el mismo cuerpo: validación de input, Promise.all([calcularLineasBalance, calcularRenta]), mapeo IFRS si aplica, construcción de los 3 builders. Está duplicado para:
- Permitir que
CierrePeriodoService.cerrarPeriodoinvoquethis.calcular(...)sin instanciarBalanceService(evita ciclo de instancias y acoplamiento al lifecycle público). - Permitir que
ResultadosAcumuladosService.generarResultadoAcumuladohaga lo mismo. - Mantener
BalanceService.calcularcomo la API estable del dominio expuesta por HTTP.
La alternativa (mover todo a la base y dejar BalanceService.calcular como return super.calcular(...)) es válida pero introduce un nivel extra de indirección sin beneficio claro. La duplicación es intencional y los tests cubren ambas rutas.