< 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: 466
Uncovered lines: 135
Coverable lines: 601
Total lines: 966
Line coverage: 77.5%
Branch coverage
42%
Covered branches: 155
Total branches: 368
Branch coverage: 42.1%
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.22%593673.95%
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.");
 1277            if (grado.EscalafonId != escalafonId)
 0278                return (false, null, "El grado seleccionado no pertenece al escalafón indicado.");
 279
 280            // Validar fecha de nacimiento
 1281            if (request.FechaNacimiento != default)
 282            {
 1283                var fechaNac = request.FechaNacimiento.Date;
 1284                if (fechaNac > DateTime.UtcNow.Date)
 0285                    return (false, null, "La fecha de nacimiento no puede ser futura.");
 1286                if (fechaNac < new DateTime(1940, 1, 1))
 0287                    return (false, null, "La fecha de nacimiento no puede ser anterior a 01/01/1940.");
 288            }
 289
 290            // Buscar o crear persona
 1291            Persona? persona = await _context.Personas.FirstOrDefaultAsync(p => p.Cedula == cedulaLimpia);
 292
 1293            if (persona == null)
 294            {
 295                // Crear persona - Convertir fechas a UTC
 1296                var fechaNacimientoUtc = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 1297                    ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 1298                    : request.FechaNacimiento.Date.ToUniversalTime();
 299
 1300                persona = new Persona
 1301                {
 1302                    Cedula = cedulaLimpia,
 1303                    PrimerNombre = request.PrimerNombre,
 1304                    SegundoNombre = request.SegundoNombre,
 1305                    PrimerApellido = request.PrimerApellido,
 1306                    SegundoApellido = request.SegundoApellido,
 1307                    FechaNacimiento = fechaNacimientoUtc,
 1308                    Email = request.Email,
 1309                    Telefono = request.Telefono,
 1310                    Direccion = request.Direccion,
 1311                    CodigoPostal = request.CodigoPostal,
 1312                    Departamento = request.Departamento,
 1313                    Localidad = request.Localidad
 1314                };
 315
 1316                _context.Personas.Add(persona);
 1317                await _context.SaveChangesAsync();
 318            }
 319            else
 320            {
 321                // Persona ya existe: actualizar campos de dirección si vienen en el request
 0322                bool personaModificada = false;
 0323                if (request.CodigoPostal != null) { persona.CodigoPostal = request.CodigoPostal; personaModificada = tru
 0324                if (request.Departamento != null) { persona.Departamento = request.Departamento; personaModificada = tru
 0325                if (request.Localidad != null) { persona.Localidad = request.Localidad; personaModificada = true; }
 0326                if (personaModificada)
 327                {
 0328                    persona.FechaActualizacion = DateTime.UtcNow;
 0329                    _context.Personas.Update(persona);
 0330                    await _context.SaveChangesAsync();
 331                }
 332            }
 333
 334            // Determinar régimen: explícito si se pasa, sino auto-calcular
 335            long regimenId;
 1336            if (!string.IsNullOrWhiteSpace(request.Ley))
 337            {
 0338                var regimenExplicito = await _context.Regimenes
 0339                    .FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 0340                if (regimenExplicito == null)
 0341                    return (false, null, $"No se encontró el régimen con ley {request.Ley}");
 0342                regimenId = regimenExplicito.Id;
 343            }
 344            else
 345            {
 1346                regimenId = await DeterminarRegimenIdAsync(request.FechaIngreso, request.EscalafonId);
 347            }
 348
 349            // Crear relación laboral - Convertir fechas a UTC
 1350            var fechaIngresoUtc = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 1351                ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 1352                : request.FechaIngreso.Date.ToUniversalTime();
 353
 1354            var relacionLaboral = new RelacionLaboral
 1355            {
 1356                PersonaId = persona.Id,
 1357                RegimenId = regimenId,
 1358                EscalafonId = request.EscalafonId ?? 1,
 1359                GradoId = request.GradoId,
 1360                SituacionId = request.SituacionId ?? 1,
 1361                ProgramaId = request.ProgramaId ?? 1,
 1362                UnidadId = request.UnidadId ?? 1,
 1363                SubUnidadId = request.SubUnidadId,
 1364                Estado = EstadoRelacion.Activo,
 1365                FechaInicio = fechaIngresoUtc,
 1366                Observaciones = request.Observaciones
 1367            };
 368
 1369            _context.RelacionesLaborales.Add(relacionLaboral);
 1370            await _context.SaveChangesAsync();
 371
 1372            await _auditoriaService.LogAuditoriaAsync(
 1373                usuarioId: _currentUser.UserId ?? 0,
 1374                AccionEnum.CrearPersonal,
 1375                ContextoEnum.Personal,
 1376                host: _currentUser.Host,
 1377                entidad: nameof(Persona),
 1378                entidadId: persona.Id,
 1379                detalle: new { relacionLaboralId = relacionLaboral.Id, personaId = persona.Id, cedula = persona.Cedula }
 380
 1381            return (true, relacionLaboral.Id, null);
 382        }
 0383        catch (Exception ex)
 384        {
 0385            _logger.LogError(ex, "Error al crear personal");
 0386            var errorMessage = ex.Message;
 0387            var innerEx = ex.InnerException;
 0388            while (innerEx != null)
 389            {
 0390                errorMessage += $" | {innerEx.Message}";
 0391                innerEx = innerEx.InnerException;
 392            }
 0393            return (false, null, $"Error al crear personal: {errorMessage}");
 394        }
 4395    }
 396
 397    public async Task<(bool success, string? error)> UpdatePersonalAsync(long id, UpdatePersonalRequest request)
 398    {
 399        try
 400        {
 3401            var persona = await _context.Personas.FindAsync(id);
 3402            if (persona == null)
 1403                return (false, "Persona no encontrada");
 404
 405            // Validar campos de compensación si se proporcionan
 2406            var errorCompensacion = CompensacionValidator.ValidarCompensaciones(
 2407                request.RiesgoVuelo,
 2408                request.PrimaTecnica,
 2409                request.PrimaSolidariaFamiliar,
 2410                request.AniosInactivos);
 411
 2412            if (errorCompensacion != null)
 413            {
 0414                return (false, errorCompensacion);
 415            }
 416
 417            // Actualizar datos personales
 2418            if (!string.IsNullOrWhiteSpace(request.PrimerNombre))
 1419                persona.PrimerNombre = request.PrimerNombre.Trim();
 420
 2421            if (request.SegundoNombre != null)
 0422                persona.SegundoNombre = string.IsNullOrWhiteSpace(request.SegundoNombre) ? null : request.SegundoNombre.
 423
 2424            if (!string.IsNullOrWhiteSpace(request.PrimerApellido))
 1425                persona.PrimerApellido = request.PrimerApellido.Trim();
 426
 2427            if (request.SegundoApellido != null)
 0428                persona.SegundoApellido = string.IsNullOrWhiteSpace(request.SegundoApellido) ? null : request.SegundoApe
 429
 2430            if (request.FechaNacimiento.HasValue)
 431            {
 0432                var fechaNac = request.FechaNacimiento.Value;
 0433                persona.FechaNacimiento = fechaNac.Kind == DateTimeKind.Unspecified
 0434                    ? DateTime.SpecifyKind(fechaNac.Date, DateTimeKind.Utc)
 0435                    : fechaNac.Date.ToUniversalTime();
 436            }
 437
 2438            if (!string.IsNullOrWhiteSpace(request.Email))
 1439                persona.Email = request.Email.Trim();
 440
 2441            if (request.Telefono != null)
 0442                persona.Telefono = string.IsNullOrWhiteSpace(request.Telefono) ? null : request.Telefono.Trim();
 443
 2444            if (request.Direccion != null)
 0445                persona.Direccion = string.IsNullOrWhiteSpace(request.Direccion) ? null : request.Direccion.Trim();
 446
 2447            if (request.CodigoPostal != null)
 0448                persona.CodigoPostal = string.IsNullOrWhiteSpace(request.CodigoPostal) ? null : request.CodigoPostal.Tri
 449
 2450            if (request.Departamento != null)
 0451                persona.Departamento = string.IsNullOrWhiteSpace(request.Departamento) ? null : request.Departamento.Tri
 452
 2453            if (request.Localidad != null)
 0454                persona.Localidad = string.IsNullOrWhiteSpace(request.Localidad) ? null : request.Localidad.Trim();
 455
 2456            if (persona.FechaNacimiento.HasValue)
 457            {
 1458                var fn = persona.FechaNacimiento.Value;
 1459                persona.FechaNacimiento = fn.Kind == DateTimeKind.Unspecified
 1460                    ? DateTime.SpecifyKind(fn.Date, DateTimeKind.Utc)
 1461                    : fn.ToUniversalTime();
 462            }
 463
 2464            persona.FechaActualizacion = DateTime.UtcNow;
 2465            _context.Personas.Update(persona);
 466
 467            // Buscar relación activa para la persona
 2468            var relacionActiva = await _context.RelacionesLaborales
 2469                .FirstOrDefaultAsync(r => r.PersonaId == persona.Id && r.Estado == EstadoRelacion.Activo);
 470
 471            // Obtener la última relación (puede ser null)
 2472            var ultimaRelacion = await _context.RelacionesLaborales
 2473                .Where(r => r.PersonaId == persona.Id)
 2474                .OrderByDescending(r => r.FechaInicio)
 2475                .FirstOrDefaultAsync();
 476
 477            // Validar transición de estado usando la relación más relevante (activa si existe, sino la última)
 2478            var relacionParaValidacion = relacionActiva ?? ultimaRelacion;
 479
 2480            if (!string.IsNullOrWhiteSpace(request.Estado) && relacionParaValidacion != null)
 481            {
 0482                var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 0483                    relacionParaValidacion.Estado.ToString(),
 0484                    request.Estado);
 0485                if (errorTransicion != null)
 0486                    return (false, errorTransicion);
 487            }
 488
 489            // Determinar si se solicitaron cambios laborales
 2490            bool cambioLaboralSolicitado = request.GradoId.HasValue || request.RegimenId.HasValue || request.EscalafonId
 2491                request.SituacionId.HasValue || request.ProgramaId.HasValue || request.UnidadId.HasValue || request.SubU
 492
 2493            if (relacionActiva != null)
 494            {
 495                // Si hay relación activa, verificar si los cambios requieren crear una nueva relación
 2496                bool requiereNueva = false;
 3497                if (request.GradoId.HasValue && request.GradoId.Value != relacionActiva.GradoId) requiereNueva = true;
 2498                if (request.RegimenId.HasValue && request.RegimenId.Value != relacionActiva.RegimenId) requiereNueva = t
 2499                if (request.EscalafonId.HasValue && request.EscalafonId.Value != relacionActiva.EscalafonId) requiereNue
 2500                if (request.SituacionId.HasValue && request.SituacionId.Value != relacionActiva.SituacionId) requiereNue
 2501                if (request.ProgramaId.HasValue && request.ProgramaId.Value != relacionActiva.ProgramaId) requiereNueva 
 2502                if (request.UnidadId.HasValue && request.UnidadId.Value != relacionActiva.UnidadId) requiereNueva = true
 2503                if (request.SubUnidadId.HasValue && request.SubUnidadId.Value != relacionActiva.SubUnidadId) requiereNue
 504
 2505                if (requiereNueva)
 506                {
 507                    // Cerrar relación activa
 1508                    relacionActiva.FechaFin = DateTime.UtcNow;
 1509                    relacionActiva.Estado = EstadoRelacion.Inactivo;
 1510                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 511
 512                    // Crear nueva relación activa con los valores actualizados
 1513                    var nuevaRelacion = new RelacionLaboral
 1514                    {
 1515                        PersonaId = persona.Id,
 1516                        RegimenId = request.RegimenId ?? relacionActiva.RegimenId,
 1517                        EscalafonId = request.EscalafonId ?? relacionActiva.EscalafonId,
 1518                        GradoId = request.GradoId ?? relacionActiva.GradoId,
 1519                        SituacionId = request.SituacionId ?? relacionActiva.SituacionId,
 1520                        ProgramaId = request.ProgramaId ?? relacionActiva.ProgramaId,
 1521                        UnidadId = request.UnidadId ?? relacionActiva.UnidadId,
 1522                        SubUnidadId = request.SubUnidadId ?? relacionActiva.SubUnidadId,
 1523                        Estado = EstadoRelacion.Activo,
 1524                        FechaInicio = DateTime.UtcNow,
 1525                        Observaciones = request.Observaciones ?? relacionActiva.Observaciones,
 1526                        FechaActualizacion = DateTime.UtcNow,
 1527                        RiesgoVuelo = request.RiesgoVuelo ?? relacionActiva.RiesgoVuelo,
 1528                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? relacionActiva.PrimaTecnica : r
 1529                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? relacionAct
 1530                        AniosInactivos = request.AniosInactivos ?? relacionActiva.AniosInactivos
 1531                    };
 532
 1533                    _context.RelacionesLaborales.Add(nuevaRelacion);
 534                }
 535                else
 536                {
 537                    // Actualizar en sitio la relación activa
 1538                    if (!string.IsNullOrWhiteSpace(request.Estado))
 539                    {
 0540                        if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0541                            relacionActiva.Estado = est;
 542                        else
 543                        {
 0544                            var estLower = request.Estado.Trim().ToLower();
 0545                            if (estLower == "activo") relacionActiva.Estado = EstadoRelacion.Activo;
 0546                            else if (estLower == "inactivo") relacionActiva.Estado = EstadoRelacion.Inactivo;
 547                        }
 548                    }
 549
 1550                    if (request.RegimenId.HasValue) relacionActiva.RegimenId = request.RegimenId.Value;
 1551                    if (request.EscalafonId.HasValue) relacionActiva.EscalafonId = request.EscalafonId.Value;
 1552                    if (request.GradoId.HasValue) relacionActiva.GradoId = request.GradoId.Value;
 1553                    if (request.SituacionId.HasValue) relacionActiva.SituacionId = request.SituacionId.Value;
 1554                    if (request.ProgramaId.HasValue) relacionActiva.ProgramaId = request.ProgramaId.Value;
 1555                    if (request.UnidadId.HasValue) relacionActiva.UnidadId = request.UnidadId.Value;
 1556                    if (request.SubUnidadId.HasValue) relacionActiva.SubUnidadId = request.SubUnidadId.Value;
 557
 1558                    if (request.RiesgoVuelo.HasValue) relacionActiva.RiesgoVuelo = request.RiesgoVuelo;
 1559                    if (request.PrimaTecnica != null) relacionActiva.PrimaTecnica = string.IsNullOrWhiteSpace(request.Pr
 1560                    if (request.PrimaSolidariaFamiliar != null) relacionActiva.PrimaSolidariaFamiliar = string.IsNullOrW
 1561                    if (request.AniosInactivos.HasValue) relacionActiva.AniosInactivos = request.AniosInactivos;
 1562                    if (request.Observaciones != null) relacionActiva.Observaciones = request.Observaciones;
 563
 1564                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 565                }
 566            }
 567            else
 568            {
 569                // No hay relación activa
 0570                if (cambioLaboralSolicitado)
 571                {
 572                    // Crear nueva relación activa basándose en la última relación si existe
 0573                    var plantilla = ultimaRelacion;
 574
 0575                    var nuevaRelacion = new RelacionLaboral
 0576                    {
 0577                        PersonaId = persona.Id,
 0578                        RegimenId = request.RegimenId ?? plantilla?.RegimenId ?? 0,
 0579                        EscalafonId = request.EscalafonId ?? plantilla?.EscalafonId ?? 0,
 0580                        GradoId = request.GradoId ?? plantilla?.GradoId ?? 0,
 0581                        SituacionId = request.SituacionId ?? plantilla?.SituacionId ?? 0,
 0582                        ProgramaId = request.ProgramaId ?? plantilla?.ProgramaId ?? 0,
 0583                        UnidadId = request.UnidadId ?? plantilla?.UnidadId ?? 0,
 0584                        SubUnidadId = request.SubUnidadId ?? plantilla?.SubUnidadId,
 0585                        Estado = EstadoRelacion.Activo,
 0586                        FechaInicio = DateTime.UtcNow,
 0587                        Observaciones = request.Observaciones ?? plantilla?.Observaciones,
 0588                        FechaActualizacion = DateTime.UtcNow,
 0589                        RiesgoVuelo = request.RiesgoVuelo ?? plantilla?.RiesgoVuelo,
 0590                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? plantilla?.PrimaTecnica : reque
 0591                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? plantilla?.
 0592                        AniosInactivos = request.AniosInactivos ?? plantilla?.AniosInactivos
 0593                    };
 594
 0595                    _context.RelacionesLaborales.Add(nuevaRelacion);
 596                }
 597                else
 598                {
 599                    // Solo actualizar estado u observaciones de la última relación si existe
 0600                    if (ultimaRelacion != null)
 601                    {
 0602                        if (!string.IsNullOrWhiteSpace(request.Estado))
 603                        {
 0604                            if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0605                                ultimaRelacion.Estado = est;
 606                            else
 607                            {
 0608                                var estLower = request.Estado.Trim().ToLower();
 0609                                if (estLower == "activo") ultimaRelacion.Estado = EstadoRelacion.Activo;
 0610                                else if (estLower == "inactivo") ultimaRelacion.Estado = EstadoRelacion.Inactivo;
 611                            }
 612                        }
 613
 0614                        if (request.Observaciones != null) ultimaRelacion.Observaciones = request.Observaciones;
 0615                        ultimaRelacion.FechaActualizacion = DateTime.UtcNow;
 616                    }
 617                }
 618            }
 619
 2620            await _context.SaveChangesAsync();
 621
 2622            return (true, null);
 623        }
 0624        catch (Exception ex)
 625        {
 0626            _logger.LogError(ex, "Error al actualizar personal");
 0627            return (false, $"Error al actualizar personal: {ex.Message}");
 628        }
 3629    }
 630
 631    public async Task<(bool success, string? error)> RegistrarBajaAsync(long id, RegistrarBajaRequest request)
 632    {
 633        try
 634        {
 635            // id is PersonaId: buscar la relación laboral activa de la persona
 4636            var relacionLaboral = await _context.RelacionesLaborales
 4637                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 638
 4639            if (relacionLaboral == null)
 640            {
 1641                return (false, "Relación laboral no encontrada");
 642            }
 643
 644            // Validar motivo de movimiento: debe existir y NO ser una alta
 3645            var tipoMovimiento = await _context.TiposMovimiento
 3646                .FirstOrDefaultAsync(t => t.Id == request.MotivoId);
 647
 3648            if (tipoMovimiento == null)
 649            {
 1650                return (false, "Motivo de baja no encontrado");
 651            }
 652
 2653            if (tipoMovimiento.EsAlta)
 654            {
 1655                return (false, "El motivo proporcionado corresponde a un alta, no puede usarse para una baja");
 656            }
 657
 658            // Validar MotivoBaja (catálogo)
 1659            if (request.MotivoBajaId <= 0)
 660            {
 0661                return (false, "El motivo de baja (catálogo) es requerido");
 662            }
 663
 1664            var motivoBajaExiste = await _motivoBajaRepository.ExistsAsync(request.MotivoBajaId);
 1665            if (!motivoBajaExiste)
 666            {
 0667                return (false, "Motivo de baja (catálogo) no encontrado o no vigente");
 668            }
 669
 670            // Validar que la fecha de baja no sea futura
 1671            var errorFechaBaja = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaBaja);
 1672            if (errorFechaBaja != null)
 673            {
 0674                return (false, errorFechaBaja);
 675            }
 676
 677            // Validar fecha de baja vs último movimiento y fecha de inicio
 1678            var validadorMovimiento = new MovimientoLaboralValidator(_context);
 1679            var errorFechaMovimiento = await validadorMovimiento.ValidarFechaMovimiento(relacionLaboral.Id, request.Fech
 1680            if (errorFechaMovimiento != null)
 681            {
 0682                return (false, errorFechaMovimiento);
 683            }
 684
 1685            var errorFechaBajaVsMovimiento = await validadorMovimiento.ValidarFechaBajaVsUltimoMovimiento(relacionLabora
 1686            if (errorFechaBajaVsMovimiento != null)
 687            {
 0688                return (false, errorFechaBajaVsMovimiento);
 689            }
 690
 691            // Validar transición de estado: debe ir de activo a inactivo
 1692            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1693                relacionLaboral.Estado.ToString(),
 1694                "Inactivo");
 695
 1696            if (errorTransicion != null)
 697            {
 0698                return (false, errorTransicion);
 699            }
 700
 701            // Cambiar estado a inactivo
 1702            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1703            var fechaBajaUtc = request.FechaBaja.Kind == DateTimeKind.Unspecified
 1704                ? DateTime.SpecifyKind(request.FechaBaja.Date, DateTimeKind.Utc)
 1705                : request.FechaBaja.Date.ToUniversalTime();
 1706            relacionLaboral.FechaFin = fechaBajaUtc;
 1707            relacionLaboral.MotivoBajaId = request.MotivoBajaId;
 1708            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 709
 710            // Registrar movimiento de baja
 1711            var movimiento = new MovimientoLaboral
 1712            {
 1713                RelacionLaboralId = relacionLaboral.Id,
 1714                TipoMovimientoId = request.MotivoId,
 1715                FechaMovimiento = fechaBajaUtc,
 1716                UsuarioId = _currentUser.UserId ?? 0,
 1717                Observaciones = request.Observaciones
 1718            };
 719
 1720            _context.MovimientosLaborales.Add(movimiento);
 1721            await _context.SaveChangesAsync();
 722
 1723            await _auditoriaService.LogAuditoriaAsync(
 1724                usuarioId: _currentUser.UserId ?? 0,
 1725                AccionEnum.BajaPersonal,
 1726                ContextoEnum.Personal,
 1727                host: _currentUser.Host,
 1728                entidad: nameof(RelacionLaboral),
 1729                entidadId: relacionLaboral.Id,
 1730                detalle: new { personaId = id, relacionLaboralId = relacionLaboral.Id, request });
 731
 1732            return (true, null);
 733        }
 0734        catch (Exception ex)
 735        {
 0736            _logger.LogError(ex, "Error al registrar baja");
 0737            return (false, $"Error al registrar baja: {ex.Message}");
 738        }
 4739    }
 740
 741    public async Task<(bool success, string? error)> RegistrarAscensoAsync(long id, AscensoRequest request)
 742    {
 743        try
 744        {
 5745            var relacionLaboral = await _context.RelacionesLaborales
 5746                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 747
 5748            if (relacionLaboral == null)
 1749                return (false, "Relación laboral no encontrada");
 750
 751            // Validar grado objetivo
 4752            var grado = await _context.Grados.FirstOrDefaultAsync(g => g.Id == request.NuevoGradoId && g.Vigente);
 4753            if (grado == null)
 1754                return (false, "Grado no encontrado");
 755
 756            // Validar ley objetivo: ahora se recibe el número de ley como string en request.Ley
 3757            Entidades.Regimen? regimenAscenso = null;
 758
 3759            regimenAscenso = await _context.Regimenes.FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 3760            if (regimenAscenso == null)
 761            {
 1762                return (false, $"No se encontró la ley {request.Ley}");
 763            }
 764
 765            // Validar que el grado objetivo
 2766            if (relacionLaboral.GradoId != 0)
 767            {
 2768                var gradoActual = await _context.Grados.FirstOrDefaultAsync(g => g.Id == relacionLaboral.GradoId);
 2769                if (gradoActual != null)
 770                {
 2771                    if (grado.Orden <= gradoActual.Orden)
 772                    {
 1773                        return (false, "El grado objetivo debe ser superior al grado actual");
 774                    }
 775                }
 776            }
 777
 1778            var fechaAscensoUtc = request.FechaAscenso.Kind == DateTimeKind.Unspecified
 1779                ? DateTime.SpecifyKind(request.FechaAscenso.Date, DateTimeKind.Utc)
 1780                : request.FechaAscenso.Date.ToUniversalTime();
 781
 782            // Validar unidad objetivo si se proporciona
 783            Unidad? unidadObj = null;
 1784            if (request.UnidadId.HasValue)
 785            {
 0786                unidadObj = await _context.Unidades.FirstOrDefaultAsync(u => u.Id == request.UnidadId.Value && u.Vigente
 0787                if (unidadObj == null)
 788                {
 0789                    return (false, "Unidad no válida o no vigente");
 790                }
 791            }
 792
 793            // Cerrar la relación laboral actual
 794            // Validar transición: debe estar activo para poder ascender
 1795            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1796                relacionLaboral.Estado.ToString(),
 1797                "Inactivo");
 798
 1799            if (errorTransicion != null)
 800            {
 0801                return (false, errorTransicion);
 802            }
 803
 1804            relacionLaboral.FechaFin = fechaAscensoUtc;
 1805            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 1806            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1807            relacionLaboral.Observaciones = request.Observacion;
 808
 809            // Crear nueva relación laboral con el nuevo grado
 1810            var nuevaRelacion = new RelacionLaboral
 1811            {
 1812                PersonaId = relacionLaboral.PersonaId,
 1813                RegimenId = regimenAscenso.Id,
 1814                EscalafonId = relacionLaboral.EscalafonId,
 1815                GradoId = request.NuevoGradoId,
 1816                SituacionId = relacionLaboral.SituacionId,
 1817                ProgramaId = relacionLaboral.ProgramaId,
 1818                UnidadId = request.UnidadId ?? relacionLaboral.UnidadId,
 1819                SubUnidadId = relacionLaboral.SubUnidadId,
 1820                Estado = EstadoRelacion.Activo,
 1821                FechaInicio = fechaAscensoUtc,
 1822                Observaciones = relacionLaboral.Observaciones,
 1823                FechaActualizacion = DateTime.UtcNow
 1824            };
 825
 1826            _context.RelacionesLaborales.Add(nuevaRelacion);
 827
 828            // Registrar movimientos
 829            // Prefer specific tipo_movimiento ids: 6 = Cambio de Situacion (cierre), 7 = Ascenso (alta)
 1830            var tipoCierre = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("cambio
 1831            var tipoAlta = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("ascenso"
 832
 1833            if (tipoCierre == null)
 834            {
 1835                tipoCierre = new Entidades.TipoMovimiento { Nombre = "Cambio de Situacion", EsAlta = false };
 1836                _context.TiposMovimiento.Add(tipoCierre);
 1837                await _context.SaveChangesAsync();
 838            }
 839
 1840            if (tipoAlta == null)
 841            {
 1842                tipoAlta = new Entidades.TipoMovimiento { Nombre = "Ascenso", EsAlta = true };
 1843                _context.TiposMovimiento.Add(tipoAlta);
 1844                await _context.SaveChangesAsync();
 845            }
 846
 847            // Movimiento de cierre
 1848            var movimientoCierre = new Entidades.MovimientoLaboral
 1849            {
 1850                RelacionLaboralId = relacionLaboral.Id,
 1851                TipoMovimientoId = tipoCierre.Id,
 1852                FechaMovimiento = fechaAscensoUtc,
 1853                UsuarioId = _currentUser.UserId ?? 0,
 1854                Observaciones = request.Observacion
 1855            };
 856
 1857            _context.MovimientosLaborales.Add(movimientoCierre);
 858
 1859            await _context.SaveChangesAsync();
 860
 1861            var movimientoAlta = new Entidades.MovimientoLaboral
 1862            {
 1863                RelacionLaboralId = nuevaRelacion.Id,
 1864                TipoMovimientoId = tipoAlta.Id,
 1865                FechaMovimiento = fechaAscensoUtc,
 1866                UsuarioId = _currentUser.UserId ?? 0,
 1867                Observaciones = request.Observacion
 1868            };
 869
 1870            _context.MovimientosLaborales.Add(movimientoAlta);
 871
 1872            await _context.SaveChangesAsync();
 873
 1874            await _auditoriaService.LogAuditoriaAsync(
 1875                usuarioId: _currentUser.UserId ?? 0,
 1876                AccionEnum.AscensoPersonal,
 1877                ContextoEnum.Personal,
 1878                host: _currentUser.Host,
 1879                entidad: nameof(RelacionLaboral),
 1880                entidadId: nuevaRelacion.Id,
 1881                detalle: new { personaId = id, relacionAnteriorId = relacionLaboral.Id, relacionNuevaId = nuevaRelacion.
 882
 1883            return (true, null);
 884        }
 0885        catch (Exception ex)
 886        {
 0887            _logger.LogError(ex, "Error al registrar ascenso");
 0888            return (false, $"Error al registrar ascenso: {ex.Message}");
 889        }
 5890    }
 891
 892    private IQueryable<RelacionLaboral> ApplyOrdering(
 893        IQueryable<RelacionLaboral> query,
 894        string? orderBy,
 895        string sort)
 896    {
 1897        var ascending = sort.ToLower() == "asc";
 898
 1899        return orderBy?.ToLower() switch
 1900        {
 0901            "ci" => ascending
 0902                ? query.OrderBy(r => r.Persona.Cedula)
 0903                : query.OrderByDescending(r => r.Persona.Cedula),
 0904            "nombre" => ascending
 0905                ? query.OrderBy(r => r.Persona.PrimerNombre)
 0906                : query.OrderByDescending(r => r.Persona.PrimerNombre),
 0907            "apellido" => ascending
 0908                ? query.OrderBy(r => r.Persona.PrimerApellido)
 0909                : query.OrderByDescending(r => r.Persona.PrimerApellido),
 0910            "grado" => ascending
 0911                ? query.OrderBy(r => r.Grado.Orden)
 0912                : query.OrderByDescending(r => r.Grado.Orden),
 0913            "fechaingreso" => ascending
 0914                ? query.OrderBy(r => r.FechaInicio)
 0915                : query.OrderByDescending(r => r.FechaInicio),
 1916            _ => query.OrderByDescending(r => r.FechaInicio)
 1917        };
 918    }
 919
 920    #region Métodos Privados
 921
 922    /// <summary>
 923    /// Determina el régimen (Ley Vieja 14.157 / Ley Nueva 19.695) según la lógica del legacy MONTEPIO.PRG.
 924    /// Fecha límite de la reforma: 28/02/2019.
 925    /// - Escalafón AV (Oficiales): >= 15 años al 28/02/2019 → Ley Vieja
 926    /// - Otros escalafones (Subalternos): >= 3 años al 28/02/2019 → Ley Vieja
 927    /// - Ingreso posterior a la reforma → siempre Ley Nueva
 928    /// </summary>
 929    private async Task<long> DeterminarRegimenIdAsync(DateTime fechaIngreso, long? escalafonId)
 930    {
 1931        var fechaReforma = new DateTime(2019, 2, 28);
 932        string numeroLey;
 933
 1934        if (fechaIngreso.Date >= fechaReforma)
 935        {
 1936            numeroLey = "19.695";
 937        }
 938        else
 939        {
 0940            int aniosAlLimite = fechaReforma.Year - fechaIngreso.Year;
 0941            if (fechaIngreso.Date > fechaReforma.AddYears(-aniosAlLimite)) aniosAlLimite--;
 942
 0943            var escalafon = escalafonId.HasValue
 0944                ? await _context.Escalafones.FindAsync(escalafonId.Value)
 0945                : null;
 0946            bool esOficial = escalafon?.Codigo == "AV";
 0947            int umbral = esOficial ? 15 : 3;
 948
 0949            numeroLey = aniosAlLimite >= umbral ? "14.157" : "19.695";
 950        }
 951
 1952        var regimen = await _context.Regimenes
 1953            .FirstOrDefaultAsync(r => r.NumeroLey == numeroLey && r.Vigente);
 954
 955        // Fallback a Ley Nueva si no se encuentra (no debería ocurrir)
 1956        if (regimen == null)
 957        {
 0958            regimen = await _context.Regimenes
 0959                .FirstOrDefaultAsync(r => r.NumeroLey == "19.695" && r.Vigente);
 960        }
 961
 1962        return regimen!.Id;
 1963    }
 964
 965    #endregion
 966}