Skip to content

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).

  • 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.

MétodoFirmaResultado
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.
ContractService (orquestación)
├── ContractComplianceValidator → reglas legales
├── ContractRepository → SQL (CRUD + filePath)
├── ContractFormatters → RUT, fechas, horarios para el documento
├── ContractHtmlGenerator → HTML → PDF buffer
└── ContractGenerator → Tipo ContractContext + plantillas

ContractService 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.

ContractComplianceValidator.validate(data, employee, jornada) aplica cuatro reglas y lanza un único Error agregado:

ReglaComprobaciónMensaje
Jornada obligatoriajornada !== null"Debe asignar una Jornada de Trabajo válida."
AFP del empleadoemployee.afp_id || employee.afp_nombre"El empleado debe tener una AFP asignada."
Salud del empleadoemployee.isapre_id || employee.isapre_nombre"El empleado debe tener un sistema de Salud válido (Fonasa/Isapre)."
Coherencia de fechasend > 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".

createContract se ejecuta dentro de withTransaction(ctx, async (client) => {...}). Si cualquier paso falla, rollback.

  1. validateRequiredFields(data) y validateCategoria(data.categoria_trabajador).

  2. 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 hay cargo_id.
    • getJornadaData(client, jornada_id) → jornada completa con horarios.
    • getCompanyInfo(ctx) → desde CompanyService con fallback a [NOMBRE_EMPRESA].
    • getLegalRepInfo(ctx) → desde LegalRepService con fallback a [NOMBRE_REP].
  3. ContractComplianceValidator.validate(data, emp, jornada) — bloquea si falla.

  4. ContractRepository.create(client, data, userId) inserta y devuelve id.

  5. buildContractContext(emp, data, cargoNombre, jornada, empresa, repLegal) arma el ContractContext para la plantilla (formatea RUT, fechas largas, horarios, sueldo).

  6. generateAndSavePdf(tenantDb, contratoId, plantilla, ctx) invoca ContractHtmlGenerator, crea el directorio storage/<tenant>/contracts/ si no existe y escribe contrato_<id>.pdf.

  7. ContractRepository.updatePath(client, contratoId, pdfPath, userId) guarda la ruta del archivo.

  8. success(contratoId) — la transacción commitea.

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ó (getDownloadPath con force=true).
const pdfPath = await ContractService.regenerateContractPdf(ctx, contractId);
// → "/app/storage/tenant_abc/contracts/contrato_xyz.pdf"
async getDownloadPath(ctx, id, format: 'pdf' | 'docx', force = false)

Lógica:

  1. Lee pdfPath desde DB.
  2. Si el archivo no existe en disco, anula pdfPath.
  3. Si pdfPath es null o force=true, llama a regenerateContractPdf. Si la regeneración falla pero hay un PDF antiguo en disco, lo devuelve.
  4. Si format === "pdf", devuelve pdfPath.
  5. Si format === "docx", busca el DOCX paralelo <pdfPath sin .pdf>.docx. Si existe, lo devuelve; si no, fallback al PDF.

plantilla puede ser "CONTRATO" (default) o "ANEXO". La diferencia la maneja ContractHtmlGenerator al cargar el template HTML correspondiente.

Helpers de presentación usados al armar el ContractContext:

HelperUso
formatRut(rut)Aplica separadores de miles y guión: 12345678912.345.678-9.
formatDateLong(date)Convierte a “15 de marzo de 2026” en español.
formatSchedule(jornada)Lee los días trabaja_lunestrabaja_domingo y arma "Lunes a Viernes: de 09:00 a 19:00 horas." con bloque de colación.
OrigenMensajeCuá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.

ContractService actualmente no emite eventos de dominio. Cambios contractuales se observan vía cambios en la tabla remuneraciones.contratos.

ServiceCómo usa al contrato
PayrollServiceLee contrato vigente del periodo para sueldo base, jornada, AFP, isapre y centro de costo.
AttendanceServiceToma la jornada del contrato vigente para generar la grilla esperada.
VacationServiceLee fecha de ingreso + jornada para calcular saldo de feriado.
FiniquitoServiceToma el contrato vigente o recientemente terminado para liquidar el cierre.