Skip to content

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.

  • Directoryorchestrator/src/domain/vacations/
    • VacationService.ts
    • VacationRepository.ts
    • types.ts

Tabla: remuneraciones.vacaciones. Saldos: vista calculada que combina devengo + uso histórico.

MétodoFirmaResultado
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.
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;
}

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;
DetalleJustificación
Iteración en UTCEvita off-by-one por DST chileno.
dow >= 1 && dow <= 5Excluye sábado (6) y domingo (0).
feriados.has(iso)Lee desde tabla de feriados nacionales del tenant.
Sin parámetro de jornadaAsume estándar 5 días/semana. Para jornadas atípicas (por ej. cuartos turnos), este cálculo subestima.

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 N días hábiles, contando el total de días calendario recorridos.
  • Devuelve 0 si diasHabiles <= 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”.

CampoRegla
empleado_id, fecha_inicio, fecha_finObligatorios. validateRequired.
fecha_inicio, fecha_finFormato 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_habilesSi 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.

MensajeCuá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.
ServiceCómo consume
AttendanceServiceCuando una solicitud pasa a APROBADA, los días del rango se marcan en la grilla mensual con tipo_ausencia = "VACACIONES".
PayrollEngineLee días de vacaciones del periodo para no descontar y para potencialmente calcular base diaria por separado.
FiniquitoServiceLlama a VacationService.getEmployeeBalance(client, empleadoId, fechaTermino) dentro de la transacción del finiquito para conocer el saldo pendiente que debe pagarse.
PermissionServiceSi un permiso descuenta_vacaciones=true se aprueba, el saldo se rebaja al consumir.