Skip to content

Plataforma técnica · Orchestrator

Declaraciones Service

Orchestrator Declaraciones F29

DeclaracionesService orquesta el ciclo de vida del F29. No calcula valores (eso es del F29GeneratorService); coordina generación, lectura, transición de estados con guardas, ajuste de baja de remanente y regeneración de asientos contables anuales.

MétodoPara
generateF29(ctx, params)Genera o regenera borrador. Delega cálculo a F29GeneratorService.
list(ctx, filter)Lista headers F29 con filtros (período/año/mes/rut/estado).
getById(ctx, id) / getByPeriod(ctx, periodo, rut)Recupera F29 completo (header + detalles).
updateStatus(ctx, id, estado)Transición de estado con guardas y efectos contables (ver abajo).
delete(ctx, id)Borrado físico dentro de TX.
registrarAjusteBajaRemanente(ctx, f29Id, input)Aplica baja de remanente notificada por SII, regenera F29, emite asiento.
eliminarAjusteBajaRemanente(ctx, f29Id)Revierte el ajuste, regenera F29.
getAjusteBajaRemanente(ctx, f29Id)Recupera el ajuste vigente del F29.
regenerarAsientosAnual(ctx, anio)Re-emite asientos contables declaraciones.asientos_f29 para el año.

Valida formato del período (YYYY-MM), delega a f29Generator.generate(ctx, params) que abre su propia TX y emite f29:generada post-COMMIT. Después lee el F29 completo con getFull(pool, id) y lo retorna.

Sobrescritura controlada: si ya existe un F29 con estado VALIDADO, DECLARADO o ANULADO para el mismo período/RUT, el INSERT del generator viola la UNIQUE constraint (23505) y este service traduce a:

ValidationError("Ya existe una declaración (Validada, Declarada o Anulada)
para este período. No se puede sobrescribir con un borrador.")

Solo borradores se pueden sobrescribir libremente (el generator hace DELETE WHERE estado = 'BORRADOR' antes del INSERT).

updateStatus(ctx, id, estado) — máquina de estados con efectos

Section titled “updateStatus(ctx, id, estado) — máquina de estados con efectos”

Normaliza nombres femeninos legacy (DECLARADADECLARADO, etc.) y valida contra ['BORRADOR', 'VALIDADO', 'DECLARADO', 'ANULADO', 'RECTIFICADO'].

flowchart TB
  IN["updateStatus(id, estado)"]
  NORM["normalizar enum<br/>(DECLARADA → DECLARADO)"]
  VAL["validar estado ∈<br/>{BORRADOR, VALIDADO, DECLARADO,<br/>ANULADO, RECTIFICADO}"]
  TX["withTransaction"]
  FIND["findById(id)"]
  GUARD{"estado actual = DECLARADO<br/>y target ∈ {BORRADOR, VALIDADO, ANULADO}?"}
  CHECK["existsIvaPagadoInPeriod(año, mes)"]
  REJ["ValidationError<br/>'No se puede revertir: IVA en PAGADO'"]
  UPD["updateStatus(id, target, userId)"]
  ROLLB{"venía de DECLARADO?"}
  REVIVA["revertirIvaPeriodo<br/>(PAGADO → CONTABILIZADO)"]
  REGAN["generarAsientosF29Anual"]
  TODECL{"target = DECLARADO?"}
  MARK["contabilizarIvaPeriodo<br/>(CONTABILIZADO → PAGADO)"]
  REGAN2["generarAsientosF29Anual"]
  EMIT["outbox.queue<br/>(f29:status_cambiado)<br/>solo si cambia"]
  COMMIT["COMMIT"]

  IN --> NORM --> VAL --> TX --> FIND --> GUARD
  GUARD -- sí --> CHECK
  CHECK -- existe pagado --> REJ
  CHECK -- todos CONTABILIZADO --> UPD
  GUARD -- no --> UPD
  UPD --> ROLLB
  ROLLB -- sí --> REVIVA --> REGAN --> TODECL
  ROLLB -- no --> TODECL
  TODECL -- sí --> MARK --> REGAN2 --> EMIT
  TODECL -- no --> EMIT
  EMIT --> COMMIT
PasoTabla afectada
contabilizarIvaPeriodooperaciones_sii.compras_ventas_detalle (CONTABILIZADO → PAGADO en el período)
generarAsientosF29Anualdeclaraciones.asientos_f29 (regenera para el año del F29)

Solo permite volver a BORRADOR/VALIDADO/ANULADO si todas las líneas IVA del período están en CONTABILIZADO (ninguna PAGADO). Si hay pagado, lanza:

ValidationError("No se puede revertir el F29: existen IVA en estado PAGADO
para el periodo. Solo se permite revertir cuando todos están en CONTABILIZADO.")

Cuando el SII notifica que el remanente declarado en un período fue reducido (rechazo parcial), el contador debe registrar la baja. El servicio:

  1. Valida que el F29 esté en BORRADOR o VALIDADO (un DECLARADO se debe revertir primero).
  2. Valida que el monto sea positivo y no exceda el remanenteArrastradoOriginal (remanente anterior + ajuste previo si lo hubo).
  3. Si está VALIDADO, lo regresa a BORRADOR (la regeneración va a sobreescribirlo).
  4. Upsert el ajuste en declaraciones.ajustes_remanente con un cuenta_gasto_codigo (default 3701040).
  5. Regenera el F29 con F29GeneratorService.generateInTransaction(client) (sin emitir hook — el caller decide).
  6. Re-upsert el ajuste asociándolo al nuevo f29Id.
  7. Emite asiento contable en declaraciones.asientos_f29:
    • Debe: cuenta de gasto del ajuste (3701040).
    • Haber: 1108002 (IVA Crédito Fiscal).
    • Concepto: AJUSTE_BAJA_REMANENTE_<periodo>.
sequenceDiagram
  autonumber
  participant H as Handler
  participant DS as DeclaracionesService
  participant F29G as F29GeneratorService
  participant Repo as Repositories
  participant DB as PostgreSQL

  H->>DS: registrarAjusteBajaRemanente(f29Id, { monto, motivo })
  DS->>Repo: findById(f29Id) + validaciones
  alt VALIDADO
    DS->>Repo: updateStatus(f29Id, 'BORRADOR')
  end
  DS->>Repo: AjusteRemanenteRepository.upsert(f29Id, datos)
  DS->>F29G: generateInTransaction(client, params)
  F29G->>DB: regenerar F29 (header + detalle)
  F29G-->>DS: nuevo f29Id
  DS->>Repo: AjusteRemanenteRepository.upsert(nuevoF29Id, datos)
  DS->>DB: DELETE asientos_f29 con concepto AJUSTE_BAJA_REMANENTE_<periodo>
  DS->>DB: INSERT 2 filas (cargo gasto, abono IVA crédito)
  DS->>DB: COMMIT
  DS-->>H: F29Full actualizado

Eliminación (eliminarAjusteBajaRemanente) es simétrica: valida estado, borra el ajuste y los asientos, regenera el F29 sin ajuste.

Para uso administrativo: re-genera todos los declaraciones.asientos_f29 del año a partir de los F29 existentes en estado DECLARADO. Falla si no hay ningún F29 declarado en el año (ValidationError).

Útil cuando se modifica la fórmula de asientos o se necesita reconstruir el libro mayor desde cero por inconsistencia.

TargetPermitidoEfecto adicional
VALIDADOSolo cambio de estado.
DECLARADO+ contabiliza IVA + asientos.
ANULADOSolo cambio de estado.
RECTIFICADOSolo cambio de estado.
EventoCuándoPayload
f29:status_cambiadoupdateStatus cuando normalizedEstado !== estadoActual{ declaracionId, periodo: { anio, mes }, estado, prevEstado }

No emite en delete, registrarAjusteBajaRemanente ni eliminarAjusteBajaRemanente — esas operaciones invocan F29GeneratorService.generateInTransaction que tampoco emite (por diseño, ver nota en index).

MétodoRutaService call
POST/api/declaraciones/f29/generategenerateF29(ctx, params)
GET/api/declaraciones/f29list(ctx, filter)
GET/api/declaraciones/f29/:idgetById(ctx, id)
GET/api/declaraciones/f29/periodo/:periodo/:rutgetByPeriod(ctx, periodo, rut)
PATCH/api/declaraciones/f29/:id/statusupdateStatus(ctx, id, estado)
DELETE/api/declaraciones/f29/:iddelete(ctx, id)
POST/api/declaraciones/f29/:id/ajuste-remanenteregistrarAjusteBajaRemanente(...)
DELETE/api/declaraciones/f29/:id/ajuste-remanenteeliminarAjusteBajaRemanente(ctx, id)
GET/api/declaraciones/f29/:id/ajuste-remanentegetAjusteBajaRemanente(ctx, id)
POST/api/declaraciones/f29/regenerar-asientos/:anioregenerarAsientosAnual(ctx, anio)