< 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
72%
Covered lines: 287
Uncovered lines: 108
Coverable lines: 395
Total lines: 563
Line coverage: 72.6%
Branch coverage
42%
Covered branches: 42
Total branches: 100
Branch coverage: 42%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
AbrirPeriodoAsync()53.57%312884.21%
ObtenerActivoAsync()100%210%
ObtenerEnCursoAsync()100%210%
ObtenerPorIdAsync(...)100%210%
ObtenerPrecondicionesAsync()0%420200%
Ok()100%210%
Detail()0%4260%
ValidarInsumosAsync()0%620%
CerrarInsumosAsync()81.25%181680.48%
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()100%210%

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. En desarrollo podés eliminarlo de la BD para recrearlo."
 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.UtcNow,
 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);
 179        string Detail(string tipo, string okMsg, string failMsg) =>
 0180            Ok(tipo) ? okMsg : (validacion.ItemsIncompletos.FirstOrDefault(i => i.Tipo == tipo)?.Descripcion ?? failMsg)
 181
 0182        var lotesOk = Ok("lote_faltante");
 0183        var novedadesOk = Ok("novedades_pendientes");
 0184        var beneficiosOk = Ok("beneficios_faltantes");
 0185        var remuneracionesOk = Ok("tabla_referencia") ||
 0186            !validacion.ItemsIncompletos.Any(i => i.Clave == "SinRemuneraciones");
 0187        var franjasOk = !validacion.ItemsIncompletos.Any(i => i.Clave == "SinFranjasIrpf");
 0188        var funcionariosOk = !validacion.ItemsIncompletos.Any(i => i.Clave == "SinFuncionarios");
 189
 0190        return new PrecondicionesDto
 0191        {
 0192            LotesAprobados = new PrecondicionItemDto
 0193            {
 0194                Ok = lotesOk,
 0195                Detail = lotesOk ? "Lotes aprobados" : "Sin lotes aprobados para el período"
 0196            },
 0197            NovedadesSinConfirmar = new PrecondicionItemDto
 0198            {
 0199                Ok = novedadesOk,
 0200                Detail = novedadesOk ? "Sin novedades pendientes" :
 0201                    validacion.ItemsIncompletos.FirstOrDefault(i => i.Tipo == "novedades_pendientes")?.Descripcion ?? "N
 0202            },
 0203            BeneficiosVigentes = new PrecondicionItemDto
 0204            {
 0205                Ok = beneficiosOk,
 0206                Detail = beneficiosOk ? "Beneficios sociales activos" : "Sin beneficios sociales activos vigentes"
 0207            },
 0208            RemuneracionesVigentes = new PrecondicionItemDto
 0209            {
 0210                Ok = remuneracionesOk,
 0211                Detail = remuneracionesOk ? "Tablas de remuneración vigentes" : "Sin tablas de remuneración para el perí
 0212            },
 0213            FranjasIrpf = new PrecondicionItemDto
 0214            {
 0215                Ok = franjasOk,
 0216                Detail = franjasOk ? "Franjas IRPF vigentes" : "Sin franjas IRPF para el período"
 0217            },
 0218            FuncionariosActivos = new PrecondicionItemDto
 0219            {
 0220                Ok = funcionariosOk,
 0221                Detail = funcionariosOk ? "Funcionarios activos en el padrón" : "No hay funcionarios activos"
 0222            }
 0223        };
 0224    }
 225
 226    public async Task<ValidacionInsumosDto?> ValidarInsumosAsync(long periodoId)
 227    {
 0228        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 0229        if (periodo is null) return null;
 0230        return await _evaluador.EvaluarAsync(periodo);
 0231    }
 232
 233    public async Task<(bool Exito, Periodo? Periodo, ValidacionInsumosDto? Validacion, string? Error)> CerrarInsumosAsyn
 234        long periodoId, List<string>? confirmaciones = null)
 235    {
 8236        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 8237        if (periodo is null)
 0238            return (false, null, null, "Período no encontrado");
 239
 8240        if (periodo.Estado != Periodo.EstadoAbierta)
 1241            return (false, null, null, $"Solo se puede cerrar insumos desde estado ABIERTA. Estado actual: {periodo.Esta
 242
 7243        var validacion = await _evaluador.EvaluarAsync(periodo);
 244
 7245        if (!validacion.TodosCompletos)
 246        {
 5247            if (confirmaciones is null || confirmaciones.Count == 0)
 4248                return (false, null, validacion, "Hay insumos incompletos. Revisá el detalle y confirmá cada omisión par
 249
 2250            var bloqueantes = validacion.ItemsIncompletos.Where(i => !i.EsConfirmable).ToList();
 1251            if (bloqueantes.Count > 0)
 0252                return (false, null, validacion, "Existen problemas bloqueantes que no pueden omitirse.");
 253
 1254            var clavesSinConfirmar = validacion.ItemsIncompletos
 1255                .Where(i => !confirmaciones.Contains(i.Clave))
 1256                .ToList();
 1257            if (clavesSinConfirmar.Count > 0)
 0258                return (false, null, new ValidacionInsumosDto
 0259                {
 0260                    TodosCompletos = false,
 0261                    PuedeConfirmar = true,
 0262                    ItemsIncompletos = clavesSinConfirmar
 0263                }, "Faltan confirmar algunos items incompletos.");
 264        }
 265
 3266        var usuarioId = _currentUser.UserId ?? 0;
 3267        periodo.Estado = Periodo.EstadoListaCalculo;
 3268        periodo.FechaCierreInsumos = DateTime.UtcNow;
 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.UtcNow;
 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.UtcNow;
 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        await _periodosRepository.ActualizarAsync(periodo);
 403
 0404        await _auditoriaService.LogAuditoriaAsync(
 0405            usuarioId,
 0406            AccionEnum.AbrirPeriodoLiquidacion,
 0407            ContextoEnum.Liquidaciones,
 0408            _currentUser.Host,
 0409            entidad: nameof(Periodo),
 0410            entidadId: periodo.Id,
 0411            detalle: new { anio = periodo.Anio, mes = periodo.Mes, motivo = "reapertura desde CALCULADA para corrección"
 412
 0413        return (true, periodo, null);
 0414    }
 415
 416    public async Task<(bool Exito, Periodo? Periodo, string? Error)> CerrarAsync(long periodoId)
 417    {
 3418        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3419        if (periodo is null)
 0420            return (false, null, "Período no encontrado");
 421
 3422        if (periodo.Estado != Periodo.EstadoCalculada)
 1423            return (false, null, $"Solo se puede cerrar desde estado CALCULADA. Estado actual: {periodo.Estado}");
 424
 2425        var usuarioId = _currentUser.UserId ?? 0;
 2426        periodo.Estado = Periodo.EstadoCerrada;
 2427        periodo.FechaCierre = DateTime.UtcNow;
 2428        periodo.UsuarioCierreId = usuarioId;
 2429        await _periodosRepository.ActualizarAsync(periodo);
 430
 2431        await _auditoriaService.LogAuditoriaAsync(
 2432            usuarioId,
 2433            AccionEnum.CerrarPeriodo,
 2434            ContextoEnum.Liquidaciones,
 2435            _currentUser.Host,
 2436            entidad: nameof(Periodo),
 2437            entidadId: periodo.Id,
 2438            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 439
 2440        return (true, periodo, null);
 3441    }
 442
 443    private async Task<string> SerializarSnapshotAsync(
 444        short anio,
 445        short mes,
 446        ParametroPeriodo parametrosPeriodo,
 447        DateTime fechaInicioPeriodo,
 448        DateTime fechaFinPeriodo)
 449    {
 5450        var lotes = await _context.LotesCompensacion
 5451            .AsNoTracking()
 5452            .Where(l => l.Periodo.Year == anio && l.Periodo.Month == mes && l.Estado == "aprobado")
 5453            .Select(l => new
 5454            {
 5455                l.Id,
 5456                l.TipoCompensacionId,
 5457                l.Periodo,
 5458                l.Estado,
 5459                l.Fuente,
 5460                l.NombreArchivo,
 5461                l.HashArchivo,
 5462                l.UsuarioId,
 5463                l.AprobadoPor,
 5464                l.AprobadoEn
 5465            })
 5466            .ToListAsync();
 467
 5468        var beneficios = await _context.BeneficiosSociales
 5469            .AsNoTracking()
 5470            .Where(b => b.Estado == "activo" && b.VigenteDesde <= fechaFinPeriodo && (b.VigenteHasta == null || b.Vigent
 5471            .Select(b => new
 5472            {
 5473                b.Id,
 5474                b.PersonaId,
 5475                b.TipoBeneficioId,
 5476                b.Cantidad,
 5477                b.VigenteDesde,
 5478                b.VigenteHasta,
 5479                b.Estado,
 5480                b.ClaveEvento,
 5481                b.FechaEvento,
 5482                b.RetroactividadPendiente
 5483            })
 5484            .ToListAsync();
 485
 5486        var remuneraciones = await _context.RemuneracionesGrado
 5487            .AsNoTracking()
 5488            .Where(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaInicio
 5489            .Select(r => new
 5490            {
 5491                r.Id,
 5492                r.GradoId,
 5493                r.ItemId,
 5494                r.Monto,
 5495                r.VigenteDesde,
 5496                r.VigenteHasta
 5497            })
 5498            .ToListAsync();
 499
 5500        var franjasIrpf = await _context.FranjasIrpf
 5501            .AsNoTracking()
 5502            .Where(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaInicio
 5503            .Select(f => new
 5504            {
 5505                f.Id,
 5506                f.NumeroFranja,
 5507                f.DesdeBpc,
 5508                f.HastaBpc,
 5509                f.Tasa,
 5510                f.VigenteDesde,
 5511                f.VigenteHasta
 5512            })
 5513            .ToListAsync();
 514
 5515        var snapshot = new
 5516        {
 5517            anio,
 5518            mes,
 5519            fechaCreacion = DateTime.UtcNow,
 5520            parametros = new
 5521            {
 5522                parametrosPeriodo.Anio,
 5523                parametrosPeriodo.Mes,
 5524                parametrosPeriodo.BpcAnual,
 5525                parametrosPeriodo.TasaMontepioLeyVieja,
 5526                parametrosPeriodo.TasaMontepioLeyNueva,
 5527                parametrosPeriodo.TasaFonasa,
 5528                parametrosPeriodo.TasaFrl,
 5529                parametrosPeriodo.UmbralDeduccionBpc,
 5530                parametrosPeriodo.TasaDeduccionAlta,
 5531                parametrosPeriodo.TasaDeduccionBaja,
 5532                parametrosPeriodo.PorcentajeMinimoDeficit
 5533            },
 5534            lotesCompensacion = lotes,
 5535            beneficiosSociales = beneficios,
 5536            remuneracionesGrado = remuneraciones,
 5537            franjasIrpf = franjasIrpf
 5538        };
 539
 5540        return JsonSerializer.Serialize(snapshot, new JsonSerializerOptions
 5541        {
 5542            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 5543            WriteIndented = false
 5544        });
 5545    }
 546
 547    private static string CalcularHash(string contenidoJson)
 548    {
 5549        using var sha256 = SHA256.Create();
 5550        var bytes = Encoding.UTF8.GetBytes(contenidoJson);
 5551        return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
 5552    }
 553
 554    public async Task<List<Periodo>> ObtenerUltimosAsync(int cantidad = 6)
 555    {
 0556        return await _context.PeriodosLiquidacion
 0557            .AsNoTracking()
 0558            .OrderByDescending(p => p.Anio)
 0559            .ThenByDescending(p => p.Mes)
 0560            .Take(cantidad)
 0561            .ToListAsync();
 0562    }
 563}