< 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
76%
Covered lines: 595
Uncovered lines: 187
Coverable lines: 782
Total lines: 1177
Line coverage: 76%
Branch coverage
49%
Covered branches: 249
Total branches: 500
Branch coverage: 49.8%
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()80%1010100%
CreatePersonalAsync()51.78%965676.61%
UpdatePersonalAsync()56.43%39226487.75%
RegistrarBajaAsync()62.5%252486.44%
RegistrarAscensoAsync()75%333291.58%
ApplyOrdering(...)27.27%2262225%
DeterminarRegimenIdAsync()100%11100%
EsSituacionSuperior(...)50%1212100%
EsSituacionSubalterno(...)50%1010100%
ObtenerReporteMovimientosAsync()0%2162460%

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
 3323    public PersonalService(
 3324        IPersonalRepository personalRepository,
 3325        IMotivoBajaRepository motivoBajaRepository,
 3326        ApplicationDbContext context,
 3327        ILogger<PersonalService> logger,
 3328        IAuditoriaService auditoriaService,
 3329        ICurrentUserContext currentUser)
 30    {
 3331        _personalRepository = personalRepository;
 3332        _motivoBajaRepository = motivoBajaRepository;
 3333        _context = context;
 3334        _logger = logger;
 3335        _auditoriaService = auditoriaService;
 3336        _currentUser = currentUser;
 3337    }
 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            Grado = r.Grado.Denominacion,
 2118            Ley = r.Regimen.NumeroLey,
 2119            Estado = r.Estado.ToString(),
 2120            FechaIngreso = r.FechaInicio
 2121        }).ToList();
 122
 1123        return new
 1124        {
 1125            items,
 1126            total,
 1127            page,
 1128            pageSize,
 1129            totalPages = (int)Math.Ceiling((double)total / pageSize)
 1130        };
 1131    }
 132
 133    public async Task<PersonalDetalleDto?> GetDetalleAsync(long id)
 134    {
 135        // id is PersonaId: obtener la relación laboral activa o la última relación laboral
 5136        var relacionLaboral = await _context.RelacionesLaborales
 5137            .Include(r => r.Persona)
 5138            .Include(r => r.Regimen)
 5139            .Include(r => r.Escalafon)
 5140            .Include(r => r.Grado)
 5141            .Include(r => r.Situacion)
 5142            .Include(r => r.Programa)
 5143            .Include(r => r.Unidad)
 5144            .Include(r => r.SubUnidad)
 5145            .Include(r => r.MotivoBaja)
 5146            .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 147
 5148        if (relacionLaboral == null)
 149        {
 2150            relacionLaboral = await _context.RelacionesLaborales
 2151                .Include(r => r.Persona)
 2152                .Include(r => r.Regimen)
 2153                .Include(r => r.Escalafon)
 2154                .Include(r => r.Grado)
 2155                .Include(r => r.Situacion)
 2156                .Include(r => r.Programa)
 2157                .Include(r => r.Unidad)
 2158                .Include(r => r.SubUnidad)
 2159                .Include(r => r.MotivoBaja)
 2160                .Where(r => r.PersonaId == id)
 2161                .OrderByDescending(r => r.FechaInicio)
 2162                .FirstOrDefaultAsync();
 163        }
 164
 5165        if (relacionLaboral == null)
 1166            return null;
 167
 4168        var cuentaBancaria = await _context.CuentasBancarias
 4169            .Include(c => c.Banco)
 4170            .FirstOrDefaultAsync(c => c.PersonaId == relacionLaboral.PersonaId && c.Estado == "activa");
 171
 172        // Obtener todos los ids de relación laboral de la persona y luego el historial de movimientos para cualquiera d
 4173        var relacionIds = await _context.RelacionesLaborales
 4174            .Where(r => r.PersonaId == relacionLaboral.PersonaId)
 4175            .Select(r => r.Id)
 4176            .ToListAsync();
 177
 4178        var historial = await _context.MovimientosLaborales
 4179            .Include(m => m.TipoMovimiento)
 4180            .Include(m => m.Usuario)
 4181                .ThenInclude(u => u!.Persona)
 4182            .Where(m => relacionIds.Contains(m.RelacionLaboralId))
 4183            .OrderByDescending(m => m.Id)
 4184            .Select(m => new HistorialMovimientoDto
 4185            {
 4186                Id = m.Id,
 4187                TipoMovimiento = m.TipoMovimiento.Nombre,
 4188                EsAlta = m.TipoMovimiento.EsAlta,
 4189                FechaMovimiento = m.FechaMovimiento,
 4190                Observaciones = m.Observaciones,
 4191                RegistradoPor = m.Usuario != null ? m.Usuario.Username ?? string.Empty : string.Empty,
 4192                FechaRegistro = m.FechaMovimiento
 4193            })
 4194            .ToListAsync();
 195
 4196        return new PersonalDetalleDto
 4197        {
 4198            Id = relacionLaboral.Id,
 4199            DatosPersonales = new DatosPersonalesDto
 4200            {
 4201                PersonaId = relacionLaboral.PersonaId,
 4202                Ci = relacionLaboral.Persona.Cedula,
 4203                PrimerNombre = relacionLaboral.Persona.PrimerNombre,
 4204                SegundoNombre = relacionLaboral.Persona.SegundoNombre,
 4205                PrimerApellido = relacionLaboral.Persona.PrimerApellido,
 4206                SegundoApellido = relacionLaboral.Persona.SegundoApellido,
 4207                FechaNacimiento = relacionLaboral.Persona.FechaNacimiento,
 4208                Email = relacionLaboral.Persona.Email,
 4209                Telefono = relacionLaboral.Persona.Telefono,
 4210                Direccion = relacionLaboral.Persona.Direccion,
 4211                CodigoPostal = relacionLaboral.Persona.CodigoPostal,
 4212                Departamento = relacionLaboral.Persona.Departamento,
 4213                Localidad = relacionLaboral.Persona.Localidad,
 4214                CuentaBancaria = cuentaBancaria == null ? null : new CuentaBancariaDto
 4215                {
 4216                    Id = cuentaBancaria.Id,
 4217                    BancoId = cuentaBancaria.BancoId,
 4218                    Banco = cuentaBancaria.Banco.Nombre,
 4219                    NumeroCuenta = cuentaBancaria.NumeroCuenta
 4220                }
 4221            },
 4222            DatosLaborales = new DatosLaboralesDto
 4223            {
 4224                RelacionLaboralId = relacionLaboral.Id,
 4225                Estado = relacionLaboral.Estado.ToString(),
 4226                FechaInicio = relacionLaboral.FechaInicio,
 4227                FechaFin = relacionLaboral.FechaFin,
 4228                RegimenId = relacionLaboral.RegimenId,
 4229                Regimen = relacionLaboral.Regimen.Denominacion,
 4230                Ley = relacionLaboral.Regimen.NumeroLey,
 4231                EscalafonId = relacionLaboral.EscalafonId,
 4232                Escalafon = relacionLaboral.Escalafon.Denominacion,
 4233                GradoId = relacionLaboral.GradoId,
 4234                Grado = relacionLaboral.Grado.Denominacion,
 4235                SituacionId = relacionLaboral.SituacionId,
 4236                Situacion = relacionLaboral.Situacion.Denominacion,
 4237                ProgramaId = relacionLaboral.ProgramaId,
 4238                Programa = relacionLaboral.Programa.Denominacion,
 4239                UnidadId = relacionLaboral.UnidadId,
 4240                Unidad = relacionLaboral.Unidad.Denominacion,
 4241                SubUnidadId = relacionLaboral.SubUnidadId,
 4242                SubUnidad = relacionLaboral.SubUnidad?.Denominacion,
 4243                MotivoBajaId = relacionLaboral.MotivoBajaId,
 4244                MotivoBaja = relacionLaboral.MotivoBaja?.Denominacion,
 4245                RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 4246                CategoriaViatico = relacionLaboral.CategoriaViatico,
 4247                UsaFonasa = relacionLaboral.UsaFonasa,
 4248                AniosServicioAnterior = relacionLaboral.AniosServicioAnterior,
 4249                PrimaTecnica = relacionLaboral.PrimaTecnica,
 4250                PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 4251                AniosInactivos = relacionLaboral.AniosInactivos,
 4252                Observaciones = relacionLaboral.Observaciones
 4253            },
 4254            Historial = historial
 4255        };
 5256    }
 257
 258    public async Task<(bool success, long? personalId, string? error)> CreatePersonalAsync(CreatePersonalRequest request
 259    {
 260        try
 261        {
 262            // Validar y limpiar CI
 7263            var cedulaLimpia = CedulaValidator.Limpiar(request.Ci);
 7264            if (!CedulaValidator.Validar(cedulaLimpia))
 265            {
 1266                return (false, null, "La cédula ingresada no es válida.");
 267            }
 268
 269            // Validar que la fecha de ingreso no sea futura
 6270            var errorFechaIngreso = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaIngreso);
 6271            if (errorFechaIngreso != null)
 272            {
 1273                return (false, null, errorFechaIngreso);
 274            }
 275
 276            // Validar que no exista una relación laboral activa para esa cédula
 5277            var validadorRelacion = new RelacionLaboralValidator(_context);
 5278            var errorRelacionActiva = await validadorRelacion.ObtenerErrorSiExisteRelacionActiva(cedulaLimpia);
 5279            if (errorRelacionActiva != null)
 280            {
 1281                return (false, null, errorRelacionActiva);
 282            }
 283
 284            // Validar que el grado pertenece al escalafón indicado
 4285            var escalafonId = request.EscalafonId ?? 1;
 4286            var grado = await _context.Grados.FindAsync(request.GradoId);
 4287            if (grado == null)
 0288                return (false, null, "El grado seleccionado no existe.");
 289            // TODO: reactivar validación cuando el frontend envíe EscalafonId correctamente
 290            // if (grado.EscalafonId != escalafonId)
 291            //     return (false, null, "El grado seleccionado no pertenece al escalafón indicado.");
 292
 293            // Validar fecha de nacimiento
 4294            if (request.FechaNacimiento != default)
 295            {
 4296                var fechaNac = request.FechaNacimiento.Date;
 4297                if (fechaNac > DateTime.Now.Date)
 0298                    return (false, null, "La fecha de nacimiento no puede ser futura.");
 4299                if (fechaNac < new DateTime(1940, 1, 1))
 0300                    return (false, null, "La fecha de nacimiento no puede ser anterior a 01/01/1940.");
 301            }
 302
 303            // Buscar o crear persona
 4304            Persona? persona = await _context.Personas.FirstOrDefaultAsync(p => p.Cedula == cedulaLimpia);
 305
 4306            if (persona == null)
 307            {
 308                // Crear persona - Convertir fechas a UTC
 4309                var fechaNacimientoUtc = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 4310                    ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 4311                    : request.FechaNacimiento.Date.ToUniversalTime();
 312
 4313                persona = new Persona
 4314                {
 4315                    Cedula = cedulaLimpia,
 4316                    PrimerNombre = request.PrimerNombre,
 4317                    SegundoNombre = request.SegundoNombre,
 4318                    PrimerApellido = request.PrimerApellido,
 4319                    SegundoApellido = request.SegundoApellido,
 4320                    FechaNacimiento = fechaNacimientoUtc,
 4321                    Email = request.Email,
 4322                    Telefono = request.Telefono,
 4323                    Direccion = request.Direccion,
 4324                    CodigoPostal = request.CodigoPostal,
 4325                    Departamento = request.Departamento,
 4326                    Localidad = request.Localidad
 4327                };
 328
 4329                _context.Personas.Add(persona);
 4330                await _context.SaveChangesAsync();
 331            }
 332            else
 333            {
 334                // Persona ya existe: actualizar campos de dirección si vienen en el request
 0335                bool personaModificada = false;
 0336                if (request.CodigoPostal != null) { persona.CodigoPostal = request.CodigoPostal; personaModificada = tru
 0337                if (request.Departamento != null) { persona.Departamento = request.Departamento; personaModificada = tru
 0338                if (request.Localidad != null) { persona.Localidad = request.Localidad; personaModificada = true; }
 0339                if (personaModificada)
 340                {
 0341                    persona.FechaActualizacion = DateTime.Now;
 0342                    _context.Personas.Update(persona);
 0343                    await _context.SaveChangesAsync();
 344                }
 345            }
 346
 347            // Determinar régimen: explícito si se pasa, sino auto-calcular
 348            long regimenId;
 4349            if (!string.IsNullOrWhiteSpace(request.Ley))
 350            {
 0351                var regimenExplicito = await _context.Regimenes
 0352                    .FirstOrDefaultAsync(r => r.NumeroLey == request.Ley && r.Vigente);
 0353                if (regimenExplicito == null)
 0354                    return (false, null, $"No se encontró el régimen con ley {request.Ley}");
 0355                regimenId = regimenExplicito.Id;
 356            }
 357            else
 358            {
 4359                regimenId = await DeterminarRegimenIdAsync(request.FechaIngreso, request.EscalafonId);
 360            }
 361
 362            // Validar que la situación fue explícitamente indicada
 4363            if (!request.SituacionId.HasValue)
 0364                return (false, null, "La situación es requerida.");
 365
 366            // Validar coherencia entre grado y situación
 4367            var situacion = await _context.Situaciones.FindAsync(request.SituacionId.Value);
 4368            if (situacion == null)
 0369                return (false, null, "La situación seleccionada no existe.");
 370
 4371            if (EsSituacionSuperior(situacion.Codigo) && grado.EsSubalterno)
 0372                return (false, null, $"El grado '{grado.Codigo}' corresponde a personal subalterno; no puede asignarse a
 4373            if (EsSituacionSubalterno(situacion.Codigo) && grado.EsOficial)
 0374                return (false, null, $"El grado '{grado.Codigo}' corresponde a personal superior/oficial; no puede asign
 375
 376            // Crear relación laboral - Convertir fechas a UTC
 4377            var fechaIngresoUtc = request.FechaIngreso.Kind == DateTimeKind.Unspecified
 4378                ? DateTime.SpecifyKind(request.FechaIngreso.Date, DateTimeKind.Utc)
 4379                : request.FechaIngreso.Date.ToUniversalTime();
 380
 4381            var relacionLaboral = new RelacionLaboral
 4382            {
 4383                PersonaId = persona.Id,
 4384                RegimenId = regimenId,
 4385                EscalafonId = request.EscalafonId ?? 1,
 4386                GradoId = request.GradoId,
 4387                SituacionId = request.SituacionId.Value,
 4388                ProgramaId = request.ProgramaId ?? 1,
 4389                UnidadId = request.UnidadId ?? 1,
 4390                SubUnidadId = request.SubUnidadId,
 4391                Estado = EstadoRelacion.Activo,
 4392                FechaInicio = fechaIngresoUtc,
 4393                Observaciones = request.Observaciones,
 4394                // Campos de compensación desde el alta
 4395                RiesgoVuelo = request.RiesgoVuelo,
 4396                PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? null : request.PrimaTecnica.Trim(),
 4397                CategoriaViatico = request.CategoriaViatico ?? 0,
 4398                UsaFonasa = request.UsaFonasa ?? false,
 4399                AniosServicioAnterior = request.AniosServicioAnterior ?? 0,
 4400            };
 401
 4402            _context.RelacionesLaborales.Add(relacionLaboral);
 4403            await _context.SaveChangesAsync();
 404
 4405            if (request.BancoId.HasValue)
 406            {
 2407                var banco = await _context.Bancos.FindAsync(request.BancoId.Value);
 2408                if (banco == null)
 1409                    return (false, null, "El banco seleccionado no existe.");
 410
 1411                var errorCuenta = NumeroCuentaValidator.Validar(banco.Codigo, request.NumeroCuenta);
 1412                if (errorCuenta != null)
 0413                    return (false, null, errorCuenta);
 414
 1415                _context.CuentasBancarias.Add(new CuentaBancaria
 1416                {
 1417                    PersonaId = persona.Id,
 1418                    BancoId = request.BancoId.Value,
 1419                    NumeroCuenta = request.NumeroCuenta != null ? NumeroCuentaValidator.FormatearParaGuardar(banco.Codig
 1420                    Estado = "activa"
 1421                });
 1422                await _context.SaveChangesAsync();
 423            }
 424
 425            // Prima Solidaria Familiar NO se asigna automáticamente — no aplica a todos.
 426            // Se carga manualmente desde Beneficios Sociales cuando corresponde.
 427
 3428            await _auditoriaService.LogAuditoriaAsync(
 3429                usuarioId: _currentUser.UserId ?? 0,
 3430                AccionEnum.CrearPersonal,
 3431                ContextoEnum.Personal,
 3432                host: _currentUser.Host,
 3433                entidad: nameof(Persona),
 3434                entidadId: persona.Id,
 3435                detalle: new { relacionLaboralId = relacionLaboral.Id, personaId = persona.Id, cedula = persona.Cedula }
 436
 3437            return (true, persona.Id, null);
 438        }
 0439        catch (Exception ex)
 440        {
 0441            _logger.LogError(ex, "Error al crear personal");
 0442            var errorMessage = ex.Message;
 0443            var innerEx = ex.InnerException;
 0444            while (innerEx != null)
 445            {
 0446                errorMessage += $" | {innerEx.Message}";
 0447                innerEx = innerEx.InnerException;
 448            }
 0449            return (false, null, $"Error al crear personal: {errorMessage}");
 450        }
 7451    }
 452
 453    public async Task<(bool success, string? error)> UpdatePersonalAsync(long id, UpdatePersonalRequest request)
 454    {
 455        try
 456        {
 11457            var persona = await _context.Personas.FindAsync(id);
 11458            if (persona == null)
 1459                return (false, "Persona no encontrada");
 460
 461            // Validar campos de compensación si se proporcionan
 10462            var errorCompensacion = CompensacionValidator.ValidarCompensaciones(
 10463                request.RiesgoVuelo,
 10464                request.PrimaTecnica,
 10465                request.PrimaSolidariaFamiliar,
 10466                request.AniosInactivos);
 467
 10468            if (errorCompensacion != null)
 469            {
 0470                return (false, errorCompensacion);
 471            }
 472
 473            // Actualizar datos personales
 10474            if (!string.IsNullOrWhiteSpace(request.PrimerNombre))
 3475                persona.PrimerNombre = request.PrimerNombre.Trim();
 476
 10477            persona.SegundoNombre = string.IsNullOrWhiteSpace(request.SegundoNombre) ? null : request.SegundoNombre.Trim
 478
 10479            if (!string.IsNullOrWhiteSpace(request.PrimerApellido))
 1480                persona.PrimerApellido = request.PrimerApellido.Trim();
 481
 10482            persona.SegundoApellido = string.IsNullOrWhiteSpace(request.SegundoApellido) ? null : request.SegundoApellid
 483
 10484            if (request.FechaNacimiento.HasValue)
 485            {
 0486                var fechaNac = request.FechaNacimiento.Value;
 0487                persona.FechaNacimiento = fechaNac.Kind == DateTimeKind.Unspecified
 0488                    ? DateTime.SpecifyKind(fechaNac.Date, DateTimeKind.Utc)
 0489                    : fechaNac.Date.ToUniversalTime();
 490            }
 491
 10492            if (!string.IsNullOrWhiteSpace(request.Email))
 1493                persona.Email = request.Email.Trim();
 494
 10495            persona.Telefono = string.IsNullOrWhiteSpace(request.Telefono) ? null : request.Telefono.Trim();
 10496            persona.Direccion = string.IsNullOrWhiteSpace(request.Direccion) ? null : request.Direccion.Trim();
 10497            persona.CodigoPostal = string.IsNullOrWhiteSpace(request.CodigoPostal) ? null : request.CodigoPostal.Trim();
 10498            persona.Departamento = string.IsNullOrWhiteSpace(request.Departamento) ? null : request.Departamento.Trim();
 10499            persona.Localidad = string.IsNullOrWhiteSpace(request.Localidad) ? null : request.Localidad.Trim();
 500
 10501            if (persona.FechaNacimiento.HasValue)
 502            {
 1503                var fn = persona.FechaNacimiento.Value;
 1504                persona.FechaNacimiento = fn.Kind == DateTimeKind.Unspecified
 1505                    ? DateTime.SpecifyKind(fn.Date, DateTimeKind.Utc)
 1506                    : fn.ToUniversalTime();
 507            }
 508
 10509            persona.FechaActualizacion = DateTime.Now;
 10510            _context.Personas.Update(persona);
 511
 512            // Buscar relación activa para la persona
 10513            var relacionActiva = await _context.RelacionesLaborales
 10514                .FirstOrDefaultAsync(r => r.PersonaId == persona.Id && r.Estado == EstadoRelacion.Activo);
 515
 516            // Obtener la última relación (puede ser null)
 10517            var ultimaRelacion = await _context.RelacionesLaborales
 10518                .Where(r => r.PersonaId == persona.Id)
 10519                .OrderByDescending(r => r.FechaInicio)
 10520                .FirstOrDefaultAsync();
 521
 522            // Validar transición de estado usando la relación más relevante (activa si existe, sino la última)
 10523            var relacionParaValidacion = relacionActiva ?? ultimaRelacion;
 524
 10525            if (!string.IsNullOrWhiteSpace(request.Estado) && relacionParaValidacion != null)
 526            {
 0527                var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 0528                    relacionParaValidacion.Estado.ToString(),
 0529                    request.Estado);
 0530                if (errorTransicion != null)
 0531                    return (false, errorTransicion);
 532            }
 533
 534            // Determinar si se solicitaron cambios laborales
 10535            bool cambioLaboralSolicitado = request.GradoId.HasValue || request.RegimenId.HasValue || request.EscalafonId
 10536                request.SituacionId.HasValue || request.ProgramaId.HasValue || request.UnidadId.HasValue || request.SubU
 537
 10538            if (relacionActiva != null)
 539            {
 540                // Si hay relación activa, verificar si los cambios requieren crear una nueva relación
 6541                bool requiereNueva = false;
 7542                if (request.GradoId.HasValue && request.GradoId.Value != relacionActiva.GradoId) requiereNueva = true;
 6543                if (request.RegimenId.HasValue && request.RegimenId.Value != relacionActiva.RegimenId) requiereNueva = t
 6544                if (request.EscalafonId.HasValue && request.EscalafonId.Value != relacionActiva.EscalafonId) requiereNue
 6545                if (request.SituacionId.HasValue && request.SituacionId.Value != relacionActiva.SituacionId) requiereNue
 6546                if (request.ProgramaId.HasValue && request.ProgramaId.Value != relacionActiva.ProgramaId) requiereNueva 
 6547                if (request.UnidadId.HasValue && request.UnidadId.Value != relacionActiva.UnidadId) requiereNueva = true
 6548                if (request.SubUnidadId.HasValue && request.SubUnidadId.Value != relacionActiva.SubUnidadId) requiereNue
 549
 6550                if (requiereNueva)
 551                {
 552                    // Cerrar relación activa
 1553                    relacionActiva.FechaFin = DateTime.Now;
 1554                    relacionActiva.Estado = EstadoRelacion.Inactivo;
 1555                    relacionActiva.FechaActualizacion = DateTime.Now;
 556
 557                    // Crear nueva relación activa con los valores actualizados
 1558                    var nuevaRelacion = new RelacionLaboral
 1559                    {
 1560                        PersonaId = persona.Id,
 1561                        RegimenId = request.RegimenId ?? relacionActiva.RegimenId,
 1562                        EscalafonId = request.EscalafonId ?? relacionActiva.EscalafonId,
 1563                        GradoId = request.GradoId ?? relacionActiva.GradoId,
 1564                        SituacionId = request.SituacionId ?? relacionActiva.SituacionId,
 1565                        ProgramaId = request.ProgramaId ?? relacionActiva.ProgramaId,
 1566                        UnidadId = request.UnidadId ?? relacionActiva.UnidadId,
 1567                        SubUnidadId = request.SubUnidadId ?? relacionActiva.SubUnidadId,
 1568                        Estado = EstadoRelacion.Activo,
 1569                        // Se preserva la FechaInicio original: esta rama es un cambio de
 1570                        // grado/escalafón/situación/etc SIN que la persona haya dejado de
 1571                        // estar activa (no es una reincorporación). Resetearla a "ahora" acá
 1572                        // rompía CalculadorHaberesBase (Progresivo, PROGRE.TROPA, antigüedad),
 1573                        // que usa FechaInicio como fecha de ingreso real — confirmado con el
 1574                        // recibo de la cédula 51344118 tras un cambio de escalafón.
 1575                        FechaInicio = relacionActiva.FechaInicio,
 1576                        Observaciones = request.Observaciones ?? relacionActiva.Observaciones,
 1577                        FechaActualizacion = DateTime.Now,
 1578                        RiesgoVuelo = request.RiesgoVuelo ?? relacionActiva.RiesgoVuelo,
 1579                        CategoriaViatico = request.CategoriaViatico ?? relacionActiva.CategoriaViatico,
 1580                        UsaFonasa = request.UsaFonasa ?? relacionActiva.UsaFonasa,
 1581                        AniosServicioAnterior = request.AniosServicioAnterior ?? relacionActiva.AniosServicioAnterior,
 1582                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? relacionActiva.PrimaTecnica : r
 1583                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? relacionAct
 1584                        AniosInactivos = request.AniosInactivos ?? relacionActiva.AniosInactivos,
 1585                        // Campos históricos que deben preservarse al abrir una nueva relación por
 1586                        // cambio de grado/escalafón/situación/etc — antes se perdían (quedaban NULL),
 1587                        // rompiendo el cálculo de antigüedad (Progresivo) y otros ítems dependientes.
 1588                        FechaAscensoOficial = relacionActiva.FechaAscensoOficial,
 1589                        FechaUltimoAscenso = relacionActiva.FechaUltimoAscenso,
 1590                        Superprima = relacionActiva.Superprima,
 1591                        HaberRetiro = relacionActiva.HaberRetiro,
 1592                        PorcentajeProgresivo = relacionActiva.PorcentajeProgresivo,
 1593                        GradoReincorporacionId = relacionActiva.GradoReincorporacionId
 1594                    };
 595
 1596                    _context.RelacionesLaborales.Add(nuevaRelacion);
 597                }
 598                else
 599                {
 600                    // Actualizar en sitio la relación activa
 5601                    if (!string.IsNullOrWhiteSpace(request.Estado))
 602                    {
 0603                        if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0604                            relacionActiva.Estado = est;
 605                        else
 606                        {
 0607                            var estLower = request.Estado.Trim().ToLower();
 0608                            if (estLower == "activo") relacionActiva.Estado = EstadoRelacion.Activo;
 0609                            else if (estLower == "inactivo") relacionActiva.Estado = EstadoRelacion.Inactivo;
 610                        }
 611                    }
 612
 5613                    if (request.RegimenId.HasValue) relacionActiva.RegimenId = request.RegimenId.Value;
 5614                    if (request.EscalafonId.HasValue) relacionActiva.EscalafonId = request.EscalafonId.Value;
 5615                    if (request.GradoId.HasValue) relacionActiva.GradoId = request.GradoId.Value;
 5616                    if (request.SituacionId.HasValue) relacionActiva.SituacionId = request.SituacionId.Value;
 5617                    if (request.ProgramaId.HasValue) relacionActiva.ProgramaId = request.ProgramaId.Value;
 5618                    if (request.UnidadId.HasValue) relacionActiva.UnidadId = request.UnidadId.Value;
 5619                    if (request.SubUnidadId.HasValue) relacionActiva.SubUnidadId = request.SubUnidadId.Value;
 620
 5621                    if (request.RiesgoVuelo.HasValue) relacionActiva.RiesgoVuelo = request.RiesgoVuelo;
 5622                    if (request.CategoriaViatico.HasValue) relacionActiva.CategoriaViatico = request.CategoriaViatico.Va
 6623                    if (request.UsaFonasa.HasValue) relacionActiva.UsaFonasa = request.UsaFonasa.Value;
 6624                    if (request.AniosServicioAnterior.HasValue) relacionActiva.AniosServicioAnterior = request.AniosServ
 5625                    if (request.PrimaTecnica != null) relacionActiva.PrimaTecnica = string.IsNullOrWhiteSpace(request.Pr
 5626                    if (request.PrimaSolidariaFamiliar != null) relacionActiva.PrimaSolidariaFamiliar = string.IsNullOrW
 5627                    if (request.AniosInactivos.HasValue) relacionActiva.AniosInactivos = request.AniosInactivos;
 6628                    if (request.Observaciones != null) relacionActiva.Observaciones = request.Observaciones;
 629
 5630                    relacionActiva.FechaActualizacion = DateTime.Now;
 631                }
 632            }
 633            else
 634            {
 635                // No hay relación activa
 4636                if (cambioLaboralSolicitado)
 637                {
 638                    // Crear nueva relación activa basándose en la última relación si existe
 1639                    var plantilla = ultimaRelacion;
 640
 1641                    var nuevaRelacion = new RelacionLaboral
 1642                    {
 1643                        PersonaId = persona.Id,
 1644                        RegimenId = request.RegimenId ?? plantilla?.RegimenId ?? 0,
 1645                        EscalafonId = request.EscalafonId ?? plantilla?.EscalafonId ?? 0,
 1646                        GradoId = request.GradoId ?? plantilla?.GradoId ?? 0,
 1647                        SituacionId = request.SituacionId ?? plantilla?.SituacionId ?? 0,
 1648                        ProgramaId = request.ProgramaId ?? plantilla?.ProgramaId ?? 0,
 1649                        UnidadId = request.UnidadId ?? plantilla?.UnidadId ?? 0,
 1650                        SubUnidadId = request.SubUnidadId ?? plantilla?.SubUnidadId,
 1651                        Estado = EstadoRelacion.Activo,
 1652                        FechaInicio = DateTime.Now,
 1653                        Observaciones = request.Observaciones ?? plantilla?.Observaciones,
 1654                        FechaActualizacion = DateTime.Now,
 1655                        RiesgoVuelo = request.RiesgoVuelo ?? plantilla?.RiesgoVuelo,
 1656                        CategoriaViatico = request.CategoriaViatico ?? plantilla?.CategoriaViatico ?? 0,
 1657                        UsaFonasa = request.UsaFonasa ?? plantilla?.UsaFonasa ?? false,
 1658                        AniosServicioAnterior = request.AniosServicioAnterior ?? plantilla?.AniosServicioAnterior ?? 0,
 1659                        PrimaTecnica = string.IsNullOrWhiteSpace(request.PrimaTecnica) ? plantilla?.PrimaTecnica : reque
 1660                        PrimaSolidariaFamiliar = string.IsNullOrWhiteSpace(request.PrimaSolidariaFamiliar) ? plantilla?.
 1661                        AniosInactivos = request.AniosInactivos ?? plantilla?.AniosInactivos,
 1662                        // Ver comentario análogo en la rama "requiereNueva" de arriba.
 1663                        FechaAscensoOficial = plantilla?.FechaAscensoOficial,
 1664                        FechaUltimoAscenso = plantilla?.FechaUltimoAscenso,
 1665                        Superprima = plantilla?.Superprima ?? false,
 1666                        HaberRetiro = plantilla?.HaberRetiro,
 1667                        PorcentajeProgresivo = plantilla?.PorcentajeProgresivo,
 1668                        GradoReincorporacionId = plantilla?.GradoReincorporacionId
 1669                    };
 670
 1671                    _context.RelacionesLaborales.Add(nuevaRelacion);
 672                }
 673                else
 674                {
 675                    // Solo actualizar estado u observaciones de la última relación si existe
 3676                    if (ultimaRelacion != null)
 677                    {
 1678                        if (!string.IsNullOrWhiteSpace(request.Estado))
 679                        {
 0680                            if (Enum.TryParse<EstadoRelacion>(request.Estado, true, out var est))
 0681                                ultimaRelacion.Estado = est;
 682                            else
 683                            {
 0684                                var estLower = request.Estado.Trim().ToLower();
 0685                                if (estLower == "activo") ultimaRelacion.Estado = EstadoRelacion.Activo;
 0686                                else if (estLower == "inactivo") ultimaRelacion.Estado = EstadoRelacion.Inactivo;
 687                            }
 688                        }
 689
 2690                        if (request.Observaciones != null) ultimaRelacion.Observaciones = request.Observaciones;
 1691                        ultimaRelacion.FechaActualizacion = DateTime.Now;
 692                    }
 693                }
 694            }
 695
 10696            if (request.BancoId.HasValue)
 697            {
 2698                var banco = await _context.Bancos.FindAsync(request.BancoId.Value);
 2699                if (banco == null)
 1700                    return (false, "El banco seleccionado no existe.");
 701
 1702                var errorCuenta = NumeroCuentaValidator.Validar(banco.Codigo, request.NumeroCuenta);
 1703                if (errorCuenta != null)
 0704                    return (false, errorCuenta);
 705
 1706                var cuentasActivas = await _context.CuentasBancarias
 1707                    .Where(c => c.PersonaId == persona.Id && c.Estado == "activa")
 1708                    .ToListAsync();
 4709                foreach (var c in cuentasActivas)
 1710                    c.Estado = "inactiva";
 711
 1712                _context.CuentasBancarias.Add(new CuentaBancaria
 1713                {
 1714                    PersonaId = persona.Id,
 1715                    BancoId = request.BancoId.Value,
 1716                    NumeroCuenta = request.NumeroCuenta != null ? NumeroCuentaValidator.FormatearParaGuardar(banco.Codig
 1717                    Estado = "activa"
 1718                });
 1719            }
 8720            else if (request.LimpiarCuentaBancaria)
 721            {
 1722                var cuentasActivas = await _context.CuentasBancarias
 1723                    .Where(c => c.PersonaId == persona.Id && c.Estado == "activa")
 1724                    .ToListAsync();
 4725                foreach (var c in cuentasActivas)
 1726                    c.Estado = "inactiva";
 727            }
 728
 9729            await _context.SaveChangesAsync();
 730
 9731            return (true, null);
 732        }
 0733        catch (Exception ex)
 734        {
 0735            _logger.LogError(ex, "Error al actualizar personal");
 0736            return (false, $"Error al actualizar personal: {ex.Message}");
 737        }
 11738    }
 739
 740    public async Task<(bool success, string? error)> RegistrarBajaAsync(long id, RegistrarBajaRequest request)
 741    {
 742        try
 743        {
 744            // id is PersonaId: buscar la relación laboral activa de la persona
 4745            var relacionLaboral = await _context.RelacionesLaborales
 4746                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 747
 4748            if (relacionLaboral == null)
 749            {
 1750                return (false, "Relación laboral no encontrada");
 751            }
 752
 753            // Validar y obtener el MotivoBaja (catálogo UCU)
 3754            if (request.MotivoBajaId <= 0)
 1755                return (false, "El motivo de baja (catálogo) es requerido");
 756
 2757            var motivoBaja = await _motivoBajaRepository.GetByIdAsync(request.MotivoBajaId);
 2758            if (motivoBaja == null)
 0759                return (false, "Motivo de baja (catálogo) no encontrado o no vigente");
 760
 761            // Determinar TipoMovimiento automáticamente según el motivo:
 762            //   RETIRO_OBL / RETIRO_VOL → "Retiro" | resto → "Baja"
 2763            var nombreTipo = motivoBaja.Codigo is "RETIRO_OBL" or "RETIRO_VOL" ? "Retiro" : "Baja";
 2764            var tipoMovimiento = await _context.TiposMovimiento
 2765                .FirstOrDefaultAsync(t => t.Nombre == nombreTipo && !t.EsAlta);
 766
 2767            if (tipoMovimiento == null)
 1768                return (false, $"Tipo de movimiento '{nombreTipo}' no encontrado en el catálogo");
 769
 770            // Validar que la fecha de baja no sea futura
 1771            var errorFechaBaja = FechaIngresoValidator.ObtenerErrorSiEsFutura(request.FechaBaja);
 1772            if (errorFechaBaja != null)
 773            {
 0774                return (false, errorFechaBaja);
 775            }
 776
 777            // Validar fecha de baja vs último movimiento y fecha de inicio
 1778            var validadorMovimiento = new MovimientoLaboralValidator(_context);
 1779            var errorFechaMovimiento = await validadorMovimiento.ValidarFechaMovimiento(relacionLaboral.Id, request.Fech
 1780            if (errorFechaMovimiento != null)
 781            {
 0782                return (false, errorFechaMovimiento);
 783            }
 784
 1785            var errorFechaBajaVsMovimiento = await validadorMovimiento.ValidarFechaBajaVsUltimoMovimiento(relacionLabora
 1786            if (errorFechaBajaVsMovimiento != null)
 787            {
 0788                return (false, errorFechaBajaVsMovimiento);
 789            }
 790
 791            // Validar transición de estado: debe ir de activo a inactivo
 1792            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 1793                relacionLaboral.Estado.ToString(),
 1794                "Inactivo");
 795
 1796            if (errorTransicion != null)
 797            {
 0798                return (false, errorTransicion);
 799            }
 800
 801            // Cambiar estado a inactivo
 1802            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 1803            var fechaBajaUtc = request.FechaBaja.Kind == DateTimeKind.Unspecified
 1804                ? DateTime.SpecifyKind(request.FechaBaja.Date, DateTimeKind.Utc)
 1805                : request.FechaBaja.Date.ToUniversalTime();
 1806            relacionLaboral.FechaFin = fechaBajaUtc;
 1807            relacionLaboral.MotivoBajaId = request.MotivoBajaId;
 1808            relacionLaboral.FechaActualizacion = DateTime.Now;
 809
 810            // Registrar movimiento de baja
 1811            var movimiento = new MovimientoLaboral
 1812            {
 1813                RelacionLaboralId = relacionLaboral.Id,
 1814                TipoMovimientoId = tipoMovimiento.Id,
 1815                FechaMovimiento = fechaBajaUtc,
 1816                UsuarioId = _currentUser.UserId ?? 0,
 1817                Observaciones = request.Observaciones
 1818            };
 819
 1820            _context.MovimientosLaborales.Add(movimiento);
 1821            await _context.SaveChangesAsync();
 822
 1823            await _auditoriaService.LogAuditoriaAsync(
 1824                usuarioId: _currentUser.UserId ?? 0,
 1825                AccionEnum.BajaPersonal,
 1826                ContextoEnum.Personal,
 1827                host: _currentUser.Host,
 1828                entidad: nameof(RelacionLaboral),
 1829                entidadId: relacionLaboral.Id,
 1830                detalle: new { personaId = id, relacionLaboralId = relacionLaboral.Id, request });
 831
 1832            return (true, null);
 833        }
 0834        catch (Exception ex)
 835        {
 0836            _logger.LogError(ex, "Error al registrar baja");
 0837            return (false, $"Error al registrar baja: {ex.Message}");
 838        }
 4839    }
 840
 841    public async Task<(bool success, string? error)> RegistrarAscensoAsync(long id, AscensoRequest request)
 842    {
 843        try
 844        {
 5845            var relacionLaboral = await _context.RelacionesLaborales
 5846                .FirstOrDefaultAsync(r => r.PersonaId == id && r.Estado == EstadoRelacion.Activo);
 847
 5848            if (relacionLaboral == null)
 1849                return (false, "Relación laboral no encontrada");
 850
 851            // Validar grado objetivo
 4852            var grado = await _context.Grados.FirstOrDefaultAsync(g => g.Id == request.NuevoGradoId && g.Vigente);
 4853            if (grado == null)
 1854                return (false, "Grado no encontrado");
 855
 856            // Calcular régimen automáticamente según fecha de ingreso y escalafón (igual que en el Alta)
 3857            var regimenAscensoId = await DeterminarRegimenIdAsync(relacionLaboral.FechaInicio, relacionLaboral.Escalafon
 3858            var regimenAscenso = await _context.Regimenes.FindAsync(regimenAscensoId);
 3859            if (regimenAscenso == null)
 0860                return (false, "No se pudo determinar el régimen para el ascenso");
 861
 862            // Validar que el grado objetivo
 3863            if (relacionLaboral.GradoId != 0)
 864            {
 3865                var gradoActual = await _context.Grados.FirstOrDefaultAsync(g => g.Id == relacionLaboral.GradoId);
 3866                if (gradoActual != null)
 867                {
 3868                    if (grado.Orden <= gradoActual.Orden)
 869                    {
 1870                        return (false, "El grado objetivo debe ser superior al grado actual");
 871                    }
 872                }
 873            }
 874
 2875            var fechaAscensoUtc = request.FechaAscenso.Kind == DateTimeKind.Unspecified
 2876                ? DateTime.SpecifyKind(request.FechaAscenso.Date, DateTimeKind.Utc)
 2877                : request.FechaAscenso.Date.ToUniversalTime();
 878
 879            // Validar unidad objetivo si se proporciona
 880            Unidad? unidadObj = null;
 2881            if (request.UnidadId.HasValue)
 882            {
 0883                unidadObj = await _context.Unidades.FirstOrDefaultAsync(u => u.Id == request.UnidadId.Value && u.Vigente
 0884                if (unidadObj == null)
 885                {
 0886                    return (false, "Unidad no válida o no vigente");
 887                }
 888            }
 889
 890            // Cerrar la relación laboral actual
 891            // Validar transición: debe estar activo para poder ascender
 2892            var errorTransicion = EstadoTransicionValidator.ObtenerErrorSiTransicionInvalida(
 2893                relacionLaboral.Estado.ToString(),
 2894                "Inactivo");
 895
 2896            if (errorTransicion != null)
 897            {
 0898                return (false, errorTransicion);
 899            }
 900
 2901            relacionLaboral.FechaFin = fechaAscensoUtc;
 2902            relacionLaboral.FechaActualizacion = DateTime.Now;
 2903            relacionLaboral.Estado = EstadoRelacion.Inactivo;
 2904            relacionLaboral.Observaciones = request.Observacion;
 905
 906            // Validar programa objetivo
 2907            var programaAscenso = await _context.Programas.FirstOrDefaultAsync(p => p.Id == request.ProgramaId && p.Vige
 2908            if (programaAscenso == null)
 1909                return (false, "Programa no válido o no vigente");
 910
 911            // Determinar prima técnica: al ascender a Sgto. 1° se desactiva la prima técnica variable
 1912            var primaTecnicaNueva = relacionLaboral.PrimaTecnica;
 1913            if (grado.Codigo == "SGTO_1RO")
 0914                primaTecnicaNueva = "VACIO";
 915
 916            // Crear nueva relación laboral con el nuevo grado
 1917            var nuevaRelacion = new RelacionLaboral
 1918            {
 1919                PersonaId = relacionLaboral.PersonaId,
 1920                RegimenId = regimenAscenso.Id,
 1921                EscalafonId = relacionLaboral.EscalafonId,
 1922                GradoId = request.NuevoGradoId,
 1923                SituacionId = relacionLaboral.SituacionId,
 1924                ProgramaId = request.ProgramaId,
 1925                UnidadId = request.UnidadId ?? relacionLaboral.UnidadId,
 1926                SubUnidadId = request.SubUnidadId ?? relacionLaboral.SubUnidadId,
 1927                Estado = EstadoRelacion.Activo,
 1928                FechaInicio = fechaAscensoUtc,
 1929                PrimaTecnica = primaTecnicaNueva,
 1930                PrimaSolidariaFamiliar = relacionLaboral.PrimaSolidariaFamiliar,
 1931                RiesgoVuelo = relacionLaboral.RiesgoVuelo,
 1932                CategoriaViatico = relacionLaboral.CategoriaViatico,
 1933                UsaFonasa = relacionLaboral.UsaFonasa,
 1934                AniosServicioAnterior = relacionLaboral.AniosServicioAnterior,
 1935                Observaciones = relacionLaboral.Observaciones,
 1936                FechaActualizacion = DateTime.Now
 1937            };
 938
 1939            _context.RelacionesLaborales.Add(nuevaRelacion);
 940
 941            // Registrar movimientos
 942            // Prefer specific tipo_movimiento ids: 6 = Cambio de Situacion (cierre), 7 = Ascenso (alta)
 1943            var tipoCierre = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("cambio
 1944            var tipoAlta = await _context.TiposMovimiento.FirstOrDefaultAsync(t => t.Nombre.ToLower().Contains("ascenso"
 945
 1946            if (tipoCierre == null)
 947            {
 1948                tipoCierre = new Entidades.TipoMovimiento { Nombre = "Cambio de Situacion", EsAlta = false };
 1949                _context.TiposMovimiento.Add(tipoCierre);
 1950                await _context.SaveChangesAsync();
 951            }
 952
 1953            if (tipoAlta == null)
 954            {
 1955                tipoAlta = new Entidades.TipoMovimiento { Nombre = "Ascenso", EsAlta = true };
 1956                _context.TiposMovimiento.Add(tipoAlta);
 1957                await _context.SaveChangesAsync();
 958            }
 959
 960            // Movimiento de cierre
 1961            var movimientoCierre = new Entidades.MovimientoLaboral
 1962            {
 1963                RelacionLaboralId = relacionLaboral.Id,
 1964                TipoMovimientoId = tipoCierre.Id,
 1965                FechaMovimiento = fechaAscensoUtc,
 1966                UsuarioId = _currentUser.UserId ?? 0,
 1967                Observaciones = request.Observacion,
 1968                Puesto = request.Puesto,
 1969                Plaza = request.Plaza
 1970            };
 971
 1972            _context.MovimientosLaborales.Add(movimientoCierre);
 973
 1974            await _context.SaveChangesAsync();
 975
 1976            var movimientoAlta = new Entidades.MovimientoLaboral
 1977            {
 1978                RelacionLaboralId = nuevaRelacion.Id,
 1979                TipoMovimientoId = tipoAlta.Id,
 1980                FechaMovimiento = fechaAscensoUtc,
 1981                UsuarioId = _currentUser.UserId ?? 0,
 1982                Observaciones = request.Observacion,
 1983                Puesto = request.Puesto,
 1984                Plaza = request.Plaza
 1985            };
 986
 1987            _context.MovimientosLaborales.Add(movimientoAlta);
 988
 1989            await _context.SaveChangesAsync();
 990
 1991            await _auditoriaService.LogAuditoriaAsync(
 1992                usuarioId: _currentUser.UserId ?? 0,
 1993                AccionEnum.AscensoPersonal,
 1994                ContextoEnum.Personal,
 1995                host: _currentUser.Host,
 1996                entidad: nameof(RelacionLaboral),
 1997                entidadId: nuevaRelacion.Id,
 1998                detalle: new { personaId = id, relacionAnteriorId = relacionLaboral.Id, relacionNuevaId = nuevaRelacion.
 999
 11000            return (true, null);
 1001        }
 01002        catch (Exception ex)
 1003        {
 01004            _logger.LogError(ex, "Error al registrar ascenso");
 01005            return (false, $"Error al registrar ascenso: {ex.Message}");
 1006        }
 51007    }
 1008
 1009    private IQueryable<RelacionLaboral> ApplyOrdering(
 1010        IQueryable<RelacionLaboral> query,
 1011        string? orderBy,
 1012        string sort)
 1013    {
 11014        var ascending = sort.ToLower() == "asc";
 1015
 11016        return orderBy?.ToLower() switch
 11017        {
 01018            "ci" => ascending
 01019                ? query.OrderBy(r => r.Persona.Cedula)
 01020                : query.OrderByDescending(r => r.Persona.Cedula),
 01021            "nombre" => ascending
 01022                ? query.OrderBy(r => r.Persona.PrimerNombre)
 01023                : query.OrderByDescending(r => r.Persona.PrimerNombre),
 01024            "apellido" => ascending
 01025                ? query.OrderBy(r => r.Persona.PrimerApellido)
 01026                : query.OrderByDescending(r => r.Persona.PrimerApellido),
 01027            "grado" => ascending
 01028                ? query.OrderBy(r => r.Grado.Orden)
 01029                : query.OrderByDescending(r => r.Grado.Orden),
 01030            "fechaingreso" => ascending
 01031                ? query.OrderBy(r => r.FechaInicio)
 01032                : query.OrderByDescending(r => r.FechaInicio),
 11033            _ => query.OrderByDescending(r => r.FechaInicio)
 11034        };
 1035    }
 1036
 1037    #region Métodos Privados
 1038
 1039    /// <summary>
 1040    /// Fallback cuando el alta no especifica la ley explícitamente.
 1041    /// En el legacy (FoxPro), PERSONAL.Ley_Vieja es un campo manual por persona —
 1042    /// no existe una regla automática confiable (depende de la situación previsional
 1043    /// real del funcionario al momento de la reforma). Por eso siempre se retorna
 1044    /// Ley Nueva (19.695) como fallback seguro, y se recomienda enviar el campo
 1045    /// Ley explícitamente en el alta.
 1046    /// </summary>
 1047    private async Task<long> DeterminarRegimenIdAsync(DateTime fechaIngreso, long? escalafonId)
 1048    {
 71049        var regimen = await _context.Regimenes
 71050            .FirstOrDefaultAsync(r => r.NumeroLey == "19.695" && r.Vigente);
 71051        return regimen!.Id;
 71052    }
 1053
 1054    private static bool EsSituacionSuperior(string codigo) =>
 41055        codigo is "SIT01" or "SIT02" or "SIT04" or "SIT05" or "SIT06" or "SIT16";
 1056
 1057    private static bool EsSituacionSubalterno(string codigo) =>
 41058        codigo is "SIT07" or "SIT08" or "SIT09" or "SIT11" or "SIT17";
 1059
 1060    #endregion
 1061
 1062    public async Task<List<MovimientoReporteDto>> ObtenerReporteMovimientosAsync(int mes, int anio, string? tipo)
 1063    {
 01064        var tipoNorm = tipo?.ToLower();
 1065
 1066        // Traer movimientos del mes/año con sus relaciones
 01067        var movimientos = await _context.MovimientosLaborales
 01068            .Include(m => m.TipoMovimiento)
 01069            .Include(m => m.RelacionLaboral)
 01070                .ThenInclude(r => r.Persona)
 01071            .Include(m => m.RelacionLaboral)
 01072                .ThenInclude(r => r.Grado)
 01073            .Include(m => m.RelacionLaboral)
 01074                .ThenInclude(r => r.Escalafon)
 01075            .Include(m => m.RelacionLaboral)
 01076                .ThenInclude(r => r.Unidad)
 01077            .Include(m => m.RelacionLaboral)
 01078                .ThenInclude(r => r.Programa)
 01079            .Include(m => m.RelacionLaboral)
 01080                .ThenInclude(r => r.Regimen)
 01081            .Where(m => m.FechaMovimiento.Month == mes && m.FechaMovimiento.Year == anio)
 01082            .OrderBy(m => m.FechaMovimiento)
 01083            .ToListAsync();
 1084
 1085        // Para ascensos: agrupar los movimientos de cierre + alta para emitir un solo renglón
 01086        var resultado = new List<MovimientoReporteDto>();
 01087        var procesados = new HashSet<long>();
 1088
 01089        foreach (var mov in movimientos)
 1090        {
 01091            if (procesados.Contains(mov.Id)) continue;
 1092
 01093            var nombreTipo = mov.TipoMovimiento.Nombre.ToLower();
 1094            string categoriaReporte;
 1095
 01096            if (nombreTipo.Contains("alta por ascenso") || nombreTipo.Contains("ascenso"))
 1097            {
 01098                categoriaReporte = "Ascenso";
 01099                if (tipoNorm != null && tipoNorm != "ascenso" && tipoNorm != "todos") continue;
 1100
 1101                // Buscar el movimiento de cierre del mismo día para obtener grado anterior
 01102                var cierreRelacionado = movimientos.FirstOrDefault(m =>
 01103                    !procesados.Contains(m.Id) &&
 01104                    m.Id != mov.Id &&
 01105                    m.FechaMovimiento.Date == mov.FechaMovimiento.Date &&
 01106                    m.RelacionLaboral.PersonaId == mov.RelacionLaboral.PersonaId &&
 01107                    (m.TipoMovimiento.Nombre.ToLower().Contains("baja por ascenso") ||
 01108                     m.TipoMovimiento.Nombre.ToLower().Contains("cambio de situaci")));
 1109
 01110                var gradoAnterior = cierreRelacionado?.RelacionLaboral.Grado.Denominacion;
 01111                if (cierreRelacionado != null) procesados.Add(cierreRelacionado.Id);
 1112
 01113                resultado.Add(new MovimientoReporteDto
 01114                {
 01115                    TipoMovimiento = categoriaReporte,
 01116                    Grado = mov.RelacionLaboral.Grado.Denominacion,
 01117                    GradoAnterior = gradoAnterior,
 01118                    Escalafon = mov.RelacionLaboral.Escalafon.Denominacion,
 01119                    Cedula = mov.RelacionLaboral.Persona.Cedula,
 01120                    Nombre = mov.RelacionLaboral.Persona.PrimerNombre,
 01121                    Apellido = mov.RelacionLaboral.Persona.PrimerApellido,
 01122                    Unidad = mov.RelacionLaboral.Unidad.Denominacion,
 01123                    Programa = mov.RelacionLaboral.Programa.Denominacion,
 01124                    Ley = mov.RelacionLaboral.Regimen.NumeroLey,
 01125                    FechaMovimiento = mov.FechaMovimiento
 01126                });
 1127            }
 01128            else if (mov.TipoMovimiento.EsAlta &&
 01129                     !nombreTipo.Contains("baja por ascenso") &&
 01130                     !nombreTipo.Contains("cambio de situaci"))
 1131            {
 01132                categoriaReporte = "Alta";
 01133                if (tipoNorm != null && tipoNorm != "alta" && tipoNorm != "todos") continue;
 1134
 01135                resultado.Add(new MovimientoReporteDto
 01136                {
 01137                    TipoMovimiento = categoriaReporte,
 01138                    Grado = mov.RelacionLaboral.Grado.Denominacion,
 01139                    Escalafon = mov.RelacionLaboral.Escalafon.Denominacion,
 01140                    Cedula = mov.RelacionLaboral.Persona.Cedula,
 01141                    Nombre = mov.RelacionLaboral.Persona.PrimerNombre,
 01142                    Apellido = mov.RelacionLaboral.Persona.PrimerApellido,
 01143                    Unidad = mov.RelacionLaboral.Unidad.Denominacion,
 01144                    Programa = mov.RelacionLaboral.Programa.Denominacion,
 01145                    Ley = mov.RelacionLaboral.Regimen.NumeroLey,
 01146                    FechaMovimiento = mov.FechaMovimiento
 01147                });
 1148            }
 01149            else if (!mov.TipoMovimiento.EsAlta &&
 01150                     !nombreTipo.Contains("baja por ascenso") &&
 01151                     !nombreTipo.Contains("cambio de situaci") &&
 01152                     !nombreTipo.Contains("ascenso"))
 1153            {
 01154                categoriaReporte = "Baja";
 01155                if (tipoNorm != null && tipoNorm != "baja" && tipoNorm != "todos") continue;
 1156
 01157                resultado.Add(new MovimientoReporteDto
 01158                {
 01159                    TipoMovimiento = categoriaReporte,
 01160                    Grado = mov.RelacionLaboral.Grado.Denominacion,
 01161                    Escalafon = mov.RelacionLaboral.Escalafon.Denominacion,
 01162                    Cedula = mov.RelacionLaboral.Persona.Cedula,
 01163                    Nombre = mov.RelacionLaboral.Persona.PrimerNombre,
 01164                    Apellido = mov.RelacionLaboral.Persona.PrimerApellido,
 01165                    Unidad = mov.RelacionLaboral.Unidad.Denominacion,
 01166                    Programa = mov.RelacionLaboral.Programa.Denominacion,
 01167                    Ley = mov.RelacionLaboral.Regimen.NumeroLey,
 01168                    FechaMovimiento = mov.FechaMovimiento
 01169                });
 1170            }
 1171
 01172            procesados.Add(mov.Id);
 1173        }
 1174
 01175        return resultado;
 01176    }
 1177}