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.
Estructura del dominio
Section titled “Estructura del dominio”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
| Capa | Archivo | Rol |
|---|---|---|
| Service | PayrollService.ts | Aplicación: valida, hidrata contexto, orquesta, gestiona transacción y outbox. Hereda BaseService. |
| Engine | PayrollEngine.ts | Función pura calculate(input) determinista. Sin DB. |
| Input Builder | PayrollInputBuilder.ts | Construcción fluida del PayrollInput (~30 campos). |
| Repository | PayrollRepository.ts | Acceso a PostgreSQL: contexto, persistencia, consultas. |
| Calculators | calculators/ | Reglas específicas, cada una aislada y testeable. |
| Types | types.ts | ContractType, HealthMode, GratificationMode. |
API pública
Section titled “API pública”| Método | Modo | Resultado |
|---|---|---|
previewPayroll(ctx, contractId, period, overrides?) | Lectura | Calcula sin persistir. Devuelve PayrollResult. |
generatePayroll(ctx, contractId, period, options?) | Escritura transaccional | Calcula y persiste. Acepta dryRun, force, overrides. |
deletePayroll(ctx, id, force?) | Escritura | Borra liquidación. APROBADA/PAGADA requieren force. |
updateFileUrl(ctx, id, url) | Escritura | Asocia URL del PDF. |
updateStatus(ctx, id, estado) | Escritura transaccional | Cambia estado y encola payroll:approved si pasa a APROBADA. |
PayrollOverrides
Section titled “PayrollOverrides”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.).
Flujo de cálculo (engine puro)
Section titled “Flujo de cálculo (engine puro)”PayrollEngine.calculate(input: PayrollInput) ejecuta los pasos en orden:
-
Sueldo base prorrateado —
BaseSalaryCalculatorajusta por días trabajados sobrebase_days(default 30), con piso en el sueldo mínimo. -
Horas extra — valor hora con factor Art. 32 + recargos 50% y 100% según
attendance.horas_extra_*. -
Gratificación —
GratificationCalculatoraplica modo configurado: 25% Art. 50, abono anual, exento o sin gratificación. -
Total imponible — suma de sueldo, gratificación, bonos y horas extra. Base de cotizaciones.
-
Leyes sociales —
SocialLawsCalculatorcalcula AFP, salud, AFC y aportes patronales respetando topes UF. -
Impuesto único —
TaxCalculatoraplica tabla SII sobre base tributable (imponible menos descuentos legales menos APV régimen B). -
Haberes no imponibles —
ProrrataCalculatorprorratea colación y movilización por días trabajados. -
Descuentos y líquido — suma descuentos legales, impuesto, anticipos, préstamos y voluntarios. Líquido = haberes − descuentos.
Calculadoras
Section titled “Calculadoras”| Calculadora | Responsabilidad |
|---|---|
BaseSalaryCalculator | Sueldo prorrateado por días trabajados con piso en sueldo mínimo. |
GratificationCalculator | Art. 50: 25%, abono anual fijo, exento o sin gratificación. |
SocialLawsCalculator | AFP, salud, AFC, SIS, mutual y aportes patronales con topes. |
TaxCalculator | Impuesto único de segunda categoría sobre base tributable. |
HealthPlanCalculator | Fonasa 7% vs. Isapre: mayor entre 7% y plan UF pactado. |
ProrrataCalculator | Prorrateo mensual de colación/movilización. |
Reglas críticas
Section titled “Reglas críticas”Tasas AFC por tipo de contrato
Section titled “Tasas AFC por tipo de contrato”calculateAfcRates(contractType) ajusta las tasas antes de construir el input:
tipo_contrato | Trabajador | Empleador | Indemnización a todo evento |
|---|---|---|---|
INDEFINIDO | 0,6 % | 2,4 % | — |
PLAZO_FIJO | — | 3,0 % | — |
CASA_PARTICULAR | — | 3,0 % | 1,11 % |
Cotización de salud
Section titled “Cotización de salud”| Modo | Cálculo |
|---|---|
FONASA | 7% sobre imponible topeado. Sin diferencia adicional. |
ISAPRE | Mayor entre 7% legal y el plan UF pactado (convertido a CLP con UF del periodo). |
Conversión UF → CLP
Section titled “Conversión UF → CLP”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).
Estados y persistencia
Section titled “Estados y persistencia”BORRADOR → CALCULADA → APROBADA → PAGADA ↓ ANULADA| Estado | Significado |
|---|---|
BORRADOR | En preparación. |
CALCULADA | Generada y persistida por generatePayroll. |
APROBADA | Validada por usuario. Emite payroll:approved post-COMMIT. |
PAGADA | Pago registrado. fecha_pago = NOW() automático. |
ANULADA | Sin 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 UPDATEpara serializar transiciones. - Marca
fecha_aprobacion = NOW()al pasar aAPROBADA(idempotente). - Marca
fecha_pago = NOW()al pasar aPAGADA(idempotente).
Outbox de eventos
Section titled “Outbox de eventos”| Evento | Trigger | Payload |
|---|---|---|
payroll:approved | updateStatus(_, _, "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", { ... }); }});Errores
Section titled “Errores”| Origen | Mensaje | Cuándo |
|---|---|---|
validateRequired | "<campo> es requerido" | Falta contractId o period. |
assertExists | "PayrollContext <id> not found" | Contrato sin contexto válido para el periodo. |
| Service | notFound("Liquidación no encontrada") | deletePayroll/updateStatus con id inexistente. |
| Service | badRequest("Estado inválido. Valores permitidos: ...") | updateStatus con valor fuera del enum. |
| Repository | "No encontrada" / otros | deleteById con restricciones (estado protegido). |
Ejemplo de uso
Section titled “Ejemplo de uso”const service = new PayrollService();
// Preview sin persistirconst 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");Consumidores
Section titled “Consumidores”| Service | Cómo consume |
|---|---|
PrevisionsService | Lee liquidaciones del periodo (getAFPWorkerAggregates, getHealthWorkerAggregates, etc.) para consolidar imposiciones. |
FiniquitoService | Lee última remuneración del trabajador para determinar base indemnizatoria. |
DJ1887Service | Consume liquidaciones definitivas anuales para alimentar la declaración. |
F29GeneratorService | Suma impuesto_unico del periodo para la línea de retenciones. |