< 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: 465
Uncovered lines: 134
Coverable lines: 599
Total lines: 967
Line coverage: 77.6%
Branch coverage
42%
Covered branches: 154
Total branches: 366
Branch coverage: 42%
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()75%88100%
CreatePersonalAsync()47.05%533474.46%
UpdatePersonalAsync()36.4%316222861.64%
RegistrarBajaAsync()65%212085%
RegistrarAscensoAsync()80.76%262692.3%
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 IMotivoBajaRepository _motivoBajaRepository;
 18    private readonly ApplicationDbContext _context;
 19    private readonly ILogger<PersonalService> _logger;
 20    private readonly IAuditoriaService _auditoriaService;
 21    private readonly ICurrentUserContext _currentUser;
 22
 2023    public PersonalService(
 2024        IPersonalRepository personalRepository,
 2025        IMotivoBajaRepository motivoBajaRepository,
 2026        ApplicationDbContext context,
 2027        ILogger<PersonalService> logger,
 2028        IAuditoriaService auditoriaService,
 2029        ICurrentUserContext currentUser)
 30    {
 2031        _personalRepository = personalRepository;
 2032        _motivoBajaRepository = motivoBajaRepository;
 2033        _context = context;
 2034        _logger = logger;
 2035        _auditoriaService = auditoriaService;
 2036        _currentUser = currentUser;
 2037    }
 38
 39    public async Task<object> GetPersonalListAsync(
 40        string? buscar,
 41        long? cargoId,
 42        string? estado,
 43        string? ley,
 44        int page,
 45        int pageSize,
 46        string? orderBy,
 47        string sort)
 48    {
 149        var query = _context.RelacionesLaborales
 150            .Include(r => r.Persona)
 151            .Include(r => r.Regimen)
 152            .Include(r => r.Escalafon)
 153            .Include(r => r.Grado)
 154            .Include(r => r.Situacion)
 155            .AsQueryable();
 56
 57        // Filtro por búsqueda (CI, nombre o apellido)
 158        if (!string.IsNullOrWhiteSpace(buscar))
 59        {
 060            var buscarLower = buscar.ToLower();
 061            query = query.Where(r =>
 062                r.Persona.Cedula.Contains(buscarLower) ||
 063                r.Persona.PrimerNombre.ToLower().Contains(buscarLower) ||
 064                r.Persona.PrimerApellido.ToLower().Contains(buscarLower) ||
 065                (r.Persona.SegundoNombre != null && r.Persona.SegundoNombre.ToLower().Contains(buscarLower)) ||
 066                (r.Persona.SegundoApellido != null && r.Persona.SegundoApellido.ToLower().Contains(buscarLower)));
 67        }
 68
 69        // Filtro por cargo (grado)
 170        if (cargoId.HasValue)
 71        {
 072            query = query.Where(r => r.GradoId == cargoId.Value);
 73        }
 74
 75        // Filtro por estado (parse string to enum)
 176        if (!string.IsNullOrWhiteSpace(estado))
 77        {
 178            if (Enum.TryParse<EstadoRelacion>(estado, true, out var estadoEnum))
 79            {
 180                query = query.Where(r => r.Estado == estadoEnum);
 81            }
 82            else
 83            {
 84                // Si no se puede parsear, intentar aceptar valores comunes
 085                var estLower = estado.Trim().ToLower();
 086                if (estLower == "activo") query = query.Where(r => r.Estado == EstadoRelacion.Activo);
 087                else if (estLower == "inactivo") query = query.Where(r => r.Estado == EstadoRelacion.Inactivo);
 88            }
 89        }
 90
 91        // Filtro por ley (régimen)
 192        if (!string.IsNullOrWhiteSpace(ley))
 93        {
 094            var leyLower = ley.ToLower();
 095            query = query.Where(r => r.Regimen.NumeroLey.ToLower().Contains(leyLower));
 96        }
 97
 198        var total = await query.CountAsync();
 99
 1100        query = ApplyOrdering(query, orderBy, sort);
 101
 102        // Paginación: obtener entidades y luego proyectar a DTO en memoria para poder convertir enums a string
 1103        var relacionesPage = await query
 1104            .Skip((page - 1) * pageSize)
 1105            .Take(pageSize)
 1106            .ToListAsync();
 107
 2108        var items = relacionesPage.Select(r => new PersonalListDto
 2109        {
 2110            Id = r.Id,
 2111            Ci = r.Persona.Cedula,
 2112            personalId = r.PersonaId,
 2113            PrimerNombre = r.Persona.PrimerNombre,
 2114            SegundoNombre = r.Persona.SegundoNombre,
 2115            PrimerApellido = r.Persona.PrimerApellido,
 2116            SegundoApellido = r.Persona.SegundoApellido,
 2117            Cargo = r.Grado.Denominacion,
 2118            Grado = r.Grado.Denominacion,
 2119            Ley = r.Regimen.NumeroLey,
 2120            Estado = r.Estado.ToString(),
 2121            FechaIngreso = r.FechaInicio
 2122        }).ToList();
 123
 1124        return new
 1125        {
 1126            items,
 1127            total,
 1128            page,
 1129            pageSize,
 1130            totalPages = (int)Math.Ceiling((double)total / pageSize)
 1131        };
 1132    }
 133
 134    public async Task<PersonalDetalleDto?> GetDetalleAsync(long id)
 135    {
 136        // id is PersonaId: obtener la relación laboral activa o la última relación laboral
 3137        var relacionLaboral = await _context.RelacionesLaborales
 3138            .Include(r => r.Persona)
 3139            .Include(r => r.Regimen)
 3140            .Include(r => r.Escalafon)
 3141            .Include(r => r.Grado)
 3142            .Include(r => r.Situacion)
 3143            .Include(r => r.Programa)
 3144            .Include(r => r.Unidad)
 3145            .Include(r => r.SubUnidad)
 3146            .Include(r => r.MotivoBaja)
 3147            .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 148
 3149        if (relacionLaboral == null)
 150        {
 2151            relacionLaboral = await _context.RelacionesLaborales
 2152                .Include(r => r.Persona)
 2153                .Include(r => r.Regimen)
 2154                .Include(r => r.Escalafon)
 2155                .Include(r => r.Grado)
 2156                .Include(r => r.Situacion)
 2157                .Include(r => r.Programa)
 2158                .Include(r => r.Unidad)
 2159                .Include(r => r.SubUnidad)
 2160                .Include(r => r.MotivoBaja)
 2161                .Where(r => r.PersonaId == id)
 2162                .OrderByDescending(r => r.FechaInicio)
 2163                .FirstOrDefaultAsync();
 164        }
 165
 3166        if (relacionLaboral == null)
 1167            return null;
 168
 169        // Obtener todos los ids de relación laboral de la persona y luego el historial de movimientos para cualquiera d
 2170        var relacionIds = await _context.RelacionesLaborales
 2171            .Where(r => r.PersonaId == relacionLaboral.PersonaId)
 2172            .Select(r => r.Id)
 2173            .ToListAsync();
 174
 2175        var historial = await _context.MovimientosLaborales
 2176            .Include(m => m.TipoMovimiento)
 2177            .Include(m => m.Usuario)
 2178                .ThenInclude(u => u.Persona)
 2179            .Where(m => relacionIds.Contains(m.RelacionLaboralId))
 2180            .OrderByDescending(m => m.Id)
 2181            .Select(m => new HistorialMovimientoDto
 2182            {
 2183                Id = m.Id,
 2184                TipoMovimiento = m.TipoMovimiento.Nombre,
 2185                EsAlta = m.TipoMovimiento.EsAlta,
 2186                FechaMovimiento = m.FechaMovimiento,
 2187                Observaciones = m.Observaciones,
 2188                RegistradoPor = m.Usuario != null ? m.Usuario.Username : null,
 2189                FechaRegistro = m.FechaMovimiento
 2190            })
 2191            .ToListAsync();
 192
 2193        return new PersonalDetalleDto
 2194        {
 2195            Id = relacionLaboral.Id,
 2196            DatosPersonales = new DatosPersonalesDto
 2197            {
 2198                PersonaId = relacionLaboral.PersonaId,
 2199                Ci = relacionLaboral.Persona.Cedula,
 2200                PrimerNombre = relacionLaboral.Persona.PrimerNombre,
 2201                SegundoNombre = relacionLaboral.Persona.SegundoNombre,
 2202                PrimerApellido = relacionLaboral.Persona.PrimerApellido,
 2203                SegundoApellido = relacionLaboral.Persona.SegundoApellido,
 2204                FechaNacimiento = relacionLaboral.Persona.FechaNacimiento,
 2205                Email = relacionLaboral.Persona.Email,
 2206                Telefono = relacionLaboral.Persona.Telefono,
 2207                Direccion = relacionLaboral.Persona.Direccion,
 2208                CodigoPostal = relacionLaboral.Persona.CodigoPostal,
 2209                Departamento = relacionLaboral.Persona.Departamento,
 2210                Localidad = relacionLaboral.Persona.Localidad
 2211            },
 2212            DatosLaborales = new DatosLaboralesDto
 2213            {
 2214                RelacionLaboralId = relacionLaboral.Id,
 2215                Estado = relacionLaboral.Estado.ToString(),
 2216                FechaInicio = relacionLaboral.FechaInicio,
 2217                FechaFin = relacionLaboral.FechaFin,
 2218                RegimenId = relacionLaboral.RegimenId,
 2219                Regimen = relacionLaboral.Regimen.Denominacion,
 2220                Ley = relacionLaboral.Regimen.NumeroLey,
 2221                EscalafonId = relacionLaboral.EscalafonId,
 2222                Escalafon = relacionLaboral.Escalafon.Denominacion,
 2223                GradoId = relacionLaboral.GradoId,
 2224                Grado = relacionLaboral.Grado.Denominacion,
 2225                Cargo = relacionLaboral.Grado.Denominacion,
 2226                SituacionId = relacionLaboral.SituacionId,
 2227                Situacion = relacionLaboral.Situacion.Denominacion,
 2228                ProgramaId = relacionLaboral.ProgramaId,
 2229                Programa = relacionLaboral.Programa.Denominacion,
 2230                UnidadId = relacionLaboral.UnidadId,
 2231                Unidad = relacionLaboral.Unidad.Denominacion,
 2232                SubUnidadId = relacionLaboral.SubUnidadId,
 2233                SubUnidad = relacionLaboral.SubUnidad?.Denominacion,
 2234                MotivoBajaId = relacionLaboral.MotivoBajaId,
 2235                MotivoBaja = relacionLaboral.MotivoBaja?.Denominacion,
 2236                RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 2237                PrimaTecnica = relacionLaboral.PrimaTecnica,
 2238                PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 2239                AniosInactivos = relacionLaboral.AniosInactivos,
 2240                Observaciones = relacionLaboral.Observaciones
 2241            },
 2242            Historial = historial
 2243        };
 3244    }
 245
 246    public async Task<(bool success, long? personalId, string? error)> CreatePersonalAsync(CreatePersonalRequest request
 247    {
 248        try
 249        {
 250            // Validar y limpiar CI
 4251            var cedulaLimpia = CedulaValidator.Limpiar(request.Ci);
 4252            if (!CedulaValidator.Validar(cedulaLimpia))
 253            {
 1254                return (false, null, "La cédula ingresada no es válida.");
 255            }
 256
 257            // Validar que la fecha de ingreso no sea futura
 3258            var errorFechaIngreso = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaIngreso);
 3259            if (errorFechaIngreso != null)
 260            {
 1261                return (false, null, errorFechaIngreso);
 262            }
 263
 264            // Validar que no exista una relación laboral activa para esa cédula
 2265            var validadorRelacion = new RelacionLaboralValidator(_context);
 2266            var errorRelacionActiva = await validadorRelacion.ObtenerErrorSiExisteRelacionActiva(cedulaLimpia);
 2267            if (errorRelacionActiva != null)
 268            {
 1269                return (false, null, errorRelacionActiva);
 270            }
 271
 272            // Validar que el grado pertenece al escalafón indicado
 1273            var escalafonId = request.EscalafonId ?? 1;
 1274            var grado = await _context.Grados.FindAsync(request.GradoId);
 1275            if (grado == null)
 0276                return (false, null, "El grado seleccionado no existe.");
 277            // TODO: reactivar validación cuando el frontend envíe EscalafonId correctamente
 278            // if (grado.EscalafonId != escalafonId)
 279            //     return (false, null, "El grado seleccionado no pertenece al escalafón indicado.");
 280
 281            // Validar fecha de nacimiento
 1282            if (request.FechaNacimiento != default)
 283            {
 1284                var fechaNac = request.FechaNacimiento.Date;
 1285                if (fechaNac > DateTime.UtcNow.Date)
 0286                    return (false, null, "La fecha de nacimiento no puede ser futura.");
 1287                if (fechaNac < new DateTime(1940, 1, 1))
 0288                    return (false, null, "La fecha de nacimiento no puede ser anterior a 01/01/1940.");
 289            }
 290
 291            // Buscar o crear persona
 1292            Persona? persona = await _context.Personas.FirstOrDefaultAsync(p => p.Cedula == cedulaLimpia);
 293
 1294            if (persona == null)
 295            {
 296                // Crear persona - Convertir fechas a UTC
 1297                var fechaNacimientoUtc = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 1298                    ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 1299                    : request.FechaNacimiento.Date.ToUniversalTime();
 300
 1301                persona = new Persona
 1302                {
 1303                    Cedula = cedulaLimpia,
 1304                    PrimerNombre = request.PrimerNombre,
 1305                    SegundoNombre = request.SegundoNombre,
 1306                    PrimerApellido = request.PrimerApellido,
 1307                    SegundoApellido = request.SegundoApellido,
 1308                    FechaNacimiento = fechaNacimientoUtc,
 1309                    Email = request.Email,
 1310                    Telefono = request.Telefono,
 1311                    Direccion = request.Direccion,
 1312                    CodigoPostal = request.CodigoPostal,
 1313                    Departamento = request.Departamento,
 1314                    Localidad = request.Localidad
 1315                };
 316
 1317                _context.Personas.Add(persona);
 1318                await _context.SaveChangesAsync();
 319            }
 320            else
 321            {
 322                // Persona ya existe: actualizar campos de dirección si vienen en el request
 0323                bool personaModificada = false;
 0324                if (request.CodigoPostal != null) { persona.CodigoPostal = request.CodigoPostal; personaModificada = tru
 0325                if (request.Departamento != null) { persona.Departamento = request.Departamento; personaModificada = tru
 0326                if (request.Localidad != null) { persona.Localidad = request.Localidad; personaModificada = true; }
 0327                if (personaModificada)
 328                {
 0329                    persona.FechaActualizacion = DateTime.UtcNow;
 0330                    _context.Personas.Update(persona);
 0331                    await _context.SaveChangesAsync();
 332                }
 333            }
 334
 335            // Determinar régimen: explícito si se pasa, sino auto-calcular
 336            long regimenId;
 1337            if (!string.IsNullOrWhiteSpace(request.Ley))
 338            {
 0339                var regimenExplicito = await _context.Regimenes
 0340                    .FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 0341                if (regimenExplicito == null)
 0342                    return (false, null, $"No se encontró el régimen con ley {request.Ley}");
 0343                regimenId = regimenExplicito.Id;
 344            }
 345            else
 346            {
 1347                regimenId = await DeterminarRegimenIdAsync(request.FechaIngreso, request.EscalafonId);
 348            }
 349
 350            // Crear relación laboral - Convertir fechas a UTC
 1351            var fechaIngresoUtc = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 1352                ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 1353                : request.FechaIngreso.Date.ToUniversalTime();
 354
 1355            var relacionLaboral = new RelacionLaboral
 1356            {
 1357                PersonaId = persona.Id,
 1358                RegimenId = regimenId,
 1359                EscalafonId = request.EscalafonId ?? 1,
 1360                GradoId = request.GradoId,
 1361                SituacionId = request.SituacionId ?? 1,
 1362                ProgramaId = request.ProgramaId ?? 1,
 1363                UnidadId = request.UnidadId ?? 1,
 1364                SubUnidadId = request.SubUnidadId,
 1365                Estado = EstadoRelacion.Activo,
 1366                FechaInicio = fechaIngresoUtc,
 1367                Observaciones = request.Observaciones
 1368            };
 369
 1370            _context.RelacionesLaborales.Add(relacionLaboral);
 1371            await _context.SaveChangesAsync();
 372
 1373            await _auditoriaService.LogAuditoriaAsync(
 1374                usuarioId: _currentUser.UserId ?? 0,
 1375                AccionEnum.CrearPersonal,
 1376                ContextoEnum.Personal,
 1377                host: _currentUser.Host,
 1378                entidad: nameof(Persona),
 1379                entidadId: persona.Id,
 1380                detalle: new { relacionLaboralId = relacionLaboral.Id, personaId = persona.Id, cedula = persona.Cedula }
 381
 1382            return (true, relacionLaboral.Id, null);
 383        }
 0384        catch (Exception ex)
 385        {
 0386            _logger.LogError(ex, "Error al crear personal");
 0387            var errorMessage = ex.Message;
 0388            var innerEx = ex.InnerException;
 0389            while (innerEx != null)
 390            {
 0391                errorMessage += $" | {innerEx.Message}";
 0392                innerEx = innerEx.InnerException;
 393            }
 0394            return (false, null, $"Error al crear personal: {errorMessage}");
 395        }
 4396    }
 397
 398    public async Task<(bool success, string? error)> UpdatePersonalAsync(long id, UpdatePersonalRequest request)
 399    {
 400        try
 401        {
 3402            var persona = await _context.Personas.FindAsync(id);
 3403            if (persona == null)
 1404                return (false, "Persona no encontrada");
 405
 406            // Validar campos de compensación si se proporcionan
 2407            var errorCompensacion = CompensacionValidator.ValidarCompensaciones(
 2408                request.RiesgoVuelo,
 2409                request.PrimaTecnica,
 2410                request.PrimaSolidariaFamiliar,
 2411                request.AniosInactivos);
 412
 2413            if (errorCompensacion != null)
 414            {
 0415                return (false, errorCompensacion);
 416            }
 417
 418            // Actualizar datos personales
 2419            if (!string.IsNullOrWhiteSpace(request.PrimerNombre))
 1420                persona.PrimerNombre = request.PrimerNombre.Trim();
 421
 2422            if (request.SegundoNombre != null)
 0423                persona.SegundoNombre = string.IsNullOrWhiteSpace(request.SegundoNombre) ? null : request.SegundoNombre.
 424
 2425            if (!string.IsNullOrWhiteSpace(request.PrimerApellido))
 1426                persona.PrimerApellido = request.PrimerApellido.Trim();
 427
 2428            if (request.SegundoApellido != null)
 0429                persona.SegundoApellido = string.IsNullOrWhiteSpace(request.SegundoApellido) ? null : request.SegundoApe
 430
 2431            if (request.FechaNacimiento.HasValue)
 432            {
 0433                var fechaNac = request.FechaNacimiento.Value;
 0434                persona.FechaNacimiento = fechaNac.Kind == DateTimeKind.Unspecified
 0435                    ? DateTime.SpecifyKind(fechaNac.Date, DateTimeKind.Utc)
 0436                    : fechaNac.Date.ToUniversalTime();
 437            }
 438
 2439            if (!string.IsNullOrWhiteSpace(request.Email))
 1440                persona.Email = request.Email.Trim();
 441
 2442            if (request.Telefono != null)
 0443                persona.Telefono = string.IsNullOrWhiteSpace(request.Telefono) ? null : request.Telefono.Trim();
 444
 2445            if (request.Direccion != null)
 0446                persona.Direccion = string.IsNullOrWhiteSpace(request.Direccion) ? null : request.Direccion.Trim();
 447
 2448            if (request.CodigoPostal != null)
 0449                persona.CodigoPostal = string.IsNullOrWhiteSpace(request.CodigoPostal) ? null : request.CodigoPostal.Tri
 450
 2451            if (request.Departamento != null)
 0452                persona.Departamento = string.IsNullOrWhiteSpace(request.Departamento) ? null : request.Departamento.Tri
 453
 2454            if (request.Localidad != null)
 0455                persona.Localidad = string.IsNullOrWhiteSpace(request.Localidad) ? null : request.Localidad.Trim();
 456
 2457            if (persona.FechaNacimiento.HasValue)
 458            {
 1459                var fn = persona.FechaNacimiento.Value;
 1460                persona.FechaNacimiento = fn.Kind == DateTimeKind.Unspecified
 1461                    ? DateTime.SpecifyKind(fn.Date, DateTimeKind.Utc)
 1462                    : fn.ToUniversalTime();
 463            }
 464
 2465            persona.FechaActualizacion = DateTime.UtcNow;
 2466            _context.Personas.Update(persona);
 467
 468            // Buscar relación activa para la persona
 2469            var relacionActiva = await _context.RelacionesLaborales
 2470                .FirstOrDefaultAsync(r => r.PersonaId == persona.Id && r.Estado == EstadoRelacion.Activo);
 471
 472            // Obtener la última relación (puede ser null)
 2473            var ultimaRelacion = await _context.RelacionesLaborales
 2474                .Where(r => r.PersonaId == persona.Id)
 2475                .OrderByDescending(r => r.FechaInicio)
 2476                .FirstOrDefaultAsync();
 477
 478            // Validar transición de estado usando la relación más relevante (activa si existe, sino la última)
 2479            var relacionParaValidacion = relacionActiva ?? ultimaRelacion;
 480
 2481            if (!string.IsNullOrWhiteSpace(request.Estado) && relacionParaValidacion != null)
 482            {
 0483                var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 0484                    relacionParaValidacion.Estado.ToString(),
 0485                    request.Estado);
 0486                if (errorTransicion != null)
 0487                    return (false, errorTransicion);
 488            }
 489
 490            // Determinar si se solicitaron cambios laborales
 2491            bool cambioLaboralSolicitado = request.GradoId.HasValue || request.RegimenId.HasValue || request.EscalafonId
 2492                request.SituacionId.HasValue || request.ProgramaId.HasValue || request.UnidadId.HasValue || request.SubU
 493
 2494            if (relacionActiva != null)
 495            {
 496                // Si hay relación activa, verificar si los cambios requieren crear una nueva relación
 2497                bool requiereNueva = false;
 3498                if (request.GradoId.HasValue && request.GradoId.Value != relacionActiva.GradoId) requiereNueva = true;
 2499                if (request.RegimenId.HasValue && request.RegimenId.Value != relacionActiva.RegimenId) requiereNueva = t
 2500                if (request.EscalafonId.HasValue && request.EscalafonId.Value != relacionActiva.EscalafonId) requiereNue
 2501                if (request.SituacionId.HasValue && request.SituacionId.Value != relacionActiva.SituacionId) requiereNue
 2502                if (request.ProgramaId.HasValue && request.ProgramaId.Value != relacionActiva.ProgramaId) requiereNueva 
 2503                if (request.UnidadId.HasValue && request.UnidadId.Value != relacionActiva.UnidadId) requiereNueva = true
 2504                if (request.SubUnidadId.HasValue && request.SubUnidadId.Value != relacionActiva.SubUnidadId) requiereNue
 505
 2506                if (requiereNueva)
 507                {
 508                    // Cerrar relación activa
 1509                    relacionActiva.FechaFin = DateTime.UtcNow;
 1510                    relacionActiva.Estado = EstadoRelacion.Inactivo;
 1511                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 512
 513                    // Crear nueva relación activa con los valores actualizados
 1514                    var nuevaRelacion = new RelacionLaboral
 1515                    {
 1516                        PersonaId = persona.Id,
 1517                        RegimenId = request.RegimenId ?? relacionActiva.RegimenId,
 1518                        EscalafonId = request.EscalafonId ?? relacionActiva.EscalafonId,
 1519                        GradoId = request.GradoId ?? relacionActiva.GradoId,
 1520                        SituacionId = request.SituacionId ?? relacionActiva.SituacionId,
 1521                        ProgramaId = request.ProgramaId ?? relacionActiva.ProgramaId,
 1522                        UnidadId = request.UnidadId ?? relacionActiva.UnidadId,
 1523                        SubUnidadId = request.SubUnidadId ?? relacionActiva.SubUnidadId,
 1524                        Estado = EstadoRelacion.Activo,
 1525                        FechaInicio = DateTime.UtcNow,
 1526                        Observaciones = request.Observaciones ?? relacionActiva.Observaciones,
 1527                        FechaActualizacion = DateTime.UtcNow,
 1528                        RiesgoVuelo = request.RiesgoVuelo ?? relacionActiva.RiesgoVuelo,
 1529                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? relacionActiva.PrimaTecnica : r
 1530                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? relacionAct
 1531                        AniosInactivos = request.AniosInactivos ?? relacionActiva.AniosInactivos
 1532                    };
 533
 1534                    _context.RelacionesLaborales.Add(nuevaRelacion);
 535                }
 536                else
 537                {
 538                    // Actualizar en sitio la relación activa
 1539                    if (!string.IsNullOrWhiteSpace(request.Estado))
 540                    {
 0541                        if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0542                            relacionActiva.Estado = est;
 543                        else
 544                        {
 0545                            var estLower = request.Estado.Trim().ToLower();
 0546                            if (estLower == "activo") relacionActiva.Estado = EstadoRelacion.Activo;
 0547                            else if (estLower == "inactivo") relacionActiva.Estado = EstadoRelacion.Inactivo;
 548                        }
 549                    }
 550
 1551                    if (request.RegimenId.HasValue) relacionActiva.RegimenId = request.RegimenId.Value;
 1552                    if (request.EscalafonId.HasValue) relacionActiva.EscalafonId = request.EscalafonId.Value;
 1553                    if (request.GradoId.HasValue) relacionActiva.GradoId = request.GradoId.Value;
 1554                    if (request.SituacionId.HasValue) relacionActiva.SituacionId = request.SituacionId.Value;
 1555                    if (request.ProgramaId.HasValue) relacionActiva.ProgramaId = request.ProgramaId.Value;
 1556                    if (request.UnidadId.HasValue) relacionActiva.UnidadId = request.UnidadId.Value;
 1557                    if (request.SubUnidadId.HasValue) relacionActiva.SubUnidadId = request.SubUnidadId.Value;
 558
 1559                    if (request.RiesgoVuelo.HasValue) relacionActiva.RiesgoVuelo = request.RiesgoVuelo;
 1560                    if (request.PrimaTecnica != null) relacionActiva.PrimaTecnica = string.IsNullOrWhiteSpace(request.Pr
 1561                    if (request.PrimaSolidariaFamiliar != null) relacionActiva.PrimaSolidariaFamiliar = string.IsNullOrW
 1562                    if (request.AniosInactivos.HasValue) relacionActiva.AniosInactivos = request.AniosInactivos;
 1563                    if (request.Observaciones != null) relacionActiva.Observaciones = request.Observaciones;
 564
 1565                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 566                }
 567            }
 568            else
 569            {
 570                // No hay relación activa
 0571                if (cambioLaboralSolicitado)
 572                {
 573                    // Crear nueva relación activa basándose en la última relación si existe
 0574                    var plantilla = ultimaRelacion;
 575
 0576                    var nuevaRelacion = new RelacionLaboral
 0577                    {
 0578                        PersonaId = persona.Id,
 0579                        RegimenId = request.RegimenId ?? plantilla?.RegimenId ?? 0,
 0580                        EscalafonId = request.EscalafonId ?? plantilla?.EscalafonId ?? 0,
 0581                        GradoId = request.GradoId ?? plantilla?.GradoId ?? 0,
 0582                        SituacionId = request.SituacionId ?? plantilla?.SituacionId ?? 0,
 0583                        ProgramaId = request.ProgramaId ?? plantilla?.ProgramaId ?? 0,
 0584                        UnidadId = request.UnidadId ?? plantilla?.UnidadId ?? 0,
 0585                        SubUnidadId = request.SubUnidadId ?? plantilla?.SubUnidadId,
 0586                        Estado = EstadoRelacion.Activo,
 0587                        FechaInicio = DateTime.UtcNow,
 0588                        Observaciones = request.Observaciones ?? plantilla?.Observaciones,
 0589                        FechaActualizacion = DateTime.UtcNow,
 0590                        RiesgoVuelo = request.RiesgoVuelo ?? plantilla?.RiesgoVuelo,
 0591                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? plantilla?.PrimaTecnica : reque
 0592                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? plantilla?.
 0593                        AniosInactivos = request.AniosInactivos ?? plantilla?.AniosInactivos
 0594                    };
 595
 0596                    _context.RelacionesLaborales.Add(nuevaRelacion);
 597                }
 598                else
 599                {
 600                    // Solo actualizar estado u observaciones de la última relación si existe
 0601                    if (ultimaRelacion != null)
 602                    {
 0603                        if (!string.IsNullOrWhiteSpace(request.Estado))
 604                        {
 0605                            if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0606                                ultimaRelacion.Estado = est;
 607                            else
 608                            {
 0609                                var estLower = request.Estado.Trim().ToLower();
 0610                                if (estLower == "activo") ultimaRelacion.Estado = EstadoRelacion.Activo;
 0611                                else if (estLower == "inactivo") ultimaRelacion.Estado = EstadoRelacion.Inactivo;
 612                            }
 613                        }
 614
 0615                        if (request.Observaciones != null) ultimaRelacion.Observaciones = request.Observaciones;
 0616                        ultimaRelacion.FechaActualizacion = DateTime.UtcNow;
 617                    }
 618                }
 619            }
 620
 2621            await _context.SaveChangesAsync();
 622
 2623            return (true, null);
 624        }
 0625        catch (Exception ex)
 626        {
 0627            _logger.LogError(ex, "Error al actualizar personal");
 0628            return (false, $"Error al actualizar personal: {ex.Message}");
 629        }
 3630    }
 631
 632    public async Task<(bool success, string? error)> RegistrarBajaAsync(long id, RegistrarBajaRequest request)
 633    {
 634        try
 635        {
 636            // id is PersonaId: buscar la relación laboral activa de la persona
 4637            var relacionLaboral = await _context.RelacionesLaborales
 4638                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 639
 4640            if (relacionLaboral == null)
 641            {
 1642                return (false, "Relación laboral no encontrada");
 643            }
 644
 645            // Validar motivo de movimiento: debe existir y NO ser una alta
 3646            var tipoMovimiento = await _context.TiposMovimiento
 3647                .FirstOrDefaultAsync(t => t.Id == request.MotivoId);
 648
 3649            if (tipoMovimiento == null)
 650            {
 1651                return (false, "Motivo de baja no encontrado");
 652            }
 653
 2654            if (tipoMovimiento.EsAlta)
 655            {
 1656                return (false, "El motivo proporcionado corresponde a un alta, no puede usarse para una baja");
 657            }
 658
 659            // Validar MotivoBaja (catálogo)
 1660            if (request.MotivoBajaId <= 0)
 661            {
 0662                return (false, "El motivo de baja (catálogo) es requerido");
 663            }
 664
 1665            var motivoBajaExiste = await _motivoBajaRepository.ExistsAsync(request.MotivoBajaId);
 1666            if (!motivoBajaExiste)
 667            {
 0668                return (false, "Motivo de baja (catálogo) no encontrado o no vigente");
 669            }
 670
 671            // Validar que la fecha de baja no sea futura
 1672            var errorFechaBaja = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaBaja);
 1673            if (errorFechaBaja != null)
 674            {
 0675                return (false, errorFechaBaja);
 676            }
 677
 678            // Validar fecha de baja vs último movimiento y fecha de inicio
 1679            var validadorMovimiento = new MovimientoLaboralValidator(_context);
 1680            var errorFechaMovimiento = await validadorMovimiento.ValidarFechaMovimiento(relacionLaboral.Id, request.Fech
 1681            if (errorFechaMovimiento != null)
 682            {
 0683                return (false, errorFechaMovimiento);
 684            }
 685
 1686            var errorFechaBajaVsMovimiento = await validadorMovimiento.ValidarFechaBajaVsUltimoMovimiento(relacionLabora
 1687            if (errorFechaBajaVsMovimiento != null)
 688            {
 0689                return (false, errorFechaBajaVsMovimiento);
 690            }
 691
 692            // Validar transición de estado: debe ir de activo a inactivo
 1693            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1694                relacionLaboral.Estado.ToString(),
 1695                "Inactivo");
 696
 1697            if (errorTransicion != null)
 698            {
 0699                return (false, errorTransicion);
 700            }
 701
 702            // Cambiar estado a inactivo
 1703            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1704            var fechaBajaUtc = request.FechaBaja.Kind == DateTimeKind.Unspecified
 1705                ? DateTime.SpecifyKind(request.FechaBaja.Date, DateTimeKind.Utc)
 1706                : request.FechaBaja.Date.ToUniversalTime();
 1707            relacionLaboral.FechaFin = fechaBajaUtc;
 1708            relacionLaboral.MotivoBajaId = request.MotivoBajaId;
 1709            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 710
 711            // Registrar movimiento de baja
 1712            var movimiento = new MovimientoLaboral
 1713            {
 1714                RelacionLaboralId = relacionLaboral.Id,
 1715                TipoMovimientoId = request.MotivoId,
 1716                FechaMovimiento = fechaBajaUtc,
 1717                UsuarioId = _currentUser.UserId ?? 0,
 1718                Observaciones = request.Observaciones
 1719            };
 720
 1721            _context.MovimientosLaborales.Add(movimiento);
 1722            await _context.SaveChangesAsync();
 723
 1724            await _auditoriaService.LogAuditoriaAsync(
 1725                usuarioId: _currentUser.UserId ?? 0,
 1726                AccionEnum.BajaPersonal,
 1727                ContextoEnum.Personal,
 1728                host: _currentUser.Host,
 1729                entidad: nameof(RelacionLaboral),
 1730                entidadId: relacionLaboral.Id,
 1731                detalle: new { personaId = id, relacionLaboralId = relacionLaboral.Id, request });
 732
 1733            return (true, null);
 734        }
 0735        catch (Exception ex)
 736        {
 0737            _logger.LogError(ex, "Error al registrar baja");
 0738            return (false, $"Error al registrar baja: {ex.Message}");
 739        }
 4740    }
 741
 742    public async Task<(bool success, string? error)> RegistrarAscensoAsync(long id, AscensoRequest request)
 743    {
 744        try
 745        {
 5746            var relacionLaboral = await _context.RelacionesLaborales
 5747                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 748
 5749            if (relacionLaboral == null)
 1750                return (false, "Relación laboral no encontrada");
 751
 752            // Validar grado objetivo
 4753            var grado = await _context.Grados.FirstOrDefaultAsync(g => g.Id == request.NuevoGradoId && g.Vigente);
 4754            if (grado == null)
 1755                return (false, "Grado no encontrado");
 756
 757            // Validar ley objetivo: ahora se recibe el número de ley como string en request.Ley
 3758            Entidades.Regimen? regimenAscenso = null;
 759
 3760            regimenAscenso = await _context.Regimenes.FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 3761            if (regimenAscenso == null)
 762            {
 1763                return (false, $"No se encontró la ley {request.Ley}");
 764            }
 765
 766            // Validar que el grado objetivo
 2767            if (relacionLaboral.GradoId != 0)
 768            {
 2769                var gradoActual = await _context.Grados.FirstOrDefaultAsync(g => g.Id == relacionLaboral.GradoId);
 2770                if (gradoActual != null)
 771                {
 2772                    if (grado.Orden <= gradoActual.Orden)
 773                    {
 1774                        return (false, "El grado objetivo debe ser superior al grado actual");
 775                    }
 776                }
 777            }
 778
 1779            var fechaAscensoUtc = request.FechaAscenso.Kind == DateTimeKind.Unspecified
 1780                ? DateTime.SpecifyKind(request.FechaAscenso.Date, DateTimeKind.Utc)
 1781                : request.FechaAscenso.Date.ToUniversalTime();
 782
 783            // Validar unidad objetivo si se proporciona
 784            Unidad? unidadObj = null;
 1785            if (request.UnidadId.HasValue)
 786            {
 0787                unidadObj = await _context.Unidades.FirstOrDefaultAsync(u => u.Id == request.UnidadId.Value && u.Vigente
 0788                if (unidadObj == null)
 789                {
 0790                    return (false, "Unidad no válida o no vigente");
 791                }
 792            }
 793
 794            // Cerrar la relación laboral actual
 795            // Validar transición: debe estar activo para poder ascender
 1796            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1797                relacionLaboral.Estado.ToString(),
 1798                "Inactivo");
 799
 1800            if (errorTransicion != null)
 801            {
 0802                return (false, errorTransicion);
 803            }
 804
 1805            relacionLaboral.FechaFin = fechaAscensoUtc;
 1806            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 1807            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1808            relacionLaboral.Observaciones = request.Observacion;
 809
 810            // Crear nueva relación laboral con el nuevo grado
 1811            var nuevaRelacion = new RelacionLaboral
 1812            {
 1813                PersonaId = relacionLaboral.PersonaId,
 1814                RegimenId = regimenAscenso.Id,
 1815                EscalafonId = relacionLaboral.EscalafonId,
 1816                GradoId = request.NuevoGradoId,
 1817                SituacionId = relacionLaboral.SituacionId,
 1818                ProgramaId = relacionLaboral.ProgramaId,
 1819                UnidadId = request.UnidadId ?? relacionLaboral.UnidadId,
 1820                SubUnidadId = relacionLaboral.SubUnidadId,
 1821                Estado = EstadoRelacion.Activo,
 1822                FechaInicio = fechaAscensoUtc,
 1823                Observaciones = relacionLaboral.Observaciones,
 1824                FechaActualizacion = DateTime.UtcNow
 1825            };
 826
 1827            _context.RelacionesLaborales.Add(nuevaRelacion);
 828
 829            // Registrar movimientos
 830            // Prefer specific tipo_movimiento ids: 6 = Cambio de Situacion (cierre), 7 = Ascenso (alta)
 1831            var tipoCierre = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("cambio
 1832            var tipoAlta = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("ascenso"
 833
 1834            if (tipoCierre == null)
 835            {
 1836                tipoCierre = new Entidades.TipoMovimiento { Nombre = "Cambio de Situacion", EsAlta = false };
 1837                _context.TiposMovimiento.Add(tipoCierre);
 1838                await _context.SaveChangesAsync();
 839            }
 840
 1841            if (tipoAlta == null)
 842            {
 1843                tipoAlta = new Entidades.TipoMovimiento { Nombre = "Ascenso", EsAlta = true };
 1844                _context.TiposMovimiento.Add(tipoAlta);
 1845                await _context.SaveChangesAsync();
 846            }
 847
 848            // Movimiento de cierre
 1849            var movimientoCierre = new Entidades.MovimientoLaboral
 1850            {
 1851                RelacionLaboralId = relacionLaboral.Id,
 1852                TipoMovimientoId = tipoCierre.Id,
 1853                FechaMovimiento = fechaAscensoUtc,
 1854                UsuarioId = _currentUser.UserId ?? 0,
 1855                Observaciones = request.Observacion
 1856            };
 857
 1858            _context.MovimientosLaborales.Add(movimientoCierre);
 859
 1860            await _context.SaveChangesAsync();
 861
 1862            var movimientoAlta = new Entidades.MovimientoLaboral
 1863            {
 1864                RelacionLaboralId = nuevaRelacion.Id,
 1865                TipoMovimientoId = tipoAlta.Id,
 1866                FechaMovimiento = fechaAscensoUtc,
 1867                UsuarioId = _currentUser.UserId ?? 0,
 1868                Observaciones = request.Observacion
 1869            };
 870
 1871            _context.MovimientosLaborales.Add(movimientoAlta);
 872
 1873            await _context.SaveChangesAsync();
 874
 1875            await _auditoriaService.LogAuditoriaAsync(
 1876                usuarioId: _currentUser.UserId ?? 0,
 1877                AccionEnum.AscensoPersonal,
 1878                ContextoEnum.Personal,
 1879                host: _currentUser.Host,
 1880                entidad: nameof(RelacionLaboral),
 1881                entidadId: nuevaRelacion.Id,
 1882                detalle: new { personaId = id, relacionAnteriorId = relacionLaboral.Id, relacionNuevaId = nuevaRelacion.
 883
 1884            return (true, null);
 885        }
 0886        catch (Exception ex)
 887        {
 0888            _logger.LogError(ex, "Error al registrar ascenso");
 0889            return (false, $"Error al registrar ascenso: {ex.Message}");
 890        }
 5891    }
 892
 893    private IQueryable<RelacionLaboral> ApplyOrdering(
 894        IQueryable<RelacionLaboral> query,
 895        string? orderBy,
 896        string sort)
 897    {
 1898        var ascending = sort.ToLower() == "asc";
 899
 1900        return orderBy?.ToLower() switch
 1901        {
 0902            "ci" => ascending
 0903                ? query.OrderBy(r => r.Persona.Cedula)
 0904                : query.OrderByDescending(r => r.Persona.Cedula),
 0905            "nombre" => ascending
 0906                ? query.OrderBy(r => r.Persona.PrimerNombre)
 0907                : query.OrderByDescending(r => r.Persona.PrimerNombre),
 0908            "apellido" => ascending
 0909                ? query.OrderBy(r => r.Persona.PrimerApellido)
 0910                : query.OrderByDescending(r => r.Persona.PrimerApellido),
 0911            "grado" => ascending
 0912                ? query.OrderBy(r => r.Grado.Orden)
 0913                : query.OrderByDescending(r => r.Grado.Orden),
 0914            "fechaingreso" => ascending
 0915                ? query.OrderBy(r => r.FechaInicio)
 0916                : query.OrderByDescending(r => r.FechaInicio),
 1917            _ => query.OrderByDescending(r => r.FechaInicio)
 1918        };
 919    }
 920
 921    #region Métodos Privados
 922
 923    /// <summary>
 924    /// Determina el régimen (Ley Vieja 14.157 / Ley Nueva 19.695) según la lógica del legacy MONTEPIO.PRG.
 925    /// Fecha límite de la reforma: 28/02/2019.
 926    /// - Escalafón AV (Oficiales): >= 15 años al 28/02/2019 → Ley Vieja
 927    /// - Otros escalafones (Subalternos): >= 3 años al 28/02/2019 → Ley Vieja
 928    /// - Ingreso posterior a la reforma → siempre Ley Nueva
 929    /// </summary>
 930    private async Task<long> DeterminarRegimenIdAsync(DateTime fechaIngreso, long? escalafonId)
 931    {
 1932        var fechaReforma = new DateTime(2019, 2, 28);
 933        string numeroLey;
 934
 1935        if (fechaIngreso.Date >= fechaReforma)
 936        {
 1937            numeroLey = "19.695";
 938        }
 939        else
 940        {
 0941            int aniosAlLimite = fechaReforma.Year - fechaIngreso.Year;
 0942            if (fechaIngreso.Date > fechaReforma.AddYears(-aniosAlLimite)) aniosAlLimite--;
 943
 0944            var escalafon = escalafonId.HasValue
 0945                ? await _context.Escalafones.FindAsync(escalafonId.Value)
 0946                : null;
 0947            bool esOficial = escalafon?.Codigo == "AV";
 0948            int umbral = esOficial ? 15 : 3;
 949
 0950            numeroLey = aniosAlLimite >= umbral ? "14.157" : "19.695";
 951        }
 952
 1953        var regimen = await _context.Regimenes
 1954            .FirstOrDefaultAsync(r => r.NumeroLey == numeroLey && r.Vigente);
 955
 956        // Fallback a Ley Nueva si no se encuentra (no debería ocurrir)
 1957        if (regimen == null)
 958        {
 0959            regimen = await _context.Regimenes
 0960                .FirstOrDefaultAsync(r => r.NumeroLey == "19.695" && r.Vigente);
 961        }
 962
 1963        return regimen!.Id;
 1964    }
 965
 966    #endregion
 967}