Skip to content

Plataforma técnica · Orchestrator

Payroll Service

Remuneraciones Liquidaciones Leyes Sociales

PayrollService es la pieza central del dominio de remuneraciones del Orchestrator. Coordina la hidratación de datos, la ejecución del motor de cálculo y la persistencia de la liquidación. El dominio separa el cálculo puro (PayrollEngine y sus calculadoras) del acceso a datos (PayrollRepository) y de la orquestación transaccional (este service).

La división permite probar el motor con inputs sintéticos sin tocar base de datos, y permite que el service maneje todo lo que es side-effects: leer contexto, persistir, encolar eventos para emisión post-COMMIT.

  • Directoryorchestrator/src/domain/payroll/
    • Directorycalculators/
      • BaseSalaryCalculator.ts
      • GratificationCalculator.ts
      • HealthPlanCalculator.ts
      • ProrrataCalculator.ts
      • SocialLawsCalculator.ts
      • TaxCalculator.ts
    • PayrollEngine.ts
    • PayrollInputBuilder.ts
    • PayrollRepository.ts
    • PayrollService.ts
    • pdf-template.ts
    • types.ts
CapaArchivoRol
ServicePayrollService.tsAplicación: valida, hidrata contexto, orquesta, gestiona transacción y outbox. Hereda BaseService.
EnginePayrollEngine.tsFunción pura calculate(input) determinista. Sin DB.
Input BuilderPayrollInputBuilder.tsConstrucción fluida del PayrollInput (~30 campos).
RepositoryPayrollRepository.tsAcceso a PostgreSQL: contexto, persistencia, consultas.
Calculatorscalculators/Reglas específicas, cada una aislada y testeable.
Typestypes.tsContractType, HealthMode, GratificationMode.
MétodoModoResultado
previewPayroll(ctx, contractId, period, overrides?)LecturaCalcula sin persistir. Devuelve PayrollResult.
generatePayroll(ctx, contractId, period, options?)Escritura transaccionalCalcula y persiste. Acepta dryRun, force, overrides.
deletePayroll(ctx, id, force?)EscrituraBorra liquidación. APROBADA/PAGADA requieren force.
updateFileUrl(ctx, id, url)EscrituraAsocia URL del PDF.
updateStatus(ctx, id, estado)Escritura transaccionalCambia estado y encola payroll:approved si pasa a APROBADA.

previewPayroll y generatePayroll aceptan un objeto de overrides que reemplaza valores del contexto antes de invocar al motor. Útil para simulaciones y ajustes manuales:

interface PayrollOverrides {
sueldo_base?: number;
horas_semanales?: number;
dias_trabajados?: number;
dias_ausencia?: number;
horas_extra_50?: number;
horas_extra_100?: number;
bono_produccion?: number;
bono_imponible?: number;
gratification_type?: GratificationMode;
colacion?: number;
movilizacion?: number;
otros_no_imponibles?: number;
anticipo?: number;
descuento_voluntario?: number;
prestamo_caja?: number;
apv?: number;
health_mode?: HealthMode;
plan_salud_clp?: number;
plan_salud_uf?: number;
base_days?: number;
}

Si un override no se envía, el service usa el valor del contexto (contract, attendance, etc.).

PayrollEngine.calculate(input: PayrollInput) ejecuta los pasos en orden:

  1. Sueldo base prorrateadoBaseSalaryCalculator ajusta por días trabajados sobre base_days (default 30), con piso en el sueldo mínimo.

  2. Horas extra — valor hora con factor Art. 32 + recargos 50% y 100% según attendance.horas_extra_*.

  3. GratificaciónGratificationCalculator aplica modo configurado: 25% Art. 50, abono anual, exento o sin gratificación.

  4. Total imponible — suma de sueldo, gratificación, bonos y horas extra. Base de cotizaciones.

  5. Leyes socialesSocialLawsCalculator calcula AFP, salud, AFC y aportes patronales respetando topes UF.

  6. Impuesto únicoTaxCalculator aplica tabla SII sobre base tributable (imponible menos descuentos legales menos APV régimen B).

  7. Haberes no imponiblesProrrataCalculator prorratea colación y movilización por días trabajados.

  8. Descuentos y líquido — suma descuentos legales, impuesto, anticipos, préstamos y voluntarios. Líquido = haberes − descuentos.

CalculadoraResponsabilidad
BaseSalaryCalculatorSueldo prorrateado por días trabajados con piso en sueldo mínimo.
GratificationCalculatorArt. 50: 25%, abono anual fijo, exento o sin gratificación.
SocialLawsCalculatorAFP, salud, AFC, SIS, mutual y aportes patronales con topes.
TaxCalculatorImpuesto único de segunda categoría sobre base tributable.
HealthPlanCalculatorFonasa 7% vs. Isapre: mayor entre 7% y plan UF pactado.
ProrrataCalculatorProrrateo mensual de colación/movilización.

calculateAfcRates(contractType) ajusta las tasas antes de construir el input:

tipo_contratoTrabajadorEmpleadorIndemnización a todo evento
INDEFINIDO0,6 %2,4 %
PLAZO_FIJO3,0 %
CASA_PARTICULAR3,0 %1,11 %
ModoCálculo
FONASA7% sobre imponible topeado. Sin diferencia adicional.
ISAPREMayor entre 7% legal y el plan UF pactado (convertido a CLP con UF del periodo).

Los topes vienen en UF (pensionCap, unemploymentCap). El service multiplica por la UF del periodo y redondea:

const pensionCapCLP = Math.round(social_laws_rates.pensionCap * uf);
const afcCapCLP = Math.round(social_laws_rates.unemploymentCap * uf);

AFP, salud y SIS comparten pensionCap; AFC usa unemploymentCap (mayor).

BORRADOR → CALCULADA → APROBADA → PAGADA
ANULADA
EstadoSignificado
BORRADOREn preparación.
CALCULADAGenerada y persistida por generatePayroll.
APROBADAValidada por usuario. Emite payroll:approved post-COMMIT.
PAGADAPago registrado. fecha_pago = NOW() automático.
ANULADASin efecto.

savePayroll hace upsert de cabecera (UNIQUE(empleado_id, año, mes)) y reescribe detalle por concepto. Liquidaciones APROBADA o PAGADA no se sobrescriben sin force=true.

updateStatus:

  • Hace SELECT ... FOR UPDATE para serializar transiciones.
  • Marca fecha_aprobacion = NOW() al pasar a APROBADA (idempotente).
  • Marca fecha_pago = NOW() al pasar a PAGADA (idempotente).
EventoTriggerPayload
payroll:approvedupdateStatus(_, _, "APROBADA") cuando el estado anterior no era APROBADA.{ payrollId, employeeId, period: { year, month }, totalLiquid }
return this.withTransaction(ctx, async (client, outbox) => {
// ... validaciones + UPDATE ...
if (estado === "APROBADA" && row.estado !== "APROBADA") {
outbox.queue("payroll:approved", { ... });
}
});
OrigenMensajeCuándo
validateRequired"<campo> es requerido"Falta contractId o period.
assertExists"PayrollContext <id> not found"Contrato sin contexto válido para el periodo.
ServicenotFound("Liquidación no encontrada")deletePayroll/updateStatus con id inexistente.
ServicebadRequest("Estado inválido. Valores permitidos: ...")updateStatus con valor fuera del enum.
Repository"No encontrada" / otrosdeleteById con restricciones (estado protegido).
const service = new PayrollService();
// Preview sin persistir
const preview = await service.previewPayroll(
ctx,
"contract-uuid-123",
new Date("2026-05-01"),
{ horas_extra_50: 5 },
);
// Generar y persistir (transaccional)
const generated = await service.generatePayroll(
ctx,
"contract-uuid-123",
new Date("2026-05-01"),
{ dryRun: false, force: false },
);
// Aprobar (encola payroll:approved post-COMMIT)
await service.updateStatus(ctx, generated.id, "APROBADA");
ServiceCómo consume
PrevisionsServiceLee liquidaciones del periodo (getAFPWorkerAggregates, getHealthWorkerAggregates, etc.) para consolidar imposiciones.
FiniquitoServiceLee última remuneración del trabajador para determinar base indemnizatoria.
DJ1887ServiceConsume liquidaciones definitivas anuales para alimentar la declaración.
F29GeneratorServiceSuma impuesto_unico del periodo para la línea de retenciones.