< Summary

Information
Class: FAU.Logica.Services.PersonalService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/PersonalService.cs
Line coverage
77%
Covered lines: 411
Uncovered lines: 118
Coverable lines: 529
Total lines: 866
Line coverage: 77.6%
Branch coverage
43%
Covered branches: 143
Total branches: 332
Branch coverage: 43%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetPersonalListAsync()50%201468.29%
GetDetalleAsync()83.33%66100%
CreatePersonalAsync()61.11%211880%
UpdatePersonalAsync()37.03%274821662.14%
RegistrarBajaAsync()68.75%171684.78%
RegistrarAscensoAsync()80.76%262691.56%
ApplyOrdering(...)27.27%2262225%
DeterminarRegimenIdAsync()14.28%481444.44%

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/PersonalService.cs

#LineLine coverage
 1using FAU.DataAccess;
 2using FAU.DataAccess.Repositories;
 3using FAU.Entidades;
 4using FAU.Logica.DTOs;
 5using FAU.Logica.Validadores;
 6using Microsoft.EntityFrameworkCore;
 7using Microsoft.Extensions.Logging;
 8
 9namespace FAU.Logica.Services;
 10
 11/// <summary>
 12/// Servicio de gestión de Personal
 13/// </summary>
 14public class PersonalService : IPersonalService
 15{
 16    private readonly IPersonalRepository _personalRepository;
 17    private readonly ApplicationDbContext _context;
 18    private readonly ILogger<PersonalService> _logger;
 19
 2020    public PersonalService(
 2021        IPersonalRepository personalRepository,
 2022        ApplicationDbContext context,
 2023        ILogger<PersonalService> logger)
 24    {
 2025        _personalRepository = personalRepository;
 2026        _context = context;
 2027        _logger = logger;
 2028    }
 29
 30    public async Task<object> GetPersonalListAsync(
 31        string? buscar,
 32        long? cargoId,
 33        string? estado,
 34        string? ley,
 35        int page,
 36        int pageSize,
 37        string? orderBy,
 38        string sort)
 39    {
 140        var query = _context.RelacionesLaborales
 141            .Include(r => r.Persona)
 142            .Include(r => r.Regimen)
 143            .Include(r => r.Escalafon)
 144            .Include(r => r.Grado)
 145            .Include(r => r.Situacion)
 146            .AsQueryable();
 47
 48        // Filtro por búsqueda (CI, nombre o apellido)
 149        if (!string.IsNullOrWhiteSpace(buscar))
 50        {
 051            var buscarLower = buscar.ToLower();
 052            query = query.Where(r =>
 053                r.Persona.Cedula.Contains(buscarLower) ||
 054                r.Persona.PrimerNombre.ToLower().Contains(buscarLower) ||
 055                r.Persona.PrimerApellido.ToLower().Contains(buscarLower) ||
 056                (r.Persona.SegundoNombre != null && r.Persona.SegundoNombre.ToLower().Contains(buscarLower)) ||
 057                (r.Persona.SegundoApellido != null && r.Persona.SegundoApellido.ToLower().Contains(buscarLower)));
 58        }
 59
 60        // Filtro por cargo (grado)
 161        if (cargoId.HasValue)
 62        {
 063            query = query.Where(r => r.GradoId == cargoId.Value);
 64        }
 65
 66        // Filtro por estado (parse string to enum)
 167        if (!string.IsNullOrWhiteSpace(estado))
 68        {
 169            if (Enum.TryParse<EstadoRelacion>(estado, true, out var estadoEnum))
 70            {
 171                query = query.Where(r => r.Estado == estadoEnum);
 72            }
 73            else
 74            {
 75                // Si no se puede parsear, intentar aceptar valores comunes
 076                var estLower = estado.Trim().ToLower();
 077                if (estLower == "activo") query = query.Where(r => r.Estado == EstadoRelacion.Activo);
 078                else if (estLower == "inactivo") query = query.Where(r => r.Estado == EstadoRelacion.Inactivo);
 79            }
 80        }
 81
 82        // Filtro por ley (régimen)
 183        if (!string.IsNullOrWhiteSpace(ley))
 84        {
 085            var leyLower = ley.ToLower();
 086            query = query.Where(r => r.Regimen.NumeroLey.ToLower().Contains(leyLower));
 87        }
 88
 189        var total = await query.CountAsync();
 90
 191        query = ApplyOrdering(query, orderBy, sort);
 92
 93        // Paginación: obtener entidades y luego proyectar a DTO en memoria para poder convertir enums a string
 194        var relacionesPage = await query
 195            .Skip((page - 1) * pageSize)
 196            .Take(pageSize)
 197            .ToListAsync();
 98
 299        var items = relacionesPage.Select(r => new PersonalListDto
 2100        {
 2101            Id = r.Id,
 2102            Ci = r.Persona.Cedula,
 2103            personalId = r.PersonaId,
 2104            PrimerNombre = r.Persona.PrimerNombre,
 2105            SegundoNombre = r.Persona.SegundoNombre,
 2106            PrimerApellido = r.Persona.PrimerApellido,
 2107            SegundoApellido = r.Persona.SegundoApellido,
 2108            Cargo = r.Grado.Denominacion,
 2109            Grado = r.Grado.Denominacion,
 2110            Ley = r.Regimen.NumeroLey,
 2111            Estado = r.Estado.ToString(),
 2112            FechaIngreso = r.FechaInicio
 2113        }).ToList();
 114
 1115        return new
 1116        {
 1117            items,
 1118            total,
 1119            page,
 1120            pageSize,
 1121            totalPages = (int)Math.Ceiling((double)total / pageSize)
 1122        };
 1123    }
 124
 125    public async Task<PersonalDetalleDto?> GetDetalleAsync(long id)
 126    {
 127        // id is PersonaId: obtener la relación laboral activa o la última relación laboral
 3128        var relacionLaboral = await _context.RelacionesLaborales
 3129            .Include(r => r.Persona)
 3130            .Include(r => r.Regimen)
 3131            .Include(r => r.Escalafon)
 3132            .Include(r => r.Grado)
 3133            .Include(r => r.Situacion)
 3134            .Include(r => r.Programa)
 3135            .Include(r => r.Unidad)
 3136            .Include(r => r.Compania)
 3137            .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 138
 3139        if (relacionLaboral == null)
 140        {
 2141            relacionLaboral = await _context.RelacionesLaborales
 2142                .Include(r => r.Persona)
 2143                .Include(r => r.Regimen)
 2144                .Include(r => r.Escalafon)
 2145                .Include(r => r.Grado)
 2146                .Include(r => r.Situacion)
 2147                .Include(r => r.Programa)
 2148                .Include(r => r.Unidad)
 2149                .Include(r => r.Compania)
 2150                .Where(r => r.PersonaId == id)
 2151                .OrderByDescending(r => r.FechaInicio)
 2152                .FirstOrDefaultAsync();
 153        }
 154
 3155        if (relacionLaboral == null)
 1156            return null;
 157
 158        // Obtener todos los ids de relación laboral de la persona y luego el historial de movimientos para cualquiera d
 2159        var relacionIds = await _context.RelacionesLaborales
 2160            .Where(r => r.PersonaId == relacionLaboral.PersonaId)
 2161            .Select(r => r.Id)
 2162            .ToListAsync();
 163
 2164        var historial = await _context.MovimientosLaborales
 2165            .Include(m => m.TipoMovimiento)
 2166            .Include(m => m.Usuario)
 2167                .ThenInclude(u => u.Persona)
 2168            .Where(m => relacionIds.Contains(m.RelacionLaboralId))
 2169            .OrderByDescending(m => m.Id)
 2170            .Select(m => new HistorialMovimientoDto
 2171            {
 2172                Id = m.Id,
 2173                TipoMovimiento = m.TipoMovimiento.Nombre,
 2174                EsAlta = m.TipoMovimiento.EsAlta,
 2175                FechaMovimiento = m.FechaMovimiento,
 2176                Observaciones = m.Observaciones,
 2177                RegistradoPor = m.Usuario != null ? m.Usuario.Username : null,
 2178                FechaRegistro = m.FechaMovimiento
 2179            })
 2180            .ToListAsync();
 181
 2182        return new PersonalDetalleDto
 2183        {
 2184            Id = relacionLaboral.Id,
 2185            DatosPersonales = new DatosPersonalesDto
 2186            {
 2187                PersonaId = relacionLaboral.PersonaId,
 2188                Ci = relacionLaboral.Persona.Cedula,
 2189                PrimerNombre = relacionLaboral.Persona.PrimerNombre,
 2190                SegundoNombre = relacionLaboral.Persona.SegundoNombre,
 2191                PrimerApellido = relacionLaboral.Persona.PrimerApellido,
 2192                SegundoApellido = relacionLaboral.Persona.SegundoApellido,
 2193                FechaNacimiento = relacionLaboral.Persona.FechaNacimiento,
 2194                Email = relacionLaboral.Persona.Email,
 2195                Telefono = relacionLaboral.Persona.Telefono,
 2196                Direccion = relacionLaboral.Persona.Direccion
 2197            },
 2198            DatosLaborales = new DatosLaboralesDto
 2199            {
 2200                RelacionLaboralId = relacionLaboral.Id,
 2201                Estado = relacionLaboral.Estado.ToString(),
 2202                FechaInicio = relacionLaboral.FechaInicio,
 2203                FechaFin = relacionLaboral.FechaFin,
 2204                RegimenId = relacionLaboral.RegimenId,
 2205                Regimen = relacionLaboral.Regimen.Denominacion,
 2206                Ley = relacionLaboral.Regimen.NumeroLey,
 2207                EscalafonId = relacionLaboral.EscalafonId,
 2208                Escalafon = relacionLaboral.Escalafon.Denominacion,
 2209                GradoId = relacionLaboral.GradoId,
 2210                Grado = relacionLaboral.Grado.Denominacion,
 2211                Cargo = relacionLaboral.Grado.Denominacion,
 2212                SituacionId = relacionLaboral.SituacionId,
 2213                Situacion = relacionLaboral.Situacion.Denominacion,
 2214                ProgramaId = relacionLaboral.ProgramaId,
 2215                Programa = relacionLaboral.Programa.Denominacion,
 2216                UnidadId = relacionLaboral.UnidadId,
 2217                Unidad = relacionLaboral.Unidad.Denominacion,
 2218                CompaniaId = relacionLaboral.CompaniaId,
 2219                Compania = relacionLaboral.Compania?.Denominacion,
 2220                RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 2221                PrimaTecnica = relacionLaboral.PrimaTecnica,
 2222                PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 2223                AniosInactivos = relacionLaboral.AniosInactivos,
 2224                Observaciones = relacionLaboral.Observaciones
 2225            },
 2226            Historial = historial
 2227        };
 3228    }
 229
 230    public async Task<(bool success, long? personalId, string? error)> CreatePersonalAsync(CreatePersonalRequest request
 231    {
 232        try
 233        {
 234            // Validar y limpiar CI
 4235            var cedulaLimpia = CedulaValidator.Limpiar(request.Ci);
 4236            if (!CedulaValidator.Validar(cedulaLimpia))
 237            {
 1238                return (false, null, "La cédula ingresada no es válida.");
 239            }
 240
 241            // Validar que la fecha de ingreso no sea futura
 3242            var errorFechaIngreso = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaIngreso);
 3243            if (errorFechaIngreso != null)
 244            {
 1245                return (false, null, errorFechaIngreso);
 246            }
 247
 248            // Validar que no exista una relación laboral activa para esa cédula
 2249            var validadorRelacion = new RelacionLaboralValidator(_context);
 2250            var errorRelacionActiva = await validadorRelacion.ObtenerErrorSiExisteRelacionActiva(cedulaLimpia);
 2251            if (errorRelacionActiva != null)
 252            {
 1253                return (false, null, errorRelacionActiva);
 254            }
 255
 256            // Buscar o crear persona
 1257            Persona? persona = await _context.Personas.FirstOrDefaultAsync(p => p.Cedula == cedulaLimpia);
 258
 1259            if (persona == null)
 260            {
 261                // Crear persona - Convertir fechas a UTC
 1262                var fechaNacimientoUtc = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 1263                    ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 1264                    : request.FechaNacimiento.Date.ToUniversalTime();
 265
 1266                persona = new Persona
 1267                {
 1268                    Cedula = cedulaLimpia,
 1269                    PrimerNombre = request.PrimerNombre,
 1270                    SegundoNombre = request.SegundoNombre,
 1271                    PrimerApellido = request.PrimerApellido,
 1272                    SegundoApellido = request.SegundoApellido,
 1273                    FechaNacimiento = fechaNacimientoUtc,
 1274                    Email = request.Email,
 1275                    Telefono = request.Telefono,
 1276                    Direccion = request.Direccion
 1277                };
 278
 1279                _context.Personas.Add(persona);
 1280                await _context.SaveChangesAsync();
 281            }
 282
 283            // Determinar régimen: explícito si se pasa, sino auto-calcular
 284            long regimenId;
 1285            if (!string.IsNullOrWhiteSpace(request.Ley))
 286            {
 0287                var regimenExplicito = await _context.Regimenes
 0288                    .FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 0289                if (regimenExplicito == null)
 0290                    return (false, null, $"No se encontró el régimen con ley {request.Ley}");
 0291                regimenId = regimenExplicito.Id;
 292            }
 293            else
 294            {
 1295                regimenId = await DeterminarRegimenIdAsync(request.FechaIngreso, request.EscalafonId);
 296            }
 297
 298            // Crear relación laboral - Convertir fechas a UTC
 1299            var fechaIngresoUtc = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 1300                ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 1301                : request.FechaIngreso.Date.ToUniversalTime();
 302
 1303            var relacionLaboral = new RelacionLaboral
 1304            {
 1305                PersonaId = persona.Id,
 1306                RegimenId = regimenId,
 1307                EscalafonId = request.EscalafonId ?? 1,
 1308                GradoId = request.GradoId,
 1309                SituacionId = request.SituacionId ?? 1,
 1310                ProgramaId = request.ProgramaId ?? 1,
 1311                UnidadId = request.UnidadId ?? 1,
 1312                CompaniaId = request.CompaniaId,
 1313                Estado = EstadoRelacion.Activo,
 1314                FechaInicio = fechaIngresoUtc,
 1315                Observaciones = request.Observaciones
 1316            };
 317
 1318            _context.RelacionesLaborales.Add(relacionLaboral);
 1319            await _context.SaveChangesAsync();
 320
 1321            return (true, relacionLaboral.Id, null);
 322        }
 0323        catch (Exception ex)
 324        {
 0325            _logger.LogError(ex, "Error al crear personal");
 0326            var errorMessage = ex.Message;
 0327            var innerEx = ex.InnerException;
 0328            while (innerEx != null)
 329            {
 0330                errorMessage += $" | {innerEx.Message}";
 0331                innerEx = innerEx.InnerException;
 332            }
 0333            return (false, null, $"Error al crear personal: {errorMessage}");
 334        }
 4335    }
 336
 337    public async Task<(bool success, string? error)> UpdatePersonalAsync(long id, UpdatePersonalRequest request)
 338    {
 339        try
 340        {
 3341            var persona = await _context.Personas.FindAsync(id);
 3342            if (persona == null)
 1343                return (false, "Persona no encontrada");
 344
 345            // Validar campos de compensación si se proporcionan
 2346            var errorCompensacion = CompensacionValidator.ValidarCompensaciones(
 2347                request.RiesgoVuelo,
 2348                request.PrimaTecnica,
 2349                request.PrimaSolidariaFamiliar,
 2350                request.AniosInactivos);
 351
 2352            if (errorCompensacion != null)
 353            {
 0354                return (false, errorCompensacion);
 355            }
 356
 357            // Actualizar datos personales
 2358            if (!string.IsNullOrWhiteSpace(request.PrimerNombre))
 1359                persona.PrimerNombre = request.PrimerNombre.Trim();
 360
 2361            if (request.SegundoNombre != null)
 0362                persona.SegundoNombre = string.IsNullOrWhiteSpace(request.SegundoNombre) ? null : request.SegundoNombre.
 363
 2364            if (!string.IsNullOrWhiteSpace(request.PrimerApellido))
 1365                persona.PrimerApellido = request.PrimerApellido.Trim();
 366
 2367            if (request.SegundoApellido != null)
 0368                persona.SegundoApellido = string.IsNullOrWhiteSpace(request.SegundoApellido) ? null : request.SegundoApe
 369
 2370            if (request.FechaNacimiento.HasValue)
 371            {
 0372                var fechaNac = request.FechaNacimiento.Value;
 0373                persona.FechaNacimiento = fechaNac.Kind == DateTimeKind.Unspecified
 0374                    ? DateTime.SpecifyKind(fechaNac.Date, DateTimeKind.Utc)
 0375                    : fechaNac.Date.ToUniversalTime();
 376            }
 377
 2378            if (!string.IsNullOrWhiteSpace(request.Email))
 1379                persona.Email = request.Email.Trim();
 380
 2381            if (request.Telefono != null)
 0382                persona.Telefono = string.IsNullOrWhiteSpace(request.Telefono) ? null : request.Telefono.Trim();
 383
 2384            if (request.Direccion != null)
 0385                persona.Direccion = string.IsNullOrWhiteSpace(request.Direccion) ? null : request.Direccion.Trim();
 386
 2387            if (persona.FechaNacimiento.HasValue)
 388            {
 1389                var fn = persona.FechaNacimiento.Value;
 1390                persona.FechaNacimiento = fn.Kind == DateTimeKind.Unspecified
 1391                    ? DateTime.SpecifyKind(fn.Date, DateTimeKind.Utc)
 1392                    : fn.ToUniversalTime();
 393            }
 394
 2395            persona.FechaActualizacion = DateTime.UtcNow;
 2396            _context.Personas.Update(persona);
 397
 398            // Buscar relación activa para la persona
 2399            var relacionActiva = await _context.RelacionesLaborales
 2400                .FirstOrDefaultAsync(r => r.PersonaId == persona.Id && r.Estado == EstadoRelacion.Activo);
 401
 402            // Obtener la última relación (puede ser null)
 2403            var ultimaRelacion = await _context.RelacionesLaborales
 2404                .Where(r => r.PersonaId == persona.Id)
 2405                .OrderByDescending(r => r.FechaInicio)
 2406                .FirstOrDefaultAsync();
 407
 408            // Validar transición de estado usando la relación más relevante (activa si existe, sino la última)
 2409            var relacionParaValidacion = relacionActiva ?? ultimaRelacion;
 410
 2411            if (!string.IsNullOrWhiteSpace(request.Estado) && relacionParaValidacion != null)
 412            {
 0413                var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 0414                    relacionParaValidacion.Estado.ToString(),
 0415                    request.Estado);
 0416                if (errorTransicion != null)
 0417                    return (false, errorTransicion);
 418            }
 419
 420            // Determinar si se solicitaron cambios laborales
 2421            bool cambioLaboralSolicitado = request.GradoId.HasValue || request.RegimenId.HasValue || request.EscalafonId
 2422                request.SituacionId.HasValue || request.ProgramaId.HasValue || request.UnidadId.HasValue || request.Comp
 423
 2424            if (relacionActiva != null)
 425            {
 426                // Si hay relación activa, verificar si los cambios requieren crear una nueva relación
 2427                bool requiereNueva = false;
 3428                if (request.GradoId.HasValue && request.GradoId.Value != relacionActiva.GradoId) requiereNueva = true;
 2429                if (request.RegimenId.HasValue && request.RegimenId.Value != relacionActiva.RegimenId) requiereNueva = t
 2430                if (request.EscalafonId.HasValue && request.EscalafonId.Value != relacionActiva.EscalafonId) requiereNue
 2431                if (request.SituacionId.HasValue && request.SituacionId.Value != relacionActiva.SituacionId) requiereNue
 2432                if (request.ProgramaId.HasValue && request.ProgramaId.Value != relacionActiva.ProgramaId) requiereNueva 
 2433                if (request.UnidadId.HasValue && request.UnidadId.Value != relacionActiva.UnidadId) requiereNueva = true
 2434                if (request.CompaniaId.HasValue && request.CompaniaId.Value != relacionActiva.CompaniaId) requiereNueva 
 435
 2436                if (requiereNueva)
 437                {
 438                    // Cerrar relación activa
 1439                    relacionActiva.FechaFin = DateTime.UtcNow;
 1440                    relacionActiva.Estado = EstadoRelacion.Inactivo;
 1441                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 442
 443                    // Crear nueva relación activa con los valores actualizados
 1444                    var nuevaRelacion = new RelacionLaboral
 1445                    {
 1446                        PersonaId = persona.Id,
 1447                        RegimenId = request.RegimenId ?? relacionActiva.RegimenId,
 1448                        EscalafonId = request.EscalafonId ?? relacionActiva.EscalafonId,
 1449                        GradoId = request.GradoId ?? relacionActiva.GradoId,
 1450                        SituacionId = request.SituacionId ?? relacionActiva.SituacionId,
 1451                        ProgramaId = request.ProgramaId ?? relacionActiva.ProgramaId,
 1452                        UnidadId = request.UnidadId ?? relacionActiva.UnidadId,
 1453                        CompaniaId = request.CompaniaId ?? relacionActiva.CompaniaId,
 1454                        Estado = EstadoRelacion.Activo,
 1455                        FechaInicio = DateTime.UtcNow,
 1456                        Observaciones = request.Observaciones ?? relacionActiva.Observaciones,
 1457                        FechaActualizacion = DateTime.UtcNow,
 1458                        RiesgoVuelo = request.RiesgoVuelo ?? relacionActiva.RiesgoVuelo,
 1459                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? relacionActiva.PrimaTecnica : r
 1460                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? relacionAct
 1461                        AniosInactivos = request.AniosInactivos ?? relacionActiva.AniosInactivos
 1462                    };
 463
 1464                    _context.RelacionesLaborales.Add(nuevaRelacion);
 465                }
 466                else
 467                {
 468                    // Actualizar en sitio la relación activa
 1469                    if (!string.IsNullOrWhiteSpace(request.Estado))
 470                    {
 0471                        if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0472                            relacionActiva.Estado = est;
 473                        else
 474                        {
 0475                            var estLower = request.Estado.Trim().ToLower();
 0476                            if (estLower == "activo") relacionActiva.Estado = EstadoRelacion.Activo;
 0477                            else if (estLower == "inactivo") relacionActiva.Estado = EstadoRelacion.Inactivo;
 478                        }
 479                    }
 480
 1481                    if (request.RegimenId.HasValue) relacionActiva.RegimenId = request.RegimenId.Value;
 1482                    if (request.EscalafonId.HasValue) relacionActiva.EscalafonId = request.EscalafonId.Value;
 1483                    if (request.GradoId.HasValue) relacionActiva.GradoId = request.GradoId.Value;
 1484                    if (request.SituacionId.HasValue) relacionActiva.SituacionId = request.SituacionId.Value;
 1485                    if (request.ProgramaId.HasValue) relacionActiva.ProgramaId = request.ProgramaId.Value;
 1486                    if (request.UnidadId.HasValue) relacionActiva.UnidadId = request.UnidadId.Value;
 1487                    if (request.CompaniaId.HasValue) relacionActiva.CompaniaId = request.CompaniaId.Value;
 488
 1489                    if (request.RiesgoVuelo.HasValue) relacionActiva.RiesgoVuelo = request.RiesgoVuelo;
 1490                    if (request.PrimaTecnica != null) relacionActiva.PrimaTecnica = string.IsNullOrWhiteSpace(request.Pr
 1491                    if (request.PrimaSolidariaFamiliar != null) relacionActiva.PrimaSolidariaFamiliar = string.IsNullOrW
 1492                    if (request.AniosInactivos.HasValue) relacionActiva.AniosInactivos = request.AniosInactivos;
 1493                    if (request.Observaciones != null) relacionActiva.Observaciones = request.Observaciones;
 494
 1495                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 496                }
 497            }
 498            else
 499            {
 500                // No hay relación activa
 0501                if (cambioLaboralSolicitado)
 502                {
 503                    // Crear nueva relación activa basándose en la última relación si existe
 0504                    var plantilla = ultimaRelacion;
 505
 0506                    var nuevaRelacion = new RelacionLaboral
 0507                    {
 0508                        PersonaId = persona.Id,
 0509                        RegimenId = request.RegimenId ?? plantilla?.RegimenId ?? 0,
 0510                        EscalafonId = request.EscalafonId ?? plantilla?.EscalafonId ?? 0,
 0511                        GradoId = request.GradoId ?? plantilla?.GradoId ?? 0,
 0512                        SituacionId = request.SituacionId ?? plantilla?.SituacionId ?? 0,
 0513                        ProgramaId = request.ProgramaId ?? plantilla?.ProgramaId ?? 0,
 0514                        UnidadId = request.UnidadId ?? plantilla?.UnidadId ?? 0,
 0515                        CompaniaId = request.CompaniaId ?? plantilla?.CompaniaId,
 0516                        Estado = EstadoRelacion.Activo,
 0517                        FechaInicio = DateTime.UtcNow,
 0518                        Observaciones = request.Observaciones ?? plantilla?.Observaciones,
 0519                        FechaActualizacion = DateTime.UtcNow,
 0520                        RiesgoVuelo = request.RiesgoVuelo ?? plantilla?.RiesgoVuelo,
 0521                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? plantilla?.PrimaTecnica : reque
 0522                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? plantilla?.
 0523                        AniosInactivos = request.AniosInactivos ?? plantilla?.AniosInactivos
 0524                    };
 525
 0526                    _context.RelacionesLaborales.Add(nuevaRelacion);
 527                }
 528                else
 529                {
 530                    // Solo actualizar estado u observaciones de la última relación si existe
 0531                    if (ultimaRelacion != null)
 532                    {
 0533                        if (!string.IsNullOrWhiteSpace(request.Estado))
 534                        {
 0535                            if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0536                                ultimaRelacion.Estado = est;
 537                            else
 538                            {
 0539                                var estLower = request.Estado.Trim().ToLower();
 0540                                if (estLower == "activo") ultimaRelacion.Estado = EstadoRelacion.Activo;
 0541                                else if (estLower == "inactivo") ultimaRelacion.Estado = EstadoRelacion.Inactivo;
 542                            }
 543                        }
 544
 0545                        if (request.Observaciones != null) ultimaRelacion.Observaciones = request.Observaciones;
 0546                        ultimaRelacion.FechaActualizacion = DateTime.UtcNow;
 547                    }
 548                }
 549            }
 550
 2551            await _context.SaveChangesAsync();
 552
 2553            return (true, null);
 554        }
 0555        catch (Exception ex)
 556        {
 0557            _logger.LogError(ex, "Error al actualizar personal");
 0558            return (false, $"Error al actualizar personal: {ex.Message}");
 559        }
 3560    }
 561
 562    public async Task<(bool success, string? error)> RegistrarBajaAsync(long id, RegistrarBajaRequest request)
 563    {
 564        try
 565        {
 566            // id is PersonaId: buscar la relación laboral activa de la persona
 4567            var relacionLaboral = await _context.RelacionesLaborales
 4568                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 569
 4570            if (relacionLaboral == null)
 571            {
 1572                return (false, "Relación laboral no encontrada");
 573            }
 574
 575            // Validar motivo de movimiento: debe existir y NO ser una alta
 3576            var tipoMovimiento = await _context.TiposMovimiento
 3577                .FirstOrDefaultAsync(t => t.Id == request.MotivoId);
 578
 3579            if (tipoMovimiento == null)
 580            {
 1581                return (false, "Motivo de baja no encontrado");
 582            }
 583
 2584            if (tipoMovimiento.EsAlta)
 585            {
 1586                return (false, "El motivo proporcionado corresponde a un alta, no puede usarse para una baja");
 587            }
 588
 589            // Validar que la fecha de baja no sea futura
 1590            var errorFechaBaja = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaBaja);
 1591            if (errorFechaBaja != null)
 592            {
 0593                return (false, errorFechaBaja);
 594            }
 595
 596            // Validar fecha de baja vs último movimiento y fecha de inicio
 1597            var validadorMovimiento = new MovimientoLaboralValidator(_context);
 1598            var errorFechaMovimiento = await validadorMovimiento.ValidarFechaMovimiento(relacionLaboral.Id, request.Fech
 1599            if (errorFechaMovimiento != null)
 600            {
 0601                return (false, errorFechaMovimiento);
 602            }
 603
 1604            var errorFechaBajaVsMovimiento = await validadorMovimiento.ValidarFechaBajaVsUltimoMovimiento(relacionLabora
 1605            if (errorFechaBajaVsMovimiento != null)
 606            {
 0607                return (false, errorFechaBajaVsMovimiento);
 608            }
 609
 610            // Validar transición de estado: debe ir de activo a inactivo
 1611            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1612                relacionLaboral.Estado.ToString(),
 1613                "Inactivo");
 614
 1615            if (errorTransicion != null)
 616            {
 0617                return (false, errorTransicion);
 618            }
 619
 620            // Cambiar estado a inactivo
 1621            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1622            var fechaBajaUtc = request.FechaBaja.Kind == DateTimeKind.Unspecified
 1623                ? DateTime.SpecifyKind(request.FechaBaja.Date, DateTimeKind.Utc)
 1624                : request.FechaBaja.Date.ToUniversalTime();
 1625            relacionLaboral.FechaFin = fechaBajaUtc;
 1626            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 627
 628            // Registrar movimiento de baja
 1629            var movimiento = new MovimientoLaboral
 1630            {
 1631                RelacionLaboralId = relacionLaboral.Id,
 1632                TipoMovimientoId = request.MotivoId,
 1633                FechaMovimiento = fechaBajaUtc,
 1634                UsuarioId = 1, // TODO: Obtener del contexto de autenticación
 1635                Observaciones = request.Observaciones
 1636            };
 637
 1638            _context.MovimientosLaborales.Add(movimiento);
 1639            await _context.SaveChangesAsync();
 640
 1641            return (true, null);
 642        }
 0643        catch (Exception ex)
 644        {
 0645            _logger.LogError(ex, "Error al registrar baja");
 0646            return (false, $"Error al registrar baja: {ex.Message}");
 647        }
 4648    }
 649
 650    public async Task<(bool success, string? error)> RegistrarAscensoAsync(long id, AscensoRequest request)
 651    {
 652        try
 653        {
 5654            var relacionLaboral = await _context.RelacionesLaborales
 5655                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 656
 5657            if (relacionLaboral == null)
 1658                return (false, "Relación laboral no encontrada");
 659
 660            // Validar grado objetivo
 4661            var grado = await _context.Grados.FirstOrDefaultAsync(g => g.Id == request.NuevoGradoId && g.Vigente);
 4662            if (grado == null)
 1663                return (false, "Grado no encontrado");
 664
 665            // Validar ley objetivo: ahora se recibe el número de ley como string en request.Ley
 3666            Entidades.Regimen? regimenAscenso = null;
 667
 3668            regimenAscenso = await _context.Regimenes.FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 3669            if (regimenAscenso == null)
 670            {
 1671                return (false, $"No se encontró la ley {request.Ley}");
 672            }
 673
 674            // Validar que el grado objetivo
 2675            if (relacionLaboral.GradoId != 0)
 676            {
 2677                var gradoActual = await _context.Grados.FirstOrDefaultAsync(g => g.Id == relacionLaboral.GradoId);
 2678                if (gradoActual != null)
 679                {
 2680                    if (grado.Orden <= gradoActual.Orden)
 681                    {
 1682                        return (false, "El grado objetivo debe ser superior al grado actual");
 683                    }
 684                }
 685            }
 686
 1687            var fechaAscensoUtc = request.FechaAscenso.Kind == DateTimeKind.Unspecified
 1688                ? DateTime.SpecifyKind(request.FechaAscenso.Date, DateTimeKind.Utc)
 1689                : request.FechaAscenso.Date.ToUniversalTime();
 690
 691            // Validar unidad objetivo si se proporciona
 692            Unidad? unidadObj = null;
 1693            if (request.UnidadId.HasValue)
 694            {
 0695                unidadObj = await _context.Unidades.FirstOrDefaultAsync(u => u.Id == request.UnidadId.Value && u.Vigente
 0696                if (unidadObj == null)
 697                {
 0698                    return (false, "Unidad no válida o no vigente");
 699                }
 700            }
 701
 702            // Cerrar la relación laboral actual
 703            // Validar transición: debe estar activo para poder ascender
 1704            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1705                relacionLaboral.Estado.ToString(),
 1706                "Inactivo");
 707
 1708            if (errorTransicion != null)
 709            {
 0710                return (false, errorTransicion);
 711            }
 712
 1713            relacionLaboral.FechaFin = fechaAscensoUtc;
 1714            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 1715            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1716            relacionLaboral.Observaciones = request.Observacion;
 717
 718            // Crear nueva relación laboral con el nuevo grado
 1719            var nuevaRelacion = new RelacionLaboral
 1720            {
 1721                PersonaId = relacionLaboral.PersonaId,
 1722                RegimenId = regimenAscenso.Id,
 1723                EscalafonId = relacionLaboral.EscalafonId,
 1724                GradoId = request.NuevoGradoId,
 1725                SituacionId = relacionLaboral.SituacionId,
 1726                ProgramaId = relacionLaboral.ProgramaId,
 1727                UnidadId = request.UnidadId ?? relacionLaboral.UnidadId,
 1728                CompaniaId = relacionLaboral.CompaniaId,
 1729                Estado = EstadoRelacion.Activo,
 1730                FechaInicio = fechaAscensoUtc,
 1731                Observaciones = relacionLaboral.Observaciones,
 1732                FechaActualizacion = DateTime.UtcNow
 1733            };
 734
 1735            _context.RelacionesLaborales.Add(nuevaRelacion);
 736
 737            // Registrar movimientos
 738            // Prefer specific tipo_movimiento ids: 6 = Cambio de Situacion (cierre), 7 = Ascenso (alta)
 1739            var tipoCierre = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("cambio
 1740            var tipoAlta = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("ascenso"
 741
 1742            if (tipoCierre == null)
 743            {
 1744                tipoCierre = new Entidades.TipoMovimiento { Nombre = "Cambio de Situacion", EsAlta = false };
 1745                _context.TiposMovimiento.Add(tipoCierre);
 1746                await _context.SaveChangesAsync();
 747            }
 748
 1749            if (tipoAlta == null)
 750            {
 1751                tipoAlta = new Entidades.TipoMovimiento { Nombre = "Ascenso", EsAlta = true };
 1752                _context.TiposMovimiento.Add(tipoAlta);
 1753                await _context.SaveChangesAsync();
 754            }
 755
 756            // Movimiento de cierre
 1757            var movimientoCierre = new Entidades.MovimientoLaboral
 1758            {
 1759                RelacionLaboralId = relacionLaboral.Id,
 1760                TipoMovimientoId = tipoCierre.Id,
 1761                FechaMovimiento = fechaAscensoUtc,
 1762                UsuarioId = 1, // TODO: obtener usuario del contexto
 1763                Observaciones = request.Observacion
 1764            };
 765
 1766            _context.MovimientosLaborales.Add(movimientoCierre);
 767
 1768            await _context.SaveChangesAsync();
 769
 1770            var movimientoAlta = new Entidades.MovimientoLaboral
 1771            {
 1772                RelacionLaboralId = nuevaRelacion.Id,
 1773                TipoMovimientoId = tipoAlta.Id,
 1774                FechaMovimiento = fechaAscensoUtc,
 1775                UsuarioId = 1, // TODO: obtener usuario del contexto
 1776                Observaciones = request.Observacion
 1777            };
 778
 1779            _context.MovimientosLaborales.Add(movimientoAlta);
 780
 1781            await _context.SaveChangesAsync();
 782
 1783            return (true, null);
 784        }
 0785        catch (Exception ex)
 786        {
 0787            _logger.LogError(ex, "Error al registrar ascenso");
 0788            return (false, $"Error al registrar ascenso: {ex.Message}");
 789        }
 5790    }
 791
 792    private IQueryable<RelacionLaboral> ApplyOrdering(
 793        IQueryable<RelacionLaboral> query,
 794        string? orderBy,
 795        string sort)
 796    {
 1797        var ascending = sort.ToLower() == "asc";
 798
 1799        return orderBy?.ToLower() switch
 1800        {
 0801            "ci" => ascending
 0802                ? query.OrderBy(r => r.Persona.Cedula)
 0803                : query.OrderByDescending(r => r.Persona.Cedula),
 0804            "nombre" => ascending
 0805                ? query.OrderBy(r => r.Persona.PrimerNombre)
 0806                : query.OrderByDescending(r => r.Persona.PrimerNombre),
 0807            "apellido" => ascending
 0808                ? query.OrderBy(r => r.Persona.PrimerApellido)
 0809                : query.OrderByDescending(r => r.Persona.PrimerApellido),
 0810            "grado" => ascending
 0811                ? query.OrderBy(r => r.Grado.Orden)
 0812                : query.OrderByDescending(r => r.Grado.Orden),
 0813            "fechaingreso" => ascending
 0814                ? query.OrderBy(r => r.FechaInicio)
 0815                : query.OrderByDescending(r => r.FechaInicio),
 1816            _ => query.OrderByDescending(r => r.FechaInicio)
 1817        };
 818    }
 819
 820    #region Métodos Privados
 821
 822    /// <summary>
 823    /// Determina el régimen (Ley Vieja 14.157 / Ley Nueva 19.695) según la lógica del legacy MONTEPIO.PRG.
 824    /// Fecha límite de la reforma: 28/02/2019.
 825    /// - Escalafón AV (Oficiales): >= 15 años al 28/02/2019 → Ley Vieja
 826    /// - Otros escalafones (Subalternos): >= 3 años al 28/02/2019 → Ley Vieja
 827    /// - Ingreso posterior a la reforma → siempre Ley Nueva
 828    /// </summary>
 829    private async Task<long> DeterminarRegimenIdAsync(DateTime fechaIngreso, long? escalafonId)
 830    {
 1831        var fechaReforma = new DateTime(2019, 2, 28);
 832        string numeroLey;
 833
 1834        if (fechaIngreso.Date >= fechaReforma)
 835        {
 1836            numeroLey = "19.695";
 837        }
 838        else
 839        {
 0840            int aniosAlLimite = fechaReforma.Year - fechaIngreso.Year;
 0841            if (fechaIngreso.Date > fechaReforma.AddYears(-aniosAlLimite)) aniosAlLimite--;
 842
 0843            var escalafon = escalafonId.HasValue
 0844                ? await _context.Escalafones.FindAsync(escalafonId.Value)
 0845                : null;
 0846            bool esOficial = escalafon?.Codigo == "AV";
 0847            int umbral = esOficial ? 15 : 3;
 848
 0849            numeroLey = aniosAlLimite >= umbral ? "14.157" : "19.695";
 850        }
 851
 1852        var regimen = await _context.Regimenes
 1853            .FirstOrDefaultAsync(r => r.NumeroLey == numeroLey && r.Vigente);
 854
 855        // Fallback a Ley Nueva si no se encuentra (no debería ocurrir)
 1856        if (regimen == null)
 857        {
 0858            regimen = await _context.Regimenes
 0859                .FirstOrDefaultAsync(r => r.NumeroLey == "19.695" && r.Vigente);
 860        }
 861
 1862        return regimen!.Id;
 1863    }
 864
 865    #endregion
 866}