< Summary

Information
Class: FAU.Logica.Services.PeriodoService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/PeriodoService.cs
Line coverage
70%
Covered lines: 288
Uncovered lines: 120
Coverable lines: 408
Total lines: 578
Line coverage: 70.5%
Branch coverage
45%
Covered branches: 45
Total branches: 100
Branch coverage: 45%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
AbrirPeriodoAsync()53.57%312884.21%
ObtenerActivoAsync()100%210%
ObtenerEnCursoAsync()100%210%
ObtenerPorIdAsync(...)100%210%
ObtenerPrecondicionesAsync()0%420200%
Ok()100%210%
ValidarInsumosAsync()0%620%
CerrarInsumosAsync()80%242079.06%
ReabrirInsumosAsync()75%4495%
IniciarCalculoAsync()66.66%6690.9%
FinalizarCalculoAsync()66.66%6690.47%
ReabrirDesdeCalculadaAsync()0%2040%
CerrarAsync()75%4495%
SerializarSnapshotAsync()100%11100%
CalcularHash(...)100%11100%
ObtenerUltimosAsync()0%620%

File(s)

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

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3using System.Text.Json;
 4using FAU.DataAccess;
 5using FAU.DataAccess.Repositories;
 6using FAU.Entidades;
 7using FAU.Logica.DTOs;
 8using Microsoft.EntityFrameworkCore;
 9
 10namespace FAU.Logica.Services;
 11
 12public class PeriodoService : IPeriodoService
 13{
 14    private readonly IPeriodosRepository _periodosRepository;
 15    private readonly ISnapshotsRepository _snapshotsRepository;
 16    private readonly ApplicationDbContext _context;
 17    private readonly IAuditoriaService _auditoriaService;
 18    private readonly ICurrentUserContext _currentUser;
 19    private readonly IInsumoEvaluador _evaluador;
 20    private readonly IDescuentoPersonalRepository _descuentoPersonalRepo;
 21
 2522    public PeriodoService(
 2523        IPeriodosRepository periodosRepository,
 2524        ISnapshotsRepository snapshotsRepository,
 2525        ApplicationDbContext context,
 2526        IAuditoriaService auditoriaService,
 2527        ICurrentUserContext currentUser,
 2528        IInsumoEvaluador evaluador,
 2529        IDescuentoPersonalRepository descuentoPersonalRepo)
 30    {
 2531        _periodosRepository = periodosRepository;
 2532        _snapshotsRepository = snapshotsRepository;
 2533        _context = context;
 2534        _auditoriaService = auditoriaService;
 2535        _currentUser = currentUser;
 2536        _evaluador = evaluador;
 2537        _descuentoPersonalRepo = descuentoPersonalRepo;
 2538    }
 39
 40    public async Task<(bool Exito, Periodo? Periodo, string? Error)> AbrirPeriodoAsync(short anio, short mes)
 41    {
 942        if (anio < 2000 || anio > 2100)
 43        {
 044            return (false, null, "El año debe estar entre 2000 y 2100");
 45        }
 46
 947        if (mes < 1 || mes > 12)
 48        {
 049            return (false, null, "El mes debe estar entre 1 y 12");
 50        }
 51
 952        var periodoExistente = await _periodosRepository.ObtenerPorAnioMesAsync(anio, mes);
 953        if (periodoExistente is not null)
 54        {
 55            // Ya existe un período para este mes → devolver el existente si está abierto,
 56            // o un error descriptivo si está en otro estado (cerrado, calculado, etc.).
 157            if (periodoExistente.Estado == Periodo.EstadoAbierta && periodoExistente.Activo)
 158                return (true, periodoExistente, null);
 59
 060            var sugerencia = periodoExistente.Estado == Periodo.EstadoListaCalculo
 061                ? " Podés reabrirlo desde el panel del período."
 062                : periodoExistente.Estado == Periodo.EstadoCerrada
 063                    ? " Un período cerrado es definitivo."
 064                    : string.Empty;
 65
 066            return (false, null,
 067                $"Ya existe un período para {mes:D2}/{anio} en estado '{periodoExistente.Estado}'.{sugerencia}");
 68        }
 69
 870        var periodoEnCurso = await _periodosRepository.ObtenerEnCursoAsync();
 871        if (periodoEnCurso is not null)
 72        {
 173            return (false, null, $"Ya existe un período activo en estado {periodoEnCurso.Estado}");
 74        }
 75
 776        var parametrosPeriodo = await _context.ParametrosPeriodo
 777            .AsNoTracking()
 778            .FirstOrDefaultAsync(p => p.Anio == anio && p.Mes == mes);
 79
 780        if (parametrosPeriodo is null)
 81        {
 282            return (false, null, "No existen parámetros de liquidación para el período indicado");
 83        }
 84
 585        var fechaInicioPeriodo = new DateTime(anio, mes, 1, 0, 0, 0, DateTimeKind.Utc);
 586        var fechaFinPeriodo = fechaInicioPeriodo.AddMonths(1).AddTicks(-1);
 87
 588        var existeRemuneraciones = await _context.RemuneracionesGrado
 589            .AsNoTracking()
 590            .AnyAsync(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaIni
 91
 592        if (!existeRemuneraciones)
 93        {
 094            return (false, null, "No existen remuneraciones de grado vigentes para el período");
 95        }
 96
 597        var existeFranjaIrpf = await _context.FranjasIrpf
 598            .AsNoTracking()
 599            .AnyAsync(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaIni
 100
 5101        if (!existeFranjaIrpf)
 102        {
 0103            return (false, null, "No existen franjas IRPF vigentes para el período");
 104        }
 105
 5106        var existeRelacionLaboral = await _context.RelacionesLaborales
 5107            .AsNoTracking()
 5108            .AnyAsync(r => r.Estado == EstadoRelacion.Activo);
 109
 5110        if (!existeRelacionLaboral)
 111        {
 0112            return (false, null, "No hay funcionarios activos en el padrón");
 113        }
 114
 5115        var snapshotJson = await SerializarSnapshotAsync(anio, mes, parametrosPeriodo, fechaInicioPeriodo, fechaFinPerio
 5116        var hashSnapshot = CalcularHash(snapshotJson);
 117
 5118        var snapshot = new SnapshotInsumos
 5119        {
 5120            ContenidoJson = snapshotJson,
 5121            HashIntegridad = hashSnapshot
 5122        };
 123
 5124        var snapshotCreado = await _snapshotsRepository.CrearAsync(snapshot);
 125
 5126        var periodo = new Periodo
 5127        {
 5128            Anio = anio,
 5129            Mes = mes,
 5130            Estado = Periodo.EstadoAbierta,
 5131            SnapshotId = snapshotCreado.Id,
 5132            FechaApertura = DateTime.Now,
 5133            UsuarioApertura = _currentUser.UserId ?? 0,
 5134            HashSnapshot = hashSnapshot,
 5135            Activo = true
 5136        };
 137
 5138        var periodoCreado = await _periodosRepository.CrearAsync(periodo);
 139
 5140        await _auditoriaService.LogAuditoriaAsync(
 5141            _currentUser.UserId ?? 0,
 5142            AccionEnum.AbrirPeriodoLiquidacion,
 5143            ContextoEnum.Liquidaciones,
 5144            _currentUser.Host,
 5145            entidad: nameof(Periodo),
 5146            entidadId: periodoCreado.Id,
 5147            detalle: new
 5148            {
 5149                anio,
 5150                mes,
 5151                snapshotId = snapshotCreado.Id,
 5152                hashSnapshot
 5153            });
 154
 5155        return (true, periodoCreado, null);
 9156    }
 157
 158    public Task<Periodo?> ObtenerActivoAsync()
 159    {
 0160        return _periodosRepository.ObtenerActivoAsync();
 161    }
 162
 163    public Task<Periodo?> ObtenerEnCursoAsync()
 164    {
 0165        return _periodosRepository.ObtenerEnCursoAsync();
 166    }
 167
 168    public Task<Periodo?> ObtenerPorIdAsync(long id)
 169    {
 0170        return _periodosRepository.ObtenerPorIdAsync(id);
 171    }
 172
 173    public async Task<PrecondicionesDto?> ObtenerPrecondicionesAsync(long periodoId)
 174    {
 0175        var validacion = await ValidarInsumosAsync(periodoId);
 0176        if (validacion is null) return null;
 177
 0178        bool Ok(string tipo) => validacion.ItemsIncompletos.All(i => i.Tipo != tipo);
 0179        var lotesOk = Ok("lote_faltante");
 0180        var novedadesOk = Ok("novedades_pendientes");
 0181        var beneficiosOk = Ok("beneficios_faltantes");
 0182        var remuneracionesOk = Ok("tabla_referencia") ||
 0183            !validacion.ItemsIncompletos.Any(i => i.Clave == "SinRemuneraciones");
 0184        var franjasOk = !validacion.ItemsIncompletos.Any(i => i.Clave == "SinFranjasIrpf");
 0185        var funcionariosOk = !validacion.ItemsIncompletos.Any(i => i.Clave == "SinFuncionarios");
 186
 0187        return new PrecondicionesDto
 0188        {
 0189            LotesAprobados = new PrecondicionItemDto
 0190            {
 0191                Ok = lotesOk,
 0192                Detail = lotesOk ? "Lotes aprobados" : "Sin lotes aprobados para el período"
 0193            },
 0194            NovedadesSinConfirmar = new PrecondicionItemDto
 0195            {
 0196                Ok = novedadesOk,
 0197                Detail = novedadesOk ? "Sin novedades pendientes" :
 0198                    validacion.ItemsIncompletos.FirstOrDefault(i => i.Tipo == "novedades_pendientes")?.Descripcion ?? "N
 0199            },
 0200            BeneficiosVigentes = new PrecondicionItemDto
 0201            {
 0202                Ok = beneficiosOk,
 0203                Detail = beneficiosOk ? "Beneficios sociales activos" : "Sin beneficios sociales activos vigentes"
 0204            },
 0205            RemuneracionesVigentes = new PrecondicionItemDto
 0206            {
 0207                Ok = remuneracionesOk,
 0208                Detail = remuneracionesOk ? "Tablas de remuneración vigentes" : "Sin tablas de remuneración para el perí
 0209            },
 0210            FranjasIrpf = new PrecondicionItemDto
 0211            {
 0212                Ok = franjasOk,
 0213                Detail = franjasOk ? "Franjas IRPF vigentes" : "Sin franjas IRPF para el período"
 0214            },
 0215            FuncionariosActivos = new PrecondicionItemDto
 0216            {
 0217                Ok = funcionariosOk,
 0218                Detail = funcionariosOk ? "Funcionarios activos en el padrón" : "No hay funcionarios activos"
 0219            }
 0220        };
 0221    }
 222
 223    public async Task<ValidacionInsumosDto?> ValidarInsumosAsync(long periodoId)
 224    {
 0225        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 0226        if (periodo is null) return null;
 0227        return await _evaluador.EvaluarAsync(periodo);
 0228    }
 229
 230    public async Task<(bool Exito, Periodo? Periodo, ValidacionInsumosDto? Validacion, string? Error)> CerrarInsumosAsyn
 231        long periodoId, List<string>? confirmaciones = null)
 232    {
 8233        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 8234        if (periodo is null)
 0235            return (false, null, null, "Período no encontrado");
 236
 8237        if (periodo.Estado != Periodo.EstadoAbierta)
 1238            return (false, null, null, $"Solo se puede cerrar insumos desde estado ABIERTA. Estado actual: {periodo.Esta
 239
 7240        var validacion = await _evaluador.EvaluarAsync(periodo);
 241
 7242        if (!validacion.TodosCompletos)
 243        {
 5244            if (confirmaciones is null || confirmaciones.Count == 0)
 4245                return (false, null, validacion, "Hay insumos incompletos. Revisá el detalle y confirmá cada omisión par
 246
 2247            var bloqueantes = validacion.ItemsIncompletos.Where(i => !i.EsConfirmable).ToList();
 1248            if (bloqueantes.Count > 0)
 0249                return (false, null, validacion, "Existen problemas bloqueantes que no pueden omitirse.");
 250
 1251            var clavesSinConfirmar = validacion.ItemsIncompletos
 1252                .Where(i => !confirmaciones.Contains(i.Clave))
 1253                .ToList();
 1254            if (clavesSinConfirmar.Count > 0)
 0255                return (false, null, new ValidacionInsumosDto
 0256                {
 0257                    TodosCompletos = false,
 0258                    PuedeConfirmar = true,
 0259                    ItemsIncompletos = clavesSinConfirmar
 0260                }, "Faltan confirmar algunos items incompletos.");
 261        }
 262
 3263        if (confirmaciones?.Contains("DescuentosBorrador") == true)
 0264            await _descuentoPersonalRepo.ConfirmarBorradoresAsync(periodoId);
 265
 3266        var usuarioId = _currentUser.UserId ?? 0;
 3267        periodo.Estado = Periodo.EstadoListaCalculo;
 3268        periodo.FechaCierreInsumos = DateTime.Now;
 3269        periodo.UsuarioCierreInsumosId = usuarioId;
 3270        await _periodosRepository.ActualizarAsync(periodo);
 271
 3272        var omisiones = confirmaciones ?? new List<string>();
 3273        await _auditoriaService.LogAuditoriaAsync(
 3274            usuarioId,
 3275            AccionEnum.CerrarInsumosPeriodo,
 3276            ContextoEnum.Liquidaciones,
 3277            _currentUser.Host,
 3278            entidad: nameof(Periodo),
 3279            entidadId: periodo.Id,
 3280            detalle: new
 3281            {
 3282                anio = periodo.Anio,
 3283                mes = periodo.Mes,
 3284                omisionesConfirmadas = omisiones.Select(c =>
 0285                    validacion.ItemsIncompletos.FirstOrDefault(i => i.Clave == c)?.Descripcion ?? c)
 3286            });
 287
 3288        return (true, periodo, null, null);
 8289    }
 290
 291    public async Task<(bool Exito, Periodo? Periodo, string? Error)> ReabrirInsumosAsync(long periodoId)
 292    {
 3293        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3294        if (periodo is null)
 0295            return (false, null, "Período no encontrado");
 296
 3297        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1298            return (false, null, $"Solo se puede reabrir desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 299
 2300        var usuarioId = _currentUser.UserId ?? 0;
 2301        periodo.Estado = Periodo.EstadoAbierta;
 2302        periodo.FechaCierreInsumos = null;
 2303        periodo.UsuarioCierreInsumosId = null;
 2304        await _periodosRepository.ActualizarAsync(periodo);
 305
 2306        await _auditoriaService.LogAuditoriaAsync(
 2307            usuarioId,
 2308            AccionEnum.ReabrirInsumosPeriodo,
 2309            ContextoEnum.Liquidaciones,
 2310            _currentUser.Host,
 2311            entidad: nameof(Periodo),
 2312            entidadId: periodo.Id,
 2313            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 314
 2315        return (true, periodo, null);
 3316    }
 317
 318    public async Task<(bool Exito, Periodo? Periodo, string? Error)> IniciarCalculoAsync(long periodoId)
 319    {
 2320        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2321        if (periodo is null)
 0322            return (false, null, "Período no encontrado");
 323
 2324        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1325            return (false, null, $"Solo se puede calcular desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 326
 1327        if (await _descuentoPersonalRepo.TieneBorradoresAsync(periodoId))
 0328            return (false, null, "Existen descuentos personales sin confirmar. Confirme todos los borradores antes de in
 329
 1330        var usuarioId = _currentUser.UserId ?? 0;
 1331        periodo.Estado = Periodo.EstadoEnCalculo;
 1332        periodo.FechaCalculo = DateTime.Now;
 1333        periodo.UsuarioCalculoId = usuarioId;
 1334        await _periodosRepository.ActualizarAsync(periodo);
 335
 1336        await _auditoriaService.LogAuditoriaAsync(
 1337            usuarioId,
 1338            AccionEnum.IniciarCalculoPeriodo,
 1339            ContextoEnum.Liquidaciones,
 1340            _currentUser.Host,
 1341            entidad: nameof(Periodo),
 1342            entidadId: periodo.Id,
 1343            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 344
 1345        return (true, periodo, null);
 2346    }
 347
 348    public async Task<(bool Exito, Periodo? Periodo, string? Error)> FinalizarCalculoAsync(long periodoId, bool exitoso,
 349    {
 2350        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2351        if (periodo is null)
 0352            return (false, null, "Período no encontrado");
 353
 2354        if (periodo.Estado != Periodo.EstadoEnCalculo)
 0355            return (false, null, $"Solo se puede finalizar el cálculo desde estado EN_CALCULO. Estado actual: {periodo.E
 356
 2357        var usuarioId = _currentUser.UserId ?? 0;
 2358        if (exitoso)
 359        {
 1360            periodo.Estado = Periodo.EstadoCalculada;
 1361            periodo.FechaCalculo = DateTime.Now;
 362        }
 363        else
 364        {
 1365            periodo.Estado = Periodo.EstadoListaCalculo;
 366        }
 367
 2368        await _periodosRepository.ActualizarAsync(periodo);
 369
 2370        await _auditoriaService.LogAuditoriaAsync(
 2371            usuarioId,
 2372            AccionEnum.IniciarCalculoPeriodo,
 2373            ContextoEnum.Liquidaciones,
 2374            _currentUser.Host,
 2375            entidad: nameof(Periodo),
 2376            entidadId: periodo.Id,
 2377            detalle: new { exitoso, mensajeError });
 378
 2379        return (true, periodo, null);
 2380    }
 381
 382    /// <summary>
 383    /// Reabre un período CALCULADA → ABIERTA para permitir correcciones y un nuevo cálculo.
 384    /// Los ítems de la versión anterior quedan en la BD como historial de auditoría.
 385    /// El próximo cálculo genera version = max_version_anterior + 1.
 386    /// </summary>
 387    public async Task<(bool Exito, Periodo? Periodo, string? Error)> ReabrirDesdeCalculadaAsync(long periodoId)
 388    {
 0389        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 0390        if (periodo is null)
 0391            return (false, null, "Período no encontrado");
 392
 0393        if (periodo.Estado != Periodo.EstadoCalculada)
 0394            return (false, null,
 0395                $"Solo se puede reabrir desde estado CALCULADA. Estado actual: {periodo.Estado}");
 396
 0397        var usuarioId = _currentUser.UserId ?? 0;
 0398        periodo.Estado = Periodo.EstadoAbierta;
 399        // Limpiamos la fecha de cálculo para que quede como un período nuevo en proceso
 0400        periodo.FechaCalculo = null;
 0401        periodo.UsuarioCalculoId = null;
 0402        periodo.FechaCierreInsumos = null;
 0403        periodo.UsuarioCierreInsumosId = null;
 0404        await _periodosRepository.ActualizarAsync(periodo);
 405
 0406        await _auditoriaService.LogAuditoriaAsync(
 0407            usuarioId,
 0408            AccionEnum.AbrirPeriodoLiquidacion,
 0409            ContextoEnum.Liquidaciones,
 0410            _currentUser.Host,
 0411            entidad: nameof(Periodo),
 0412            entidadId: periodo.Id,
 0413            detalle: new { anio = periodo.Anio, mes = periodo.Mes, motivo = "reapertura desde CALCULADA para corrección"
 414
 0415        return (true, periodo, null);
 0416    }
 417
 418    public async Task<(bool Exito, Periodo? Periodo, string? Error)> CerrarAsync(long periodoId)
 419    {
 3420        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3421        if (periodo is null)
 0422            return (false, null, "Período no encontrado");
 423
 3424        if (periodo.Estado != Periodo.EstadoCalculada)
 1425            return (false, null, $"Solo se puede cerrar desde estado CALCULADA. Estado actual: {periodo.Estado}");
 426
 2427        var usuarioId = _currentUser.UserId ?? 0;
 2428        periodo.Estado = Periodo.EstadoCerrada;
 2429        periodo.FechaCierre = DateTime.Now;
 2430        periodo.UsuarioCierreId = usuarioId;
 2431        await _periodosRepository.ActualizarAsync(periodo);
 432
 2433        await _auditoriaService.LogAuditoriaAsync(
 2434            usuarioId,
 2435            AccionEnum.CerrarPeriodo,
 2436            ContextoEnum.Liquidaciones,
 2437            _currentUser.Host,
 2438            entidad: nameof(Periodo),
 2439            entidadId: periodo.Id,
 2440            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 441
 2442        return (true, periodo, null);
 3443    }
 444
 445    private async Task<string> SerializarSnapshotAsync(
 446        short anio,
 447        short mes,
 448        ParametroPeriodo parametrosPeriodo,
 449        DateTime fechaInicioPeriodo,
 450        DateTime fechaFinPeriodo)
 451    {
 5452        var lotes = await _context.LotesCompensacion
 5453            .AsNoTracking()
 5454            .Where(l => l.Periodo.Anio == anio && l.Periodo.Mes == mes && l.Estado == "aprobado")
 5455            .Select(l => new
 5456            {
 5457                l.Id,
 5458                l.TipoCompensacionId,
 5459                l.PeriodoId,
 5460                l.Estado,
 5461                l.Fuente,
 5462                l.NombreArchivo,
 5463                l.HashArchivo,
 5464                l.UsuarioId,
 5465                l.AprobadoPor,
 5466                l.AprobadoEn
 5467            })
 5468            .ToListAsync();
 469
 5470        var beneficios = await _context.BeneficiosSociales
 5471            .AsNoTracking()
 5472            .Where(b => b.Estado == "activo" && b.VigenteDesde <= fechaFinPeriodo && (b.VigenteHasta == null || b.Vigent
 5473            .Select(b => new
 5474            {
 5475                b.Id,
 5476                b.PersonaId,
 5477                b.TipoBeneficioId,
 5478                b.Cantidad,
 5479                b.VigenteDesde,
 5480                b.VigenteHasta,
 5481                b.Estado,
 5482                b.ClaveEvento,
 5483                b.FechaEvento,
 5484                b.RetroactividadPendiente
 5485            })
 5486            .ToListAsync();
 487
 5488        var remuneraciones = await _context.RemuneracionesGrado
 5489            .AsNoTracking()
 5490            .Where(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaInicio
 5491            .Select(r => new
 5492            {
 5493                r.Id,
 5494                r.GradoId,
 5495                r.ItemId,
 5496                r.Monto,
 5497                r.VigenteDesde,
 5498                r.VigenteHasta
 5499            })
 5500            .ToListAsync();
 501
 5502        var franjasIrpf = await _context.FranjasIrpf
 5503            .AsNoTracking()
 5504            .Where(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaInicio
 5505            .Select(f => new
 5506            {
 5507                f.Id,
 5508                f.NumeroFranja,
 5509                f.DesdeBpc,
 5510                f.HastaBpc,
 5511                f.Tasa,
 5512                f.VigenteDesde,
 5513                f.VigenteHasta
 5514            })
 5515            .ToListAsync();
 516
 5517        var snapshot = new
 5518        {
 5519            anio,
 5520            mes,
 5521            fechaCreacion = DateTime.Now,
 5522            parametros = new
 5523            {
 5524                parametrosPeriodo.Anio,
 5525                parametrosPeriodo.Mes,
 5526                parametrosPeriodo.BpcAnual,
 5527                parametrosPeriodo.TasaMontepioLeyVieja,
 5528                parametrosPeriodo.TasaMontepioLeyNueva,
 5529                parametrosPeriodo.TasaFonasa,
 5530                parametrosPeriodo.TasaFrl,
 5531                parametrosPeriodo.UmbralDeduccionBpc,
 5532                parametrosPeriodo.TasaDeduccionAlta,
 5533                parametrosPeriodo.TasaDeduccionBaja,
 5534                parametrosPeriodo.PorcentajeMinimoDeficit
 5535            },
 5536            lotesCompensacion = lotes,
 5537            beneficiosSociales = beneficios,
 5538            remuneracionesGrado = remuneraciones,
 5539            franjasIrpf = franjasIrpf
 5540        };
 541
 5542        return JsonSerializer.Serialize(snapshot, new JsonSerializerOptions
 5543        {
 5544            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 5545            WriteIndented = false
 5546        });
 5547    }
 548
 549    private static string CalcularHash(string contenidoJson)
 550    {
 5551        using var sha256 = SHA256.Create();
 5552        var bytes = Encoding.UTF8.GetBytes(contenidoJson);
 5553        return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
 5554    }
 555
 556    public async Task<List<Periodo>> ObtenerUltimosAsync(int cantidad = 6)
 557    {
 0558        var periodos = await _context.PeriodosLiquidacion
 0559            .AsNoTracking()
 0560            .Include(p => p.UsuarioCierreInsumos)
 0561            .Include(p => p.UsuarioCalculo)
 0562            .Include(p => p.UsuarioCierre)
 0563            .OrderByDescending(p => p.Anio)
 0564            .ThenByDescending(p => p.Mes)
 0565            .Take(cantidad)
 0566            .ToListAsync();
 567
 0568        var userIds = periodos.Select(p => p.UsuarioApertura).Distinct().ToList();
 0569        var usernames = await _context.Usuarios
 0570            .Where(u => userIds.Contains(u.Id))
 0571            .ToDictionaryAsync(u => u.Id, u => u.Username);
 572
 0573        foreach (var p in periodos)
 0574            p.UsuarioAperturaUsername = usernames.GetValueOrDefault(p.UsuarioApertura);
 575
 0576        return periodos;
 0577    }
 578}