Plataforma técnica · Orchestrator
Contract Service
Remuneraciones Contratos Pdf
ContractService orquesta el ciclo de vida del contrato laboral: validación de compliance, recolección de contexto desde múltiples services (empleado, cargo, jornada, empresa, representante legal), persistencia transaccional y generación de PDF. Es el service más complejo del subdominio maestro laboral porque integra datos de cinco fuentes distintas en un único documento.
El dominio está partido en cinco archivos para que cada responsabilidad sea testeable por separado: orquestación (ContractService), persistencia (ContractRepository), reglas legales (ContractComplianceValidator), formatos para presentación (ContractFormatters) y generación del documento (ContractHtmlGenerator + ContractGenerator).
Ubicación
Section titled “Ubicación”Directoryorchestrator/src/domain/contracts/
- ContractService.ts
- ContractRepository.ts
- ContractComplianceValidator.ts
- ContractFormatters.ts
- ContractGenerator.ts
- ContractHtmlGenerator.ts
- types.ts
Tabla: remuneraciones.contratos. PDFs en disco bajo storage/<tenantDb>/contracts/contrato_<id>.pdf.
API pública
Section titled “API pública”| Método | Firma | Resultado |
|---|---|---|
getContracts | (ctx, filter: ContractFilter) => Promise<ContractWithDetails[]> | Lista contratos con join enriquecido (empleado, cargo, jornada, departamento). |
createContract | (ctx, data: ContractInput) => Promise<ServiceResult<string>> | Crea contrato transaccional + genera PDF. Devuelve id. |
updateContract | (ctx, id, data: ContractMutationInput) => Promise<ServiceResult<boolean>> | Update parcial. Valida categoria_trabajador si se envía. |
deleteContract | (ctx, id) => Promise<ServiceResult<boolean>> | Borra el contrato (no toca el PDF en disco). |
getDownloadPath | (ctx, id, format, force?) => Promise<string | null> | Ruta absoluta al PDF/DOCX. Si no existe o force=true, regenera. |
regenerateContractPdf | (ctx, contractId) => Promise<string> | Recalcula contexto y regenera el PDF. Útil tras cambios de empresa o rep legal. |
Estructura del dominio
Section titled “Estructura del dominio”ContractService (orquestación) ├── ContractComplianceValidator → reglas legales ├── ContractRepository → SQL (CRUD + filePath) ├── ContractFormatters → RUT, fechas, horarios para el documento ├── ContractHtmlGenerator → HTML → PDF buffer └── ContractGenerator → Tipo ContractContext + plantillasContractService no genera el documento directamente: recolecta el contexto, lo arma vía buildContractContext y delega a ContractHtmlGenerator.generate(tenantDb, contractId, plantilla, ctx) que devuelve el buffer del PDF.
Validación de compliance
Section titled “Validación de compliance”ContractComplianceValidator.validate(data, employee, jornada) aplica cuatro reglas y lanza un único Error agregado:
| Regla | Comprobación | Mensaje |
|---|---|---|
| Jornada obligatoria | jornada !== null | "Debe asignar una Jornada de Trabajo válida." |
| AFP del empleado | employee.afp_id || employee.afp_nombre | "El empleado debe tener una AFP asignada." |
| Salud del empleado | employee.isapre_id || employee.isapre_nombre | "El empleado debe tener un sistema de Salud válido (Fonasa/Isapre)." |
| Coherencia de fechas | end > start cuando ambas existen | "La fecha de término debe ser posterior a la fecha de inicio." |
Errores se agrupan: "Compliance Error: msg1 | msg2 | ...". Edad < 18 está detectada pero no bloquea (warning informativo).
validateRequiredFields(data) exige: empleado_id, tipo_contrato, fecha_inicio, sueldo_base, jornada_id.
validateCategoria(categoria) normaliza a uppercase y verifica que ∈ ["DEPENDIENTE", "MENOR_MAYOR", "CASA_PARTICULAR"]. Default "DEPENDIENTE".
Flujo de creación
Section titled “Flujo de creación”createContract se ejecuta dentro de withTransaction(ctx, async (client) => {...}). Si cualquier paso falla, rollback.
-
validateRequiredFields(data)yvalidateCategoria(data.categoria_trabajador). -
Recolección de contexto dentro de la transacción:
getEmployeeData(client, empleado_id)→ empleado + AFP join + Isapre join.getCargoNombre(client, cargo_id)→ solo el nombre si haycargo_id.getJornadaData(client, jornada_id)→ jornada completa con horarios.getCompanyInfo(ctx)→ desdeCompanyServicecon fallback a[NOMBRE_EMPRESA].getLegalRepInfo(ctx)→ desdeLegalRepServicecon fallback a[NOMBRE_REP].
-
ContractComplianceValidator.validate(data, emp, jornada)— bloquea si falla. -
ContractRepository.create(client, data, userId)inserta y devuelveid. -
buildContractContext(emp, data, cargoNombre, jornada, empresa, repLegal)arma elContractContextpara la plantilla (formatea RUT, fechas largas, horarios, sueldo). -
generateAndSavePdf(tenantDb, contratoId, plantilla, ctx)invocaContractHtmlGenerator, crea el directoriostorage/<tenant>/contracts/si no existe y escribecontrato_<id>.pdf. -
ContractRepository.updatePath(client, contratoId, pdfPath, userId)guarda la ruta del archivo. -
success(contratoId)— la transacción commitea.
Regeneración del PDF
Section titled “Regeneración del PDF”regenerateContractPdf(ctx, contractId) repite los pasos 2-7 pero con plantilla fija "CONTRATO". Útil cuando:
- Cambia la información de la empresa o representante legal.
- Se actualiza la jornada del contrato (anexo) y se quiere refrescar el documento.
- El archivo en disco se perdió (
getDownloadPathconforce=true).
const pdfPath = await ContractService.regenerateContractPdf(ctx, contractId);// → "/app/storage/tenant_abc/contracts/contrato_xyz.pdf"Descarga: getDownloadPath
Section titled “Descarga: getDownloadPath”async getDownloadPath(ctx, id, format: 'pdf' | 'docx', force = false)Lógica:
- Lee
pdfPathdesde DB. - Si el archivo no existe en disco, anula
pdfPath. - Si
pdfPathesnulloforce=true, llama aregenerateContractPdf. Si la regeneración falla pero hay un PDF antiguo en disco, lo devuelve. - Si
format === "pdf", devuelvepdfPath. - Si
format === "docx", busca el DOCX paralelo<pdfPath sin .pdf>.docx. Si existe, lo devuelve; si no, fallback al PDF.
Plantillas
Section titled “Plantillas”plantilla puede ser "CONTRATO" (default) o "ANEXO". La diferencia la maneja ContractHtmlGenerator al cargar el template HTML correspondiente.
ContractFormatters
Section titled “ContractFormatters”Helpers de presentación usados al armar el ContractContext:
| Helper | Uso |
|---|---|
formatRut(rut) | Aplica separadores de miles y guión: 123456789 → 12.345.678-9. |
formatDateLong(date) | Convierte a “15 de marzo de 2026” en español. |
formatSchedule(jornada) | Lee los días trabaja_lunes…trabaja_domingo y arma "Lunes a Viernes: de 09:00 a 19:00 horas." con bloque de colación. |
Errores
Section titled “Errores”| Origen | Mensaje | Cuándo |
|---|---|---|
validateRequiredFields | "Faltan campos obligatorios: <campos>" | Falta empleado_id, tipo_contrato, fecha_inicio, sueldo_base o jornada_id. |
validateCategoria | "categoria_trabajador inválida" | Valor fuera del enum. |
getEmployeeData | "Empleado no existe" | empleado_id no encontrado. |
getJornadaData | "jornada_id no existe" | Jornada no encontrada. |
validate | "Compliance Error: <msg1> | <msg2>" | Falla cualquier regla de compliance. |
assertExists (regenerate) | "Contract <id> not found" | Regenerar un contrato que ya no existe en DB. |
Eventos
Section titled “Eventos”ContractService actualmente no emite eventos de dominio. Cambios contractuales se observan vía cambios en la tabla remuneraciones.contratos.
Consumidores
Section titled “Consumidores”| Service | Cómo usa al contrato |
|---|---|
PayrollService | Lee contrato vigente del periodo para sueldo base, jornada, AFP, isapre y centro de costo. |
AttendanceService | Toma la jornada del contrato vigente para generar la grilla esperada. |
VacationService | Lee fecha de ingreso + jornada para calcular saldo de feriado. |
FiniquitoService | Toma el contrato vigente o recientemente terminado para liquidar el cierre. |