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:
- Cálculo del tope de salud (60 UF × 7%) por liquidación.
- Horas semanales pactadas por trabajador a diciembre (campo C35 del CSV).
- CSV de 56 columnas con dos secciones (B + C) — formato SII AT 2026.
Operaciones
Section titled “Operaciones”| Método | Para |
|---|---|
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). |
Flujo de generar(año)
Section titled “Flujo de generar(año)”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 Cálculo de Renta Tributable Real
Section titled “Cálculo de Renta Tributable Real”Cada liquidación se transforma en una LiquidacionConsolidada con renta tributable calculada según las normas vigentes:
// Por cada liquidación de la lista (orden mensual):const uf = ufPorMes.get(mes) ?? 0; // UF último día del mesconst 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 - IUSCconst 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.
| Caso | salud aplicada |
|---|---|
| Imponible ≤ 60 UF | imponible × 7% |
| Imponible > 60 UF | 60 UF × 7% (cap) |
| UF del mes no disponible (caso defensivo) | imponible × 7% (sin cap — log warning conceptual) |
Renta no gravada
Section titled “Renta no gravada”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.
consolidarPorTrabajador — algoritmo
Section titled “consolidarPorTrabajador — algoritmo”Agrupa liquidaciones por rut y aplica factor de corrección por mes:
| Campo del consolidado | Cá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.
Horas Semanales — resolveHorasSemanales
Section titled “Horas Semanales — resolveHorasSemanales”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)); // C35generateCSV(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.
Sección B (35 cols, montos actualizados)
Section titled “Sección B (35 cols, montos actualizados)”| Col | Contenido |
|---|---|
| C1 | RUT sin DV (stripDV). |
| C2 | DV del RUT (extractDV — 0-9 o K). |
| C3 | Renta total neta pagada actualizada (Art.42 N°1 LIR). |
| C4 | IUSC retenido actualizado. |
| C5 | Mayor retención solicitada (Art. 88 LIR) — hoy siempre 0. |
| C6 | Renta no gravada total actualizada. |
| C7 | Renta exenta — hoy siempre 0. |
| C8 | 3% préstamo tasa 0% año 2020-2021 — siempre 0. |
| C9 | Rebaja por zonas extremas (DL 889). |
12 columnas, una por mes (Ene→Dic). Cada celda:
- Vacía si el trabajador no tuvo renta ese mes.
- Código de tipo de jornada si sí tuvo:
A= AgrícolaB= Sin definirC= No agrícola completa (default)P= ParcialG= Gerente
Default si no se especifica: C.
| Col | Contenido |
|---|---|
| C22 | Número de certificado (numero_certificado o numero_correlativo). |
| C23–C34 | Monto ingreso mensual sin actualizar Ene–Dic (= mensual histórico). |
| C35 | Horas semanales pactadas a diciembre (99 si no aplica). |
Sección C (20 cols, montos sin actualizar)
Section titled “Sección C (20 cols, montos sin actualizar)”| Col | Contenido |
|---|---|
| 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. |
Estados
Section titled “Estados”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.
Hooks emitidos
Section titled “Hooks emitidos”| Evento | Cuándo | Payload |
|---|---|---|
dj:1887_generada | generar post-COMMIT | { declaracionId, anio, totalTrabajadores, totalRentaActualizada, totalIUSC } |
dj:1887_status_cambiado | validar cuando cambia de estado | { declaracionId, anio, estado, prevEstado } |
Dependencias cruzadas con payroll
Section titled “Dependencias cruzadas con payroll”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.
Endpoints
Section titled “Endpoints”| Método | Ruta | Service call |
|---|---|---|
POST | /api/declaraciones-juradas/1887/generar/:año | generar(ctx, año) |
GET | /api/declaraciones-juradas/1887 | list(ctx) |
GET | /api/declaraciones-juradas/1887/año/:año | getByAño(ctx, año) |
GET | /api/declaraciones-juradas/1887/:id | getWithDetalle(ctx, id) |
POST | /api/declaraciones-juradas/1887/:id/validar | validar(ctx, id) |
GET | /api/declaraciones-juradas/1887/:id/csv | generateCSV(ctx, id) (text/csv) |
Gotchas
Section titled “Gotchas”| Caso | Comportamiento |
|---|---|
Año sin liquidaciones PAGADA | Lanza Error('No hay liquidaciones PAGADAS en X para generar DJ 1887'). |
Liquidación con total_imponible = 0 | Aporta 0 a tributable; sigue contando como mes con renta. |
UF del mes faltante en nostromo_common.indicadores | Salud se calcula sin tope (imponible × 7%). |
| Factor de corrección faltante | Usa factor 1. |
Trabajador sin horas_semanales mapeadas | Usa 45 (jornada completa default); el CSV traduce a 99 si es 0. |
| Trabajador con múltiples liquidaciones en el mismo mes | Aparece duplicado en meses[]; suma todo al consolidado. |