Plataforma técnica · Orchestrator
Vacation Service
Remuneraciones Vacaciones Feriado Legal
VacationService administra solicitudes de feriado legal y mantiene los saldos por trabajador. Calcula días hábiles consultando el calendario de feriados nacionales, agrega los días corridos equivalentes, y se apoya en un constraint de exclusión PostgreSQL para impedir solapamientos entre solicitudes aprobadas.
Es el service con más lógica de cálculo del subdominio: los helpers privados calculateBusinessDays y calculateRunningDays resuelven el conteo de días hábiles y corridos respetando feriados, sin asumir 5 días/semana lineal.
Ubicación
Section titled “Ubicación”Directoryorchestrator/src/domain/vacations/
- VacationService.ts
- VacationRepository.ts
- types.ts
Tabla: remuneraciones.vacaciones. Saldos: vista calculada que combina devengo + uso histórico.
API pública
Section titled “API pública”| Método | Firma | Resultado |
|---|---|---|
findById | (ctx, id) => Promise<VacationRequestWithDetails | null> | Solicitud con datos del empleado, departamento, liquidación si la hubo. |
findAll | (ctx, filter: VacationListFilter) => Promise<{ rows, total }> | Lista filtrada con totales. |
findRecent | (ctx) => Promise<VacationRequestWithDetails[]> | Últimas solicitudes (dashboard). |
getBalances | (ctx, filter: VacationSaldoFilter) => Promise<{ rows, total }> | Saldos por trabajador con dias_corridos enriquecido. |
create | (ctx, data: VacationRequestInput) => Promise<ServiceResult<void>> | Calcula dias_habiles si no se provee y persiste. |
update | (ctx, id, data: VacationUpdateInput) => Promise<ServiceResult<void>> | Update parcial; recalcula días hábiles si cambian fechas. |
delete | (ctx, id) => Promise<ServiceResult<boolean>> | Borra; Error("No encontrado") si 0 filas. |
Método estático
Section titled “Método estático”static async getEmployeeBalance( tenantDb: string | Pool | PoolClient, empleadoId: string, corteDate: string,): Promise<VacationBalance | null>Preservado para FiniquitoService, que lo invoca con su PoolClient transaccional sin pasar por ServiceContext. Acepta string, Pool o PoolClient. Calcula el saldo a la fecha de corte y enriquece con dias_corridos.
type VacationStatus = "PENDIENTE" | "APROBADA" | "RECHAZADA" | "ANULADA";
interface VacationRequestInput { empleado_id: string; fecha_inicio: string; // YYYY-MM-DD fecha_fin: string; // YYYY-MM-DD dias_habiles?: number; // si se omite, se calcula estado_solicitud?: VacationStatus; // default PENDIENTE comentario?: string;}
interface VacationBalance { empleado_id: string; dias_base: number; // 15 días/año por defecto dias_progresivos: number; // adicionales por antigüedad o zona dias_usados: number; dias_pendientes: number; dias_corridos?: number; // calendar days que se necesitan para cubrir los hábiles pendientes corte_real?: string | Date; fecha_ingreso?: string;}Cálculo de días hábiles
Section titled “Cálculo de días hábiles”calculateBusinessDays(fromISO, toISO) itera día por día en UTC, contando solo lunes-viernes que no estén en el set de feriados del periodo:
const feriados = await VacationRepository.fetchHolidaySet(a, b);let n = 0;for (let d = new Date(a + "T00:00:00Z"); this.toISODate(d) <= b; d = this.addDaysUTC(d, 1)) { const dow = d.getUTCDay(); const iso = this.toISODate(d); if (dow >= 1 && dow <= 5 && !feriados.has(iso)) n++;}return n;| Detalle | Justificación |
|---|---|
| Iteración en UTC | Evita off-by-one por DST chileno. |
dow >= 1 && dow <= 5 | Excluye sábado (6) y domingo (0). |
feriados.has(iso) | Lee desde tabla de feriados nacionales del tenant. |
| Sin parámetro de jornada | Asume estándar 5 días/semana. Para jornadas atípicas (por ej. cuartos turnos), este cálculo subestima. |
Cálculo de días corridos
Section titled “Cálculo de días corridos”calculateRunningDays(startISO, diasHabiles) resuelve la pregunta inversa: dado N días hábiles a partir de una fecha, ¿cuántos días corridos abarca?
- Lookahead máximo: 730 días (~2 años) para evitar loops infinitos en datos corruptos.
- Itera hasta acumular
Ndías hábiles, contando el total de días calendario recorridos. - Devuelve
0sidiasHabiles <= 0.
Se aplica al cargar getBalances para que el frontend pueda mostrar “te quedan 10 días hábiles que equivalen a 14 corridos desde tu próximo corte”.
Validaciones en create
Section titled “Validaciones en create”| Campo | Regla |
|---|---|
empleado_id, fecha_inicio, fecha_fin | Obligatorios. validateRequired. |
fecha_inicio, fecha_fin | Formato YYYY-MM-DD exacto (regex ^\d{4}-\d{2}-\d{2}$). Falla → "Fechas en formato YYYY-MM-DD". |
fecha_fin >= fecha_inicio | "fecha_fin debe ser >= fecha_inicio". |
estado_solicitud | ∈ ["PENDIENTE", "APROBADA", "RECHAZADA", "ANULADA"]. Default "PENDIENTE". |
dias_habiles | Si no se envía, se calcula con calculateBusinessDays. Si se envía, clamp a Math.max(0, parseInt). |
Si el estado al crear es APROBADA, el service marca aprobado_por = ctx.userId y aprobado_at = new Date() automáticamente.
Prevención de solapamiento (constraint DB)
Section titled “Prevención de solapamiento (constraint DB)”Una solicitud con estado APROBADA no puede solaparse con otra del mismo empleado también APROBADA. La regla vive en PostgreSQL como un EXCLUDE constraint (código 23P01 cuando se viola):
try { await VacationRepository.create(pool, { ... });} catch (err) { if (isPgError(err) && err.code === "23P01") { throw Object.assign(new Error( "Solicitud solapa con otra APROBADA del mismo empleado" ), { cause: err }); } throw err;}Las solicitudes PENDIENTE, RECHAZADA o ANULADA pueden coexistir libremente — la exclusión solo aplica a aprobadas.
Errores
Section titled “Errores”| Mensaje | Cuándo |
|---|---|
"<campo> es requerido" | Falta empleado_id, fecha_inicio o fecha_fin. |
"Fechas en formato YYYY-MM-DD" | Fecha no calza con el regex. |
"fecha_fin debe ser >= fecha_inicio" | Rango invertido. |
"estado_solicitud inválido" | Estado fuera del enum. |
"Solicitud solapa con otra APROBADA del mismo empleado" | Constraint 23P01 violado. |
"No encontrado o error al actualizar" | update con id inexistente. |
"No encontrado" | delete con id inexistente. |
Consumidores
Section titled “Consumidores”| Service | Cómo consume |
|---|---|
AttendanceService | Cuando una solicitud pasa a APROBADA, los días del rango se marcan en la grilla mensual con tipo_ausencia = "VACACIONES". |
PayrollEngine | Lee días de vacaciones del periodo para no descontar y para potencialmente calcular base diaria por separado. |
FiniquitoService | Llama a VacationService.getEmployeeBalance(client, empleadoId, fechaTermino) dentro de la transacción del finiquito para conocer el saldo pendiente que debe pagarse. |
PermissionService | Si un permiso descuenta_vacaciones=true se aprueba, el saldo se rebaja al consumir. |