< 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
78%
Covered lines: 447
Uncovered lines: 122
Coverable lines: 569
Total lines: 917
Line coverage: 78.5%
Branch coverage
43%
Covered branches: 149
Total branches: 342
Branch coverage: 43.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetPersonalListAsync()50%201468.29%
GetDetalleAsync()83.33%66100%
CreatePersonalAsync()60.71%342880%
UpdatePersonalAsync()37.03%274821662.14%
RegistrarBajaAsync()68.75%171687.03%
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 ApplicationDbContext _context;
 18    private readonly ILogger<PersonalService> _logger;
 19    private readonly IAuditoriaService _auditoriaService;
 20    private readonly ICurrentUserContext _currentUser;
 21
 2022    public PersonalService(
 2023        IPersonalRepository personalRepository,
 2024        ApplicationDbContext context,
 2025        ILogger<PersonalService> logger,
 2026        IAuditoriaService auditoriaService,
 2027        ICurrentUserContext currentUser)
 28    {
 2029        _personalRepository = personalRepository;
 2030        _context = context;
 2031        _logger = logger;
 2032        _auditoriaService = auditoriaService;
 2033        _currentUser = currentUser;
 2034    }
 35
 36    public async Task<object> GetPersonalListAsync(
 37        string? buscar,
 38        long? cargoId,
 39        string? estado,
 40        string? ley,
 41        int page,
 42        int pageSize,
 43        string? orderBy,
 44        string sort)
 45    {
 146        var query = _context.RelacionesLaborales
 147            .Include(r => r.Persona)
 148            .Include(r => r.Regimen)
 149            .Include(r => r.Escalafon)
 150            .Include(r => r.Grado)
 151            .Include(r => r.Situacion)
 152            .AsQueryable();
 53
 54        // Filtro por búsqueda (CI, nombre o apellido)
 155        if (!string.IsNullOrWhiteSpace(buscar))
 56        {
 057            var buscarLower = buscar.ToLower();
 058            query = query.Where(r =>
 059                r.Persona.Cedula.Contains(buscarLower) ||
 060                r.Persona.PrimerNombre.ToLower().Contains(buscarLower) ||
 061                r.Persona.PrimerApellido.ToLower().Contains(buscarLower) ||
 062                (r.Persona.SegundoNombre != null && r.Persona.SegundoNombre.ToLower().Contains(buscarLower)) ||
 063                (r.Persona.SegundoApellido != null && r.Persona.SegundoApellido.ToLower().Contains(buscarLower)));
 64        }
 65
 66        // Filtro por cargo (grado)
 167        if (cargoId.HasValue)
 68        {
 069            query = query.Where(r => r.GradoId == cargoId.Value);
 70        }
 71
 72        // Filtro por estado (parse string to enum)
 173        if (!string.IsNullOrWhiteSpace(estado))
 74        {
 175            if (Enum.TryParse<EstadoRelacion>(estado, true, out var estadoEnum))
 76            {
 177                query = query.Where(r => r.Estado == estadoEnum);
 78            }
 79            else
 80            {
 81                // Si no se puede parsear, intentar aceptar valores comunes
 082                var estLower = estado.Trim().ToLower();
 083                if (estLower == "activo") query = query.Where(r => r.Estado == EstadoRelacion.Activo);
 084                else if (estLower == "inactivo") query = query.Where(r => r.Estado == EstadoRelacion.Inactivo);
 85            }
 86        }
 87
 88        // Filtro por ley (régimen)
 189        if (!string.IsNullOrWhiteSpace(ley))
 90        {
 091            var leyLower = ley.ToLower();
 092            query = query.Where(r => r.Regimen.NumeroLey.ToLower().Contains(leyLower));
 93        }
 94
 195        var total = await query.CountAsync();
 96
 197        query = ApplyOrdering(query, orderBy, sort);
 98
 99        // Paginación: obtener entidades y luego proyectar a DTO en memoria para poder convertir enums a string
 1100        var relacionesPage = await query
 1101            .Skip((page - 1) * pageSize)
 1102            .Take(pageSize)
 1103            .ToListAsync();
 104
 2105        var items = relacionesPage.Select(r => new PersonalListDto
 2106        {
 2107            Id = r.Id,
 2108            Ci = r.Persona.Cedula,
 2109            personalId = r.PersonaId,
 2110            PrimerNombre = r.Persona.PrimerNombre,
 2111            SegundoNombre = r.Persona.SegundoNombre,
 2112            PrimerApellido = r.Persona.PrimerApellido,
 2113            SegundoApellido = r.Persona.SegundoApellido,
 2114            Cargo = r.Grado.Denominacion,
 2115            Grado = r.Grado.Denominacion,
 2116            Ley = r.Regimen.NumeroLey,
 2117            Estado = r.Estado.ToString(),
 2118            FechaIngreso = r.FechaInicio
 2119        }).ToList();
 120
 1121        return new
 1122        {
 1123            items,
 1124            total,
 1125            page,
 1126            pageSize,
 1127            totalPages = (int)Math.Ceiling((double)total / pageSize)
 1128        };
 1129    }
 130
 131    public async Task<PersonalDetalleDto?> GetDetalleAsync(long id)
 132    {
 133        // id is PersonaId: obtener la relación laboral activa o la última relación laboral
 3134        var relacionLaboral = await _context.RelacionesLaborales
 3135            .Include(r => r.Persona)
 3136            .Include(r => r.Regimen)
 3137            .Include(r => r.Escalafon)
 3138            .Include(r => r.Grado)
 3139            .Include(r => r.Situacion)
 3140            .Include(r => r.Programa)
 3141            .Include(r => r.Unidad)
 3142            .Include(r => r.Compania)
 3143            .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 144
 3145        if (relacionLaboral == null)
 146        {
 2147            relacionLaboral = await _context.RelacionesLaborales
 2148                .Include(r => r.Persona)
 2149                .Include(r => r.Regimen)
 2150                .Include(r => r.Escalafon)
 2151                .Include(r => r.Grado)
 2152                .Include(r => r.Situacion)
 2153                .Include(r => r.Programa)
 2154                .Include(r => r.Unidad)
 2155                .Include(r => r.Compania)
 2156                .Where(r => r.PersonaId == id)
 2157                .OrderByDescending(r => r.FechaInicio)
 2158                .FirstOrDefaultAsync();
 159        }
 160
 3161        if (relacionLaboral == null)
 1162            return null;
 163
 164        // Obtener todos los ids de relación laboral de la persona y luego el historial de movimientos para cualquiera d
 2165        var relacionIds = await _context.RelacionesLaborales
 2166            .Where(r => r.PersonaId == relacionLaboral.PersonaId)
 2167            .Select(r => r.Id)
 2168            .ToListAsync();
 169
 2170        var historial = await _context.MovimientosLaborales
 2171            .Include(m => m.TipoMovimiento)
 2172            .Include(m => m.Usuario)
 2173                .ThenInclude(u => u.Persona)
 2174            .Where(m => relacionIds.Contains(m.RelacionLaboralId))
 2175            .OrderByDescending(m => m.Id)
 2176            .Select(m => new HistorialMovimientoDto
 2177            {
 2178                Id = m.Id,
 2179                TipoMovimiento = m.TipoMovimiento.Nombre,
 2180                EsAlta = m.TipoMovimiento.EsAlta,
 2181                FechaMovimiento = m.FechaMovimiento,
 2182                Observaciones = m.Observaciones,
 2183                RegistradoPor = m.Usuario != null ? m.Usuario.Username : null,
 2184                FechaRegistro = m.FechaMovimiento
 2185            })
 2186            .ToListAsync();
 187
 2188        return new PersonalDetalleDto
 2189        {
 2190            Id = relacionLaboral.Id,
 2191            DatosPersonales = new DatosPersonalesDto
 2192            {
 2193                PersonaId = relacionLaboral.PersonaId,
 2194                Ci = relacionLaboral.Persona.Cedula,
 2195                PrimerNombre = relacionLaboral.Persona.PrimerNombre,
 2196                SegundoNombre = relacionLaboral.Persona.SegundoNombre,
 2197                PrimerApellido = relacionLaboral.Persona.PrimerApellido,
 2198                SegundoApellido = relacionLaboral.Persona.SegundoApellido,
 2199                FechaNacimiento = relacionLaboral.Persona.FechaNacimiento,
 2200                Email = relacionLaboral.Persona.Email,
 2201                Telefono = relacionLaboral.Persona.Telefono,
 2202                Direccion = relacionLaboral.Persona.Direccion
 2203            },
 2204            DatosLaborales = new DatosLaboralesDto
 2205            {
 2206                RelacionLaboralId = relacionLaboral.Id,
 2207                Estado = relacionLaboral.Estado.ToString(),
 2208                FechaInicio = relacionLaboral.FechaInicio,
 2209                FechaFin = relacionLaboral.FechaFin,
 2210                RegimenId = relacionLaboral.RegimenId,
 2211                Regimen = relacionLaboral.Regimen.Denominacion,
 2212                Ley = relacionLaboral.Regimen.NumeroLey,
 2213                EscalafonId = relacionLaboral.EscalafonId,
 2214                Escalafon = relacionLaboral.Escalafon.Denominacion,
 2215                GradoId = relacionLaboral.GradoId,
 2216                Grado = relacionLaboral.Grado.Denominacion,
 2217                Cargo = relacionLaboral.Grado.Denominacion,
 2218                SituacionId = relacionLaboral.SituacionId,
 2219                Situacion = relacionLaboral.Situacion.Denominacion,
 2220                ProgramaId = relacionLaboral.ProgramaId,
 2221                Programa = relacionLaboral.Programa.Denominacion,
 2222                UnidadId = relacionLaboral.UnidadId,
 2223                Unidad = relacionLaboral.Unidad.Denominacion,
 2224                CompaniaId = relacionLaboral.CompaniaId,
 2225                Compania = relacionLaboral.Compania?.Denominacion,
 2226                RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 2227                PrimaTecnica = relacionLaboral.PrimaTecnica,
 2228                PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 2229                AniosInactivos = relacionLaboral.AniosInactivos,
 2230                Observaciones = relacionLaboral.Observaciones
 2231            },
 2232            Historial = historial
 2233        };
 3234    }
 235
 236    public async Task<(bool success, long? personalId, string? error)> CreatePersonalAsync(CreatePersonalRequest request
 237    {
 238        try
 239        {
 240            // Validar y limpiar CI
 4241            var cedulaLimpia = CedulaValidator.Limpiar(request.Ci);
 4242            if (!CedulaValidator.Validar(cedulaLimpia))
 243            {
 1244                return (false, null, "La cédula ingresada no es válida.");
 245            }
 246
 247            // Validar que la fecha de ingreso no sea futura
 3248            var errorFechaIngreso = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaIngreso);
 3249            if (errorFechaIngreso != null)
 250            {
 1251                return (false, null, errorFechaIngreso);
 252            }
 253
 254            // Validar que no exista una relación laboral activa para esa cédula
 2255            var validadorRelacion = new RelacionLaboralValidator(_context);
 2256            var errorRelacionActiva = await validadorRelacion.ObtenerErrorSiExisteRelacionActiva(cedulaLimpia);
 2257            if (errorRelacionActiva != null)
 258            {
 1259                return (false, null, errorRelacionActiva);
 260            }
 261
 262            // Validar que el grado pertenece al escalafón indicado
 1263            var escalafonId = request.EscalafonId ?? 1;
 1264            var grado = await _context.Grados.FindAsync(request.GradoId);
 1265            if (grado == null)
 0266                return (false, null, "El grado seleccionado no existe.");
 1267            if (grado.EscalafonId != escalafonId)
 0268                return (false, null, "El grado seleccionado no pertenece al escalafón indicado.");
 269
 270            // Validar fecha de nacimiento
 1271            if (request.FechaNacimiento != default)
 272            {
 1273                var fechaNac = request.FechaNacimiento.Date;
 1274                if (fechaNac > DateTime.UtcNow.Date)
 0275                    return (false, null, "La fecha de nacimiento no puede ser futura.");
 1276                if (fechaNac < new DateTime(1940, 1, 1))
 0277                    return (false, null, "La fecha de nacimiento no puede ser anterior a 01/01/1940.");
 278            }
 279
 280            // Buscar o crear persona
 1281            Persona? persona = await _context.Personas.FirstOrDefaultAsync(p => p.Cedula == cedulaLimpia);
 282
 1283            if (persona == null)
 284            {
 285                // Crear persona - Convertir fechas a UTC
 1286                var fechaNacimientoUtc = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 1287                    ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 1288                    : request.FechaNacimiento.Date.ToUniversalTime();
 289
 1290                persona = new Persona
 1291                {
 1292                    Cedula = cedulaLimpia,
 1293                    PrimerNombre = request.PrimerNombre,
 1294                    SegundoNombre = request.SegundoNombre,
 1295                    PrimerApellido = request.PrimerApellido,
 1296                    SegundoApellido = request.SegundoApellido,
 1297                    FechaNacimiento = fechaNacimientoUtc,
 1298                    Email = request.Email,
 1299                    Telefono = request.Telefono,
 1300                    Direccion = request.Direccion
 1301                };
 302
 1303                _context.Personas.Add(persona);
 1304                await _context.SaveChangesAsync();
 305            }
 306
 307            // Determinar régimen: explícito si se pasa, sino auto-calcular
 308            long regimenId;
 1309            if (!string.IsNullOrWhiteSpace(request.Ley))
 310            {
 0311                var regimenExplicito = await _context.Regimenes
 0312                    .FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 0313                if (regimenExplicito == null)
 0314                    return (false, null, $"No se encontró el régimen con ley {request.Ley}");
 0315                regimenId = regimenExplicito.Id;
 316            }
 317            else
 318            {
 1319                regimenId = await DeterminarRegimenIdAsync(request.FechaIngreso, request.EscalafonId);
 320            }
 321
 322            // Crear relación laboral - Convertir fechas a UTC
 1323            var fechaIngresoUtc = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 1324                ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 1325                : request.FechaIngreso.Date.ToUniversalTime();
 326
 1327            var relacionLaboral = new RelacionLaboral
 1328            {
 1329                PersonaId = persona.Id,
 1330                RegimenId = regimenId,
 1331                EscalafonId = request.EscalafonId ?? 1,
 1332                GradoId = request.GradoId,
 1333                SituacionId = request.SituacionId ?? 1,
 1334                ProgramaId = request.ProgramaId ?? 1,
 1335                UnidadId = request.UnidadId ?? 1,
 1336                CompaniaId = request.CompaniaId,
 1337                Estado = EstadoRelacion.Activo,
 1338                FechaInicio = fechaIngresoUtc,
 1339                Observaciones = request.Observaciones
 1340            };
 341
 1342            _context.RelacionesLaborales.Add(relacionLaboral);
 1343            await _context.SaveChangesAsync();
 344
 1345            await _auditoriaService.LogAuditoriaAsync(
 1346                usuarioId: _currentUser.UserId ?? 0,
 1347                AccionEnum.CrearPersonal,
 1348                ContextoEnum.Personal,
 1349                host: _currentUser.Host,
 1350                entidad: nameof(Persona),
 1351                entidadId: persona.Id,
 1352                detalle: new { relacionLaboralId = relacionLaboral.Id, personaId = persona.Id, cedula = persona.Cedula }
 353
 1354            return (true, relacionLaboral.Id, null);
 355        }
 0356        catch (Exception ex)
 357        {
 0358            _logger.LogError(ex, "Error al crear personal");
 0359            var errorMessage = ex.Message;
 0360            var innerEx = ex.InnerException;
 0361            while (innerEx != null)
 362            {
 0363                errorMessage += $" | {innerEx.Message}";
 0364                innerEx = innerEx.InnerException;
 365            }
 0366            return (false, null, $"Error al crear personal: {errorMessage}");
 367        }
 4368    }
 369
 370    public async Task<(bool success, string? error)> UpdatePersonalAsync(long id, UpdatePersonalRequest request)
 371    {
 372        try
 373        {
 3374            var persona = await _context.Personas.FindAsync(id);
 3375            if (persona == null)
 1376                return (false, "Persona no encontrada");
 377
 378            // Validar campos de compensación si se proporcionan
 2379            var errorCompensacion = CompensacionValidator.ValidarCompensaciones(
 2380                request.RiesgoVuelo,
 2381                request.PrimaTecnica,
 2382                request.PrimaSolidariaFamiliar,
 2383                request.AniosInactivos);
 384
 2385            if (errorCompensacion != null)
 386            {
 0387                return (false, errorCompensacion);
 388            }
 389
 390            // Actualizar datos personales
 2391            if (!string.IsNullOrWhiteSpace(request.PrimerNombre))
 1392                persona.PrimerNombre = request.PrimerNombre.Trim();
 393
 2394            if (request.SegundoNombre != null)
 0395                persona.SegundoNombre = string.IsNullOrWhiteSpace(request.SegundoNombre) ? null : request.SegundoNombre.
 396
 2397            if (!string.IsNullOrWhiteSpace(request.PrimerApellido))
 1398                persona.PrimerApellido = request.PrimerApellido.Trim();
 399
 2400            if (request.SegundoApellido != null)
 0401                persona.SegundoApellido = string.IsNullOrWhiteSpace(request.SegundoApellido) ? null : request.SegundoApe
 402
 2403            if (request.FechaNacimiento.HasValue)
 404            {
 0405                var fechaNac = request.FechaNacimiento.Value;
 0406                persona.FechaNacimiento = fechaNac.Kind == DateTimeKind.Unspecified
 0407                    ? DateTime.SpecifyKind(fechaNac.Date, DateTimeKind.Utc)
 0408                    : fechaNac.Date.ToUniversalTime();
 409            }
 410
 2411            if (!string.IsNullOrWhiteSpace(request.Email))
 1412                persona.Email = request.Email.Trim();
 413
 2414            if (request.Telefono != null)
 0415                persona.Telefono = string.IsNullOrWhiteSpace(request.Telefono) ? null : request.Telefono.Trim();
 416
 2417            if (request.Direccion != null)
 0418                persona.Direccion = string.IsNullOrWhiteSpace(request.Direccion) ? null : request.Direccion.Trim();
 419
 2420            if (persona.FechaNacimiento.HasValue)
 421            {
 1422                var fn = persona.FechaNacimiento.Value;
 1423                persona.FechaNacimiento = fn.Kind == DateTimeKind.Unspecified
 1424                    ? DateTime.SpecifyKind(fn.Date, DateTimeKind.Utc)
 1425                    : fn.ToUniversalTime();
 426            }
 427
 2428            persona.FechaActualizacion = DateTime.UtcNow;
 2429            _context.Personas.Update(persona);
 430
 431            // Buscar relación activa para la persona
 2432            var relacionActiva = await _context.RelacionesLaborales
 2433                .FirstOrDefaultAsync(r => r.PersonaId == persona.Id && r.Estado == EstadoRelacion.Activo);
 434
 435            // Obtener la última relación (puede ser null)
 2436            var ultimaRelacion = await _context.RelacionesLaborales
 2437                .Where(r => r.PersonaId == persona.Id)
 2438                .OrderByDescending(r => r.FechaInicio)
 2439                .FirstOrDefaultAsync();
 440
 441            // Validar transición de estado usando la relación más relevante (activa si existe, sino la última)
 2442            var relacionParaValidacion = relacionActiva ?? ultimaRelacion;
 443
 2444            if (!string.IsNullOrWhiteSpace(request.Estado) && relacionParaValidacion != null)
 445            {
 0446                var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 0447                    relacionParaValidacion.Estado.ToString(),
 0448                    request.Estado);
 0449                if (errorTransicion != null)
 0450                    return (false, errorTransicion);
 451            }
 452
 453            // Determinar si se solicitaron cambios laborales
 2454            bool cambioLaboralSolicitado = request.GradoId.HasValue || request.RegimenId.HasValue || request.EscalafonId
 2455                request.SituacionId.HasValue || request.ProgramaId.HasValue || request.UnidadId.HasValue || request.Comp
 456
 2457            if (relacionActiva != null)
 458            {
 459                // Si hay relación activa, verificar si los cambios requieren crear una nueva relación
 2460                bool requiereNueva = false;
 3461                if (request.GradoId.HasValue && request.GradoId.Value != relacionActiva.GradoId) requiereNueva = true;
 2462                if (request.RegimenId.HasValue && request.RegimenId.Value != relacionActiva.RegimenId) requiereNueva = t
 2463                if (request.EscalafonId.HasValue && request.EscalafonId.Value != relacionActiva.EscalafonId) requiereNue
 2464                if (request.SituacionId.HasValue && request.SituacionId.Value != relacionActiva.SituacionId) requiereNue
 2465                if (request.ProgramaId.HasValue && request.ProgramaId.Value != relacionActiva.ProgramaId) requiereNueva 
 2466                if (request.UnidadId.HasValue && request.UnidadId.Value != relacionActiva.UnidadId) requiereNueva = true
 2467                if (request.CompaniaId.HasValue && request.CompaniaId.Value != relacionActiva.CompaniaId) requiereNueva 
 468
 2469                if (requiereNueva)
 470                {
 471                    // Cerrar relación activa
 1472                    relacionActiva.FechaFin = DateTime.UtcNow;
 1473                    relacionActiva.Estado = EstadoRelacion.Inactivo;
 1474                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 475
 476                    // Crear nueva relación activa con los valores actualizados
 1477                    var nuevaRelacion = new RelacionLaboral
 1478                    {
 1479                        PersonaId = persona.Id,
 1480                        RegimenId = request.RegimenId ?? relacionActiva.RegimenId,
 1481                        EscalafonId = request.EscalafonId ?? relacionActiva.EscalafonId,
 1482                        GradoId = request.GradoId ?? relacionActiva.GradoId,
 1483                        SituacionId = request.SituacionId ?? relacionActiva.SituacionId,
 1484                        ProgramaId = request.ProgramaId ?? relacionActiva.ProgramaId,
 1485                        UnidadId = request.UnidadId ?? relacionActiva.UnidadId,
 1486                        CompaniaId = request.CompaniaId ?? relacionActiva.CompaniaId,
 1487                        Estado = EstadoRelacion.Activo,
 1488                        FechaInicio = DateTime.UtcNow,
 1489                        Observaciones = request.Observaciones ?? relacionActiva.Observaciones,
 1490                        FechaActualizacion = DateTime.UtcNow,
 1491                        RiesgoVuelo = request.RiesgoVuelo ?? relacionActiva.RiesgoVuelo,
 1492                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? relacionActiva.PrimaTecnica : r
 1493                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? relacionAct
 1494                        AniosInactivos = request.AniosInactivos ?? relacionActiva.AniosInactivos
 1495                    };
 496
 1497                    _context.RelacionesLaborales.Add(nuevaRelacion);
 498                }
 499                else
 500                {
 501                    // Actualizar en sitio la relación activa
 1502                    if (!string.IsNullOrWhiteSpace(request.Estado))
 503                    {
 0504                        if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0505                            relacionActiva.Estado = est;
 506                        else
 507                        {
 0508                            var estLower = request.Estado.Trim().ToLower();
 0509                            if (estLower == "activo") relacionActiva.Estado = EstadoRelacion.Activo;
 0510                            else if (estLower == "inactivo") relacionActiva.Estado = EstadoRelacion.Inactivo;
 511                        }
 512                    }
 513
 1514                    if (request.RegimenId.HasValue) relacionActiva.RegimenId = request.RegimenId.Value;
 1515                    if (request.EscalafonId.HasValue) relacionActiva.EscalafonId = request.EscalafonId.Value;
 1516                    if (request.GradoId.HasValue) relacionActiva.GradoId = request.GradoId.Value;
 1517                    if (request.SituacionId.HasValue) relacionActiva.SituacionId = request.SituacionId.Value;
 1518                    if (request.ProgramaId.HasValue) relacionActiva.ProgramaId = request.ProgramaId.Value;
 1519                    if (request.UnidadId.HasValue) relacionActiva.UnidadId = request.UnidadId.Value;
 1520                    if (request.CompaniaId.HasValue) relacionActiva.CompaniaId = request.CompaniaId.Value;
 521
 1522                    if (request.RiesgoVuelo.HasValue) relacionActiva.RiesgoVuelo = request.RiesgoVuelo;
 1523                    if (request.PrimaTecnica != null) relacionActiva.PrimaTecnica = string.IsNullOrWhiteSpace(request.Pr
 1524                    if (request.PrimaSolidariaFamiliar != null) relacionActiva.PrimaSolidariaFamiliar = string.IsNullOrW
 1525                    if (request.AniosInactivos.HasValue) relacionActiva.AniosInactivos = request.AniosInactivos;
 1526                    if (request.Observaciones != null) relacionActiva.Observaciones = request.Observaciones;
 527
 1528                    relacionActiva.FechaActualizacion = DateTime.UtcNow;
 529                }
 530            }
 531            else
 532            {
 533                // No hay relación activa
 0534                if (cambioLaboralSolicitado)
 535                {
 536                    // Crear nueva relación activa basándose en la última relación si existe
 0537                    var plantilla = ultimaRelacion;
 538
 0539                    var nuevaRelacion = new RelacionLaboral
 0540                    {
 0541                        PersonaId = persona.Id,
 0542                        RegimenId = request.RegimenId ?? plantilla?.RegimenId ?? 0,
 0543                        EscalafonId = request.EscalafonId ?? plantilla?.EscalafonId ?? 0,
 0544                        GradoId = request.GradoId ?? plantilla?.GradoId ?? 0,
 0545                        SituacionId = request.SituacionId ?? plantilla?.SituacionId ?? 0,
 0546                        ProgramaId = request.ProgramaId ?? plantilla?.ProgramaId ?? 0,
 0547                        UnidadId = request.UnidadId ?? plantilla?.UnidadId ?? 0,
 0548                        CompaniaId = request.CompaniaId ?? plantilla?.CompaniaId,
 0549                        Estado = EstadoRelacion.Activo,
 0550                        FechaInicio = DateTime.UtcNow,
 0551                        Observaciones = request.Observaciones ?? plantilla?.Observaciones,
 0552                        FechaActualizacion = DateTime.UtcNow,
 0553                        RiesgoVuelo = request.RiesgoVuelo ?? plantilla?.RiesgoVuelo,
 0554                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? plantilla?.PrimaTecnica : reque
 0555                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? plantilla?.
 0556                        AniosInactivos = request.AniosInactivos ?? plantilla?.AniosInactivos
 0557                    };
 558
 0559                    _context.RelacionesLaborales.Add(nuevaRelacion);
 560                }
 561                else
 562                {
 563                    // Solo actualizar estado u observaciones de la última relación si existe
 0564                    if (ultimaRelacion != null)
 565                    {
 0566                        if (!string.IsNullOrWhiteSpace(request.Estado))
 567                        {
 0568                            if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0569                                ultimaRelacion.Estado = est;
 570                            else
 571                            {
 0572                                var estLower = request.Estado.Trim().ToLower();
 0573                                if (estLower == "activo") ultimaRelacion.Estado = EstadoRelacion.Activo;
 0574                                else if (estLower == "inactivo") ultimaRelacion.Estado = EstadoRelacion.Inactivo;
 575                            }
 576                        }
 577
 0578                        if (request.Observaciones != null) ultimaRelacion.Observaciones = request.Observaciones;
 0579                        ultimaRelacion.FechaActualizacion = DateTime.UtcNow;
 580                    }
 581                }
 582            }
 583
 2584            await _context.SaveChangesAsync();
 585
 2586            return (true, null);
 587        }
 0588        catch (Exception ex)
 589        {
 0590            _logger.LogError(ex, "Error al actualizar personal");
 0591            return (false, $"Error al actualizar personal: {ex.Message}");
 592        }
 3593    }
 594
 595    public async Task<(bool success, string? error)> RegistrarBajaAsync(long id, RegistrarBajaRequest request)
 596    {
 597        try
 598        {
 599            // id is PersonaId: buscar la relación laboral activa de la persona
 4600            var relacionLaboral = await _context.RelacionesLaborales
 4601                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 602
 4603            if (relacionLaboral == null)
 604            {
 1605                return (false, "Relación laboral no encontrada");
 606            }
 607
 608            // Validar motivo de movimiento: debe existir y NO ser una alta
 3609            var tipoMovimiento = await _context.TiposMovimiento
 3610                .FirstOrDefaultAsync(t => t.Id == request.MotivoId);
 611
 3612            if (tipoMovimiento == null)
 613            {
 1614                return (false, "Motivo de baja no encontrado");
 615            }
 616
 2617            if (tipoMovimiento.EsAlta)
 618            {
 1619                return (false, "El motivo proporcionado corresponde a un alta, no puede usarse para una baja");
 620            }
 621
 622            // Validar que la fecha de baja no sea futura
 1623            var errorFechaBaja = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaBaja);
 1624            if (errorFechaBaja != null)
 625            {
 0626                return (false, errorFechaBaja);
 627            }
 628
 629            // Validar fecha de baja vs último movimiento y fecha de inicio
 1630            var validadorMovimiento = new MovimientoLaboralValidator(_context);
 1631            var errorFechaMovimiento = await validadorMovimiento.ValidarFechaMovimiento(relacionLaboral.Id, request.Fech
 1632            if (errorFechaMovimiento != null)
 633            {
 0634                return (false, errorFechaMovimiento);
 635            }
 636
 1637            var errorFechaBajaVsMovimiento = await validadorMovimiento.ValidarFechaBajaVsUltimoMovimiento(relacionLabora
 1638            if (errorFechaBajaVsMovimiento != null)
 639            {
 0640                return (false, errorFechaBajaVsMovimiento);
 641            }
 642
 643            // Validar transición de estado: debe ir de activo a inactivo
 1644            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1645                relacionLaboral.Estado.ToString(),
 1646                "Inactivo");
 647
 1648            if (errorTransicion != null)
 649            {
 0650                return (false, errorTransicion);
 651            }
 652
 653            // Cambiar estado a inactivo
 1654            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1655            var fechaBajaUtc = request.FechaBaja.Kind == DateTimeKind.Unspecified
 1656                ? DateTime.SpecifyKind(request.FechaBaja.Date, DateTimeKind.Utc)
 1657                : request.FechaBaja.Date.ToUniversalTime();
 1658            relacionLaboral.FechaFin = fechaBajaUtc;
 1659            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 660
 661            // Registrar movimiento de baja
 1662            var movimiento = new MovimientoLaboral
 1663            {
 1664                RelacionLaboralId = relacionLaboral.Id,
 1665                TipoMovimientoId = request.MotivoId,
 1666                FechaMovimiento = fechaBajaUtc,
 1667                UsuarioId = _currentUser.UserId ?? 0,
 1668                Observaciones = request.Observaciones
 1669            };
 670
 1671            _context.MovimientosLaborales.Add(movimiento);
 1672            await _context.SaveChangesAsync();
 673
 1674            await _auditoriaService.LogAuditoriaAsync(
 1675                usuarioId: _currentUser.UserId ?? 0,
 1676                AccionEnum.BajaPersonal,
 1677                ContextoEnum.Personal,
 1678                host: _currentUser.Host,
 1679                entidad: nameof(RelacionLaboral),
 1680                entidadId: relacionLaboral.Id,
 1681                detalle: new { personaId = id, relacionLaboralId = relacionLaboral.Id, request });
 682
 1683            return (true, null);
 684        }
 0685        catch (Exception ex)
 686        {
 0687            _logger.LogError(ex, "Error al registrar baja");
 0688            return (false, $"Error al registrar baja: {ex.Message}");
 689        }
 4690    }
 691
 692    public async Task<(bool success, string? error)> RegistrarAscensoAsync(long id, AscensoRequest request)
 693    {
 694        try
 695        {
 5696            var relacionLaboral = await _context.RelacionesLaborales
 5697                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 698
 5699            if (relacionLaboral == null)
 1700                return (false, "Relación laboral no encontrada");
 701
 702            // Validar grado objetivo
 4703            var grado = await _context.Grados.FirstOrDefaultAsync(g => g.Id == request.NuevoGradoId && g.Vigente);
 4704            if (grado == null)
 1705                return (false, "Grado no encontrado");
 706
 707            // Validar ley objetivo: ahora se recibe el número de ley como string en request.Ley
 3708            Entidades.Regimen? regimenAscenso = null;
 709
 3710            regimenAscenso = await _context.Regimenes.FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 3711            if (regimenAscenso == null)
 712            {
 1713                return (false, $"No se encontró la ley {request.Ley}");
 714            }
 715
 716            // Validar que el grado objetivo
 2717            if (relacionLaboral.GradoId != 0)
 718            {
 2719                var gradoActual = await _context.Grados.FirstOrDefaultAsync(g => g.Id == relacionLaboral.GradoId);
 2720                if (gradoActual != null)
 721                {
 2722                    if (grado.Orden <= gradoActual.Orden)
 723                    {
 1724                        return (false, "El grado objetivo debe ser superior al grado actual");
 725                    }
 726                }
 727            }
 728
 1729            var fechaAscensoUtc = request.FechaAscenso.Kind == DateTimeKind.Unspecified
 1730                ? DateTime.SpecifyKind(request.FechaAscenso.Date, DateTimeKind.Utc)
 1731                : request.FechaAscenso.Date.ToUniversalTime();
 732
 733            // Validar unidad objetivo si se proporciona
 734            Unidad? unidadObj = null;
 1735            if (request.UnidadId.HasValue)
 736            {
 0737                unidadObj = await _context.Unidades.FirstOrDefaultAsync(u => u.Id == request.UnidadId.Value && u.Vigente
 0738                if (unidadObj == null)
 739                {
 0740                    return (false, "Unidad no válida o no vigente");
 741                }
 742            }
 743
 744            // Cerrar la relación laboral actual
 745            // Validar transición: debe estar activo para poder ascender
 1746            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1747                relacionLaboral.Estado.ToString(),
 1748                "Inactivo");
 749
 1750            if (errorTransicion != null)
 751            {
 0752                return (false, errorTransicion);
 753            }
 754
 1755            relacionLaboral.FechaFin = fechaAscensoUtc;
 1756            relacionLaboral.FechaActualizacion = DateTime.UtcNow;
 1757            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1758            relacionLaboral.Observaciones = request.Observacion;
 759
 760            // Crear nueva relación laboral con el nuevo grado
 1761            var nuevaRelacion = new RelacionLaboral
 1762            {
 1763                PersonaId = relacionLaboral.PersonaId,
 1764                RegimenId = regimenAscenso.Id,
 1765                EscalafonId = relacionLaboral.EscalafonId,
 1766                GradoId = request.NuevoGradoId,
 1767                SituacionId = relacionLaboral.SituacionId,
 1768                ProgramaId = relacionLaboral.ProgramaId,
 1769                UnidadId = request.UnidadId ?? relacionLaboral.UnidadId,
 1770                CompaniaId = relacionLaboral.CompaniaId,
 1771                Estado = EstadoRelacion.Activo,
 1772                FechaInicio = fechaAscensoUtc,
 1773                Observaciones = relacionLaboral.Observaciones,
 1774                FechaActualizacion = DateTime.UtcNow
 1775            };
 776
 1777            _context.RelacionesLaborales.Add(nuevaRelacion);
 778
 779            // Registrar movimientos
 780            // Prefer specific tipo_movimiento ids: 6 = Cambio de Situacion (cierre), 7 = Ascenso (alta)
 1781            var tipoCierre = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("cambio
 1782            var tipoAlta = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("ascenso"
 783
 1784            if (tipoCierre == null)
 785            {
 1786                tipoCierre = new Entidades.TipoMovimiento { Nombre = "Cambio de Situacion", EsAlta = false };
 1787                _context.TiposMovimiento.Add(tipoCierre);
 1788                await _context.SaveChangesAsync();
 789            }
 790
 1791            if (tipoAlta == null)
 792            {
 1793                tipoAlta = new Entidades.TipoMovimiento { Nombre = "Ascenso", EsAlta = true };
 1794                _context.TiposMovimiento.Add(tipoAlta);
 1795                await _context.SaveChangesAsync();
 796            }
 797
 798            // Movimiento de cierre
 1799            var movimientoCierre = new Entidades.MovimientoLaboral
 1800            {
 1801                RelacionLaboralId = relacionLaboral.Id,
 1802                TipoMovimientoId = tipoCierre.Id,
 1803                FechaMovimiento = fechaAscensoUtc,
 1804                UsuarioId = _currentUser.UserId ?? 0,
 1805                Observaciones = request.Observacion
 1806            };
 807
 1808            _context.MovimientosLaborales.Add(movimientoCierre);
 809
 1810            await _context.SaveChangesAsync();
 811
 1812            var movimientoAlta = new Entidades.MovimientoLaboral
 1813            {
 1814                RelacionLaboralId = nuevaRelacion.Id,
 1815                TipoMovimientoId = tipoAlta.Id,
 1816                FechaMovimiento = fechaAscensoUtc,
 1817                UsuarioId = _currentUser.UserId ?? 0,
 1818                Observaciones = request.Observacion
 1819            };
 820
 1821            _context.MovimientosLaborales.Add(movimientoAlta);
 822
 1823            await _context.SaveChangesAsync();
 824
 1825            await _auditoriaService.LogAuditoriaAsync(
 1826                usuarioId: _currentUser.UserId ?? 0,
 1827                AccionEnum.AscensoPersonal,
 1828                ContextoEnum.Personal,
 1829                host: _currentUser.Host,
 1830                entidad: nameof(RelacionLaboral),
 1831                entidadId: nuevaRelacion.Id,
 1832                detalle: new { personaId = id, relacionAnteriorId = relacionLaboral.Id, relacionNuevaId = nuevaRelacion.
 833
 1834            return (true, null);
 835        }
 0836        catch (Exception ex)
 837        {
 0838            _logger.LogError(ex, "Error al registrar ascenso");
 0839            return (false, $"Error al registrar ascenso: {ex.Message}");
 840        }
 5841    }
 842
 843    private IQueryable<RelacionLaboral> ApplyOrdering(
 844        IQueryable<RelacionLaboral> query,
 845        string? orderBy,
 846        string sort)
 847    {
 1848        var ascending = sort.ToLower() == "asc";
 849
 1850        return orderBy?.ToLower() switch
 1851        {
 0852            "ci" => ascending
 0853                ? query.OrderBy(r => r.Persona.Cedula)
 0854                : query.OrderByDescending(r => r.Persona.Cedula),
 0855            "nombre" => ascending
 0856                ? query.OrderBy(r => r.Persona.PrimerNombre)
 0857                : query.OrderByDescending(r => r.Persona.PrimerNombre),
 0858            "apellido" => ascending
 0859                ? query.OrderBy(r => r.Persona.PrimerApellido)
 0860                : query.OrderByDescending(r => r.Persona.PrimerApellido),
 0861            "grado" => ascending
 0862                ? query.OrderBy(r => r.Grado.Orden)
 0863                : query.OrderByDescending(r => r.Grado.Orden),
 0864            "fechaingreso" => ascending
 0865                ? query.OrderBy(r => r.FechaInicio)
 0866                : query.OrderByDescending(r => r.FechaInicio),
 1867            _ => query.OrderByDescending(r => r.FechaInicio)
 1868        };
 869    }
 870
 871    #region Métodos Privados
 872
 873    /// <summary>
 874    /// Determina el régimen (Ley Vieja 14.157 / Ley Nueva 19.695) según la lógica del legacy MONTEPIO.PRG.
 875    /// Fecha límite de la reforma: 28/02/2019.
 876    /// - Escalafón AV (Oficiales): >= 15 años al 28/02/2019 → Ley Vieja
 877    /// - Otros escalafones (Subalternos): >= 3 años al 28/02/2019 → Ley Vieja
 878    /// - Ingreso posterior a la reforma → siempre Ley Nueva
 879    /// </summary>
 880    private async Task<long> DeterminarRegimenIdAsync(DateTime fechaIngreso, long? escalafonId)
 881    {
 1882        var fechaReforma = new DateTime(2019, 2, 28);
 883        string numeroLey;
 884
 1885        if (fechaIngreso.Date >= fechaReforma)
 886        {
 1887            numeroLey = "19.695";
 888        }
 889        else
 890        {
 0891            int aniosAlLimite = fechaReforma.Year - fechaIngreso.Year;
 0892            if (fechaIngreso.Date > fechaReforma.AddYears(-aniosAlLimite)) aniosAlLimite--;
 893
 0894            var escalafon = escalafonId.HasValue
 0895                ? await _context.Escalafones.FindAsync(escalafonId.Value)
 0896                : null;
 0897            bool esOficial = escalafon?.Codigo == "AV";
 0898            int umbral = esOficial ? 15 : 3;
 899
 0900            numeroLey = aniosAlLimite >= umbral ? "14.157" : "19.695";
 901        }
 902
 1903        var regimen = await _context.Regimenes
 1904            .FirstOrDefaultAsync(r => r.NumeroLey == numeroLey && r.Vigente);
 905
 906        // Fallback a Ley Nueva si no se encuentra (no debería ocurrir)
 1907        if (regimen == null)
 908        {
 0909            regimen = await _context.Regimenes
 0910                .FirstOrDefaultAsync(r => r.NumeroLey == "19.695" && r.Vigente);
 911        }
 912
 1913        return regimen!.Id;
 1914    }
 915
 916    #endregion
 917}