Skip to content

Plataforma técnica · Orchestrator

DJ 1887 Service

Orchestrator Declaraciones Dj 1887

DJ1887Service genera la Declaración Jurada 1887 anual: rentas pagadas a trabajadores dependientes y retenciones de Impuesto Único Segunda Categoría (IUSC). Lee remuneraciones.liquidaciones en estado PAGADA, calcula la renta tributable real (con tope salud 60 UF), consolida por RUT, aplica corrección monetaria mensual y persiste header + detalle.

Estructuralmente paralela a DJ1879Service, pero con tres complejidades extra:

  1. Cálculo del tope de salud (60 UF × 7%) por liquidación.
  2. Horas semanales pactadas por trabajador a diciembre (campo C35 del CSV).
  3. CSV de 56 columnas con dos secciones (B + C) — formato SII AT 2026.
MétodoPara
generar(ctx, año)Genera (o regenera borrador) la DJ del año. Emite dj:1887_generada.
getByAño(ctx, año)Header por año.
getWithDetalle(ctx, id)Header + detalle por trabajador.
list(ctx)Lista todas las DJ del tenant.
validar(ctx, id)BORRADOR → VALIDADO. Emite dj:1887_status_cambiado.
generateCSV(ctx, declaracionId)Exporta CSV multi-sección (B + C, 56 cols, 12 meses).
flowchart TB
  IN["generar(año)"]
  TX["withTransaction"]
  EX["findByAño + delete BORRADOR previo"]
  DECL["getDatosDeclarante()"]
  PAR["Promise.all en paralelo:<br/>- PayrollRepository.list(estado=PAGADA, limit=10000)<br/>- getHorasSemanalesPorRut(año)<br/>- getCorreccionMonetaria(año)<br/>- 12× getIndicators(YYYY-MM-01)"]
  UFMAP["Map<mes, uf> del último día de cada mes"]
  MAP["mapear cada liquidación →<br/>LiquidacionConsolidada<br/>(con tope salud 60 UF)"]
  CHK{"liquidaciones.length === 0?"}
  ERR["Error 'No hay liquidaciones PAGADAS'"]
  FCT["buildFactoresMap(corrección)"]
  CONS["consolidarPorTrabajador<br/>(agrupa por RUT,<br/>renta × factor[mes])"]
  TOT["calcularTotales"]
  HDR["insertHeader"]
  DET["por trabajador: insertDetalle<br/>(+ horas semanales)"]
  EMIT["outbox.queue(dj:1887_generada)"]
  COMMIT["COMMIT"]

  IN --> TX --> EX --> DECL --> PAR --> UFMAP --> MAP --> CHK
  CHK -- sí --> ERR
  CHK -- no --> FCT --> CONS --> TOT --> HDR --> DET --> EMIT --> COMMIT

Cada liquidación se transforma en una LiquidacionConsolidada con renta tributable calculada según las normas vigentes:

Cálculo por liquidación
// Por cada liquidación de la lista (orden mensual):
const uf = ufPorMes.get(mes) ?? 0; // UF último día del mes
const totalImponible = Number(r.total_imponible || 0);
// Tope salud: redondear(min(imponible, 60 UF) × 7%)
const salud = uf > 0
? Math.round(Math.min(totalImponible, 60 * uf) * 0.07)
: Math.round(totalImponible * 0.07);
const afp = Number(r.descuento_afp || 0);
const afc = Number(r.descuento_seguro_cesantia || 0);
const impuestoUnico = Number(r.descuento_impuesto || 0);
// Renta Neta Tributable = Imponible - AFP - min(Salud, 7%×60UF) - AFC - IUSC
const total_tributable = totalImponible - afp - salud - afc - impuestoUnico;
const renta_no_gravada = Number(r.movilizacion || 0)
+ Number(r.colacion || 0)
+ Number(r.asignacion_familiar || 0);

Por qué el tope salud en el cálculo de la DJ

Section titled “Por qué el tope salud en el cálculo de la DJ”

La salud real (descuento_isapre) ya viene en la liquidación, pero para efectos del IUSC la base imponible se calcula con el tope legal (60 UF × 7%), no con lo que el trabajador efectivamente pagó. Si está en Isapre cara (digamos 9% sobre un sueldo alto), el exceso sobre el 7% no es deducible para impuestos.

Casosalud aplicada
Imponible ≤ 60 UFimponible × 7%
Imponible > 60 UF60 UF × 7% (cap)
UF del mes no disponible (caso defensivo)imponible × 7% (sin cap — log warning conceptual)

Movilización, colación y asignación familiar (Art. 17 N°14 LIR) son ingresos no gravables. Se reportan aparte en C6 (Sección B) y C4’ (Sección C) del CSV.

Agrupa liquidaciones por rut y aplica factor de corrección por mes:

Campo del consolidadoCálculo
meses[]Lista de meses pagados (puede repetir si hubo varias liquidaciones).
totalImponibleΣ liq.total_imponible
rentaSinActualizarΣ liq.total_tributable
rentaActualizadaΣ round(total_tributable × factor[mes])
iuscSinActualizarΣ liq.descuento_impuesto
iuscActualizadoΣ round(descuento_impuesto × factor[mes])
leyesSocialesΣ afp + saludReal + afc (descuentos efectivos)
rentaNoGravadaΣ liq.renta_no_gravada
detalleMensual[]Un row por mes con factor_actualizacion, sin actualizar y actualizada.

buildFactoresMap filtra factores a mes_destino = 12; meses sin factor → 1. Misma lógica que DJ1879.

C35 del CSV (Sección B) es las horas semanales pactadas a diciembre. Cascade de resolución:

flowchart TB
  IN["resolveHorasSemanales(map, rut, fallback?)"]
  D["map.get(rut)"]
  DOK{"valor finito y > 0?"}
  R1["return round(valor)"]
  N["normalizeRutLookup<br/>(uppercase + remover . y -)"]
  M2["map.get(normalized)"]
  NOK{"finito y > 0?"}
  R2["return round(valor)"]
  F["fallback finito y > 0?"]
  R3["return round(fallback)"]
  R4["return 45<br/>(default por defecto)"]

  IN --> D --> DOK
  DOK -- sí --> R1
  DOK -- no --> N --> M2 --> NOK
  NOK -- sí --> R2
  NOK -- no --> F
  F -- sí --> R3
  F -- no --> R4

El SII espera 99 para gerentes / sin fiscalización / sin contrato activo (caso especial). El CSV lo aplica:

bCols.push(String(horas > 0 ? Math.round(horas) : 99)); // C35

generateCSV(ctx, declaracionId) — formato SII F1887 AT 2026

Section titled “generateCSV(ctx, declaracionId) — formato SII F1887 AT 2026”

Multi-sección: el CSV tiene dos tipos de fila, identificadas por un primer indicador:

  • Indicador 1 = Sección B (montos actualizados a diciembre). 35 columnas + 20 vacíos.
  • Indicador 2 = Sección C (montos sin actualizar). 35 vacíos + 20 columnas.

Total: 56 campos por fila. Separador ;. Sin encabezados. Sin decimales.

Primero todas las filas Sección B (una por trabajador), luego todas las Sección C.

ColContenido
C1RUT sin DV (stripDV).
C2DV del RUT (extractDV0-9 o K).
C3Renta total neta pagada actualizada (Art.42 N°1 LIR).
C4IUSC retenido actualizado.
C5Mayor retención solicitada (Art. 88 LIR) — hoy siempre 0.
C6Renta no gravada total actualizada.
C7Renta exenta — hoy siempre 0.
C83% préstamo tasa 0% año 2020-2021 — siempre 0.
C9Rebaja por zonas extremas (DL 889).

Sección C (20 cols, montos sin actualizar)

Section titled “Sección C (20 cols, montos sin actualizar)”
ColContenido
C1’Renta total neta sin actualizar.
C2’IUSC sin actualizar (rentas normales del año).
C3’IUSC sin actualizar (rentas accesorias ene-abr año siguiente) — hoy 0.
C4’Renta no gravada actualizada (noRentaTotalActualizada).
C5’Renta exenta sin actualizar — hoy 0.
C6’Rebaja zonas extremas sin actualizar.
C7’Leyes sociales (AFP + Salud real + AFC).
C8’3% préstamo sin actualizar — siempre 0.
C9’–C20’Rentas mensuales sin actualizar Ene–Dic.
BORRADOR → VALIDADO → DECLARADO (manual)

Igual que DJ 1879. validar() es idempotente (no-op si ya está validado) y emite dj:1887_status_cambiado solo cuando hay cambio real.

EventoCuándoPayload
dj:1887_generadagenerar post-COMMIT{ declaracionId, anio, totalTrabajadores, totalRentaActualizada, totalIUSC }
dj:1887_status_cambiadovalidar cuando cambia de estado{ declaracionId, anio, estado, prevEstado }

PayrollRepository.list(tenantPool, { año, estado: 'PAGADA', limit: 10000 }) — lectura cross-domain, usa el pool del tenant (no el client de TX). El cap limit: 10000 es defensivo (asume menos de 10k liquidaciones por tenant/año).

Si una empresa tiene más, el CSV quedará incompleto sin error visible. Para tenants grandes, considerar paginar o subir el cap.

MétodoRutaService call
POST/api/declaraciones-juradas/1887/generar/:añogenerar(ctx, año)
GET/api/declaraciones-juradas/1887list(ctx)
GET/api/declaraciones-juradas/1887/año/:añogetByAño(ctx, año)
GET/api/declaraciones-juradas/1887/:idgetWithDetalle(ctx, id)
POST/api/declaraciones-juradas/1887/:id/validarvalidar(ctx, id)
GET/api/declaraciones-juradas/1887/:id/csvgenerateCSV(ctx, id) (text/csv)
CasoComportamiento
Año sin liquidaciones PAGADALanza Error('No hay liquidaciones PAGADAS en X para generar DJ 1887').
Liquidación con total_imponible = 0Aporta 0 a tributable; sigue contando como mes con renta.
UF del mes faltante en nostromo_common.indicadoresSalud se calcula sin tope (imponible × 7%).
Factor de corrección faltanteUsa factor 1.
Trabajador sin horas_semanales mapeadasUsa 45 (jornada completa default); el CSV traduce a 99 si es 0.
Trabajador con múltiples liquidaciones en el mismo mesAparece duplicado en meses[]; suma todo al consolidado.