Skip to content

Plataforma técnica · Orchestrator

F29 Generator Service

Orchestrator Declaraciones F29

F29GeneratorService es el engine de cálculo del Formulario 29. Reemplaza al stored procedure declaraciones.sp_generar_f29 con lógica TypeScript testeable. Su función única es: dado un período y RUT, leer las fuentes operacionales, calcular cada línea del F29, persistir header + detalle y emitir f29:generada.

No expone consultas, listados ni transiciones de estado — esas viven en DeclaracionesService.

MétodoAbre TXEmite hookCuándo usarlo
generate(ctx, params)f29:generadaGeneración standalone (handler HTTP, job programado).
generateInTransaction(ctx, params, client)Cuando el caller ya tiene TX abierta y quiere componer.

Ambos delegan internamente en generateWithClient (privado).

DeclaracionesService.registrarAjusteBajaRemanente usa generateInTransaction porque regenera el F29 dentro de su propia TX de aplicación de ajuste — no necesita un segundo evento f29:generada (el caller decide si emite algo distinto).

flowchart TB
  IN["generate(periodo, rut)"]
  VAL["validateRequired<br/>(periodo, rut_contribuyente)"]
  TX["withTransaction"]
  CALC["calculateValues<br/>(7 fuentes en paralelo + lógica)"]
  DEL["DELETE F29 BORRADOR previo<br/>del mismo período+rut"]
  INS["INSERT header<br/>(con remanente efectivo post-ajuste)"]
  LINES["insertDetailLines<br/>(líneas 15, 16, 31, 40, 41, 89, 89B,<br/>114, 115, 142, 151, 152, 154, 502...)"]
  EMIT["outbox.queue<br/>(f29:generada)"]
  COMMIT["COMMIT → flushOutbox"]

  IN --> VAL --> TX --> CALC --> DEL --> INS --> LINES --> EMIT --> COMMIT

calculateValues — composición de fuentes

Section titled “calculateValues — composición de fuentes”

Lee 7 grupos de datos y los combina en un objeto F29Calculations:

LíneaConceptoOrigen
15Ventas netas afectascvd con conceptos VENTA-PRD-NET, VENTA-BOL-NET, NC-VENTA-NET, FACT-COMP-NET.
16Ventas exentascvd con concepto VENTA-EXE-NET.
31Débito fiscal del períodoIVA ventas (VENTA-PRD-IVA, NC-VENTA-IVA) + IVA boletas (VENTA-BOL-IVA).

Todas las queries excluyen cvd.estado = 'ANULADO'.

Una vez calculadas las fuentes, las operaciones son aritméticas directas:

CálculoFórmula
ivaDeterminadototalDebito - totalCredito - remanenteAnteriorEfectivo
remanenteSiguienteSi ivaDeterminado < 0: abs(ivaDeterminado). Si ≥ 0: 0.
totalAPagarmax(0, ivaDeterminado) + retencionesHonorarios + impuestoUnico + ppm

Una vez calculado todo, se insertan las líneas del formulario en declaraciones_f29_detalle. La función helper insertDetail:

if (monto === 0 && !['15', '31', '40'].includes(codigoLinea)) return;

Las líneas de ventas netas (15), débito fiscal (31) y crédito fiscal compras (40) se insertan siempre, incluso si son cero — son las que el SII espera ver explícitas. El resto se omite si su monto es cero.

Líneas que se generan (no exhaustivo):

CódigoDescripción
15Total Ventas Netas
16Total Ventas Exentas
31Débito Fiscal del período
34IVA retenido (facturas de compra)
40Crédito Fiscal compras
41Crédito Fiscal activo fijo
89Remanente anterior actualizado
89BAjuste baja remanente SII
114Impuesto único trabajadores
115(idem agregado)
142PPM ventas y servicios
151Total honorarios pagados
152(agregado)
154Retenciones honorarios
502IVA determinado / Remanente siguiente
573Actualización del remanente anterior por UTM

(La lista exacta vive en insertDetailLines; esta tabla es referencial.)

EventoCuándoPayload
f29:generadagenerate() post-COMMIT{ declaracionId, periodo: { anio, mes }, rutContribuyente }

No se emite desde generateInTransaction(client) — el caller decide. Detalle en index › Hooks emitidos.

ErrorCausa
ValidationError("Formato de periodo inválido. Use YYYY-MM.")El DeclaracionesService.generateF29 lo lanza antes de llamar.
ValidationError("Configuración de empresa no encontrada para el tenant actual.")CompanyRepository.getCurrent retornó null.
ValidationError("La empresa <rut> no tiene régimen tributario configurado...")empresa.regimen_id == null y no hay PPM estratégico.
ValidationError("No se pudo determinar tasa PPM (Régimen ID: X, Año: Y, ...)")getPpmScale retornó null (no hay escala para esa combinación).
PG 23505 (unique violation)Ya existe F29 NO-borrador para el período. Lo traduce DeclaracionesService.

sp_generar_f29.sql quedó en la base como referencia histórica pero no se invoca. La razón del rewrite a TS:

  • Testabilidad: este servicio se cubre con unit tests usando mocks de CommonDataService y queries fixture. El SP requería un Postgres con todas las tablas seedeadas.
  • Composición: generateInTransaction(client) permite componer la generación dentro de la TX del ajuste de baja — imposible con un SP que abre su propia TX.
  • Errores explícitos: ValidationError con mensajes accionables en vez de RAISE EXCEPTION con códigos numéricos.
  • Lógica nueva (corrección UTM con cap, ajuste baja remanente, PPM estratégico vs régimen) es más legible en TS.