< 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: 288
Uncovered lines: 109
Coverable lines: 397
Total lines: 566
Line coverage: 72.5%
Branch coverage
43%
Covered branches: 45
Total branches: 104
Branch coverage: 43.2%
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()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()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."
 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        if (confirmaciones?.Contains("DescuentosBorrador") == true)
 0267            await _descuentoPersonalRepo.ConfirmarBorradoresAsync(periodoId);
 268
 3269        var usuarioId = _currentUser.UserId ?? 0;
 3270        periodo.Estado = Periodo.EstadoListaCalculo;
 3271        periodo.FechaCierreInsumos = DateTime.UtcNow;
 3272        periodo.UsuarioCierreInsumosId = usuarioId;
 3273        await _periodosRepository.ActualizarAsync(periodo);
 274
 3275        var omisiones = confirmaciones ?? new List<string>();
 3276        await _auditoriaService.LogAuditoriaAsync(
 3277            usuarioId,
 3278            AccionEnum.CerrarInsumosPeriodo,
 3279            ContextoEnum.Liquidaciones,
 3280            _currentUser.Host,
 3281            entidad: nameof(Periodo),
 3282            entidadId: periodo.Id,
 3283            detalle: new
 3284            {
 3285                anio = periodo.Anio,
 3286                mes = periodo.Mes,
 3287                omisionesConfirmadas = omisiones.Select(c =>
 0288                    validacion.ItemsIncompletos.FirstOrDefault(i => i.Clave == c)?.Descripcion ?? c)
 3289            });
 290
 3291        return (true, periodo, null, null);
 8292    }
 293
 294    public async Task<(bool Exito, Periodo? Periodo, string? Error)> ReabrirInsumosAsync(long periodoId)
 295    {
 3296        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3297        if (periodo is null)
 0298            return (false, null, "Período no encontrado");
 299
 3300        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1301            return (false, null, $"Solo se puede reabrir desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 302
 2303        var usuarioId = _currentUser.UserId ?? 0;
 2304        periodo.Estado = Periodo.EstadoAbierta;
 2305        periodo.FechaCierreInsumos = null;
 2306        periodo.UsuarioCierreInsumosId = null;
 2307        await _periodosRepository.ActualizarAsync(periodo);
 308
 2309        await _auditoriaService.LogAuditoriaAsync(
 2310            usuarioId,
 2311            AccionEnum.ReabrirInsumosPeriodo,
 2312            ContextoEnum.Liquidaciones,
 2313            _currentUser.Host,
 2314            entidad: nameof(Periodo),
 2315            entidadId: periodo.Id,
 2316            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 317
 2318        return (true, periodo, null);
 3319    }
 320
 321    public async Task<(bool Exito, Periodo? Periodo, string? Error)> IniciarCalculoAsync(long periodoId)
 322    {
 2323        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2324        if (periodo is null)
 0325            return (false, null, "Período no encontrado");
 326
 2327        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1328            return (false, null, $"Solo se puede calcular desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 329
 1330        if (await _descuentoPersonalRepo.TieneBorradoresAsync(periodoId))
 0331            return (false, null, "Existen descuentos personales sin confirmar. Confirme todos los borradores antes de in
 332
 1333        var usuarioId = _currentUser.UserId ?? 0;
 1334        periodo.Estado = Periodo.EstadoEnCalculo;
 1335        periodo.FechaCalculo = DateTime.UtcNow;
 1336        periodo.UsuarioCalculoId = usuarioId;
 1337        await _periodosRepository.ActualizarAsync(periodo);
 338
 1339        await _auditoriaService.LogAuditoriaAsync(
 1340            usuarioId,
 1341            AccionEnum.IniciarCalculoPeriodo,
 1342            ContextoEnum.Liquidaciones,
 1343            _currentUser.Host,
 1344            entidad: nameof(Periodo),
 1345            entidadId: periodo.Id,
 1346            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 347
 1348        return (true, periodo, null);
 2349    }
 350
 351    public async Task<(bool Exito, Periodo? Periodo, string? Error)> FinalizarCalculoAsync(long periodoId, bool exitoso,
 352    {
 2353        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2354        if (periodo is null)
 0355            return (false, null, "Período no encontrado");
 356
 2357        if (periodo.Estado != Periodo.EstadoEnCalculo)
 0358            return (false, null, $"Solo se puede finalizar el cálculo desde estado EN_CALCULO. Estado actual: {periodo.E
 359
 2360        var usuarioId = _currentUser.UserId ?? 0;
 2361        if (exitoso)
 362        {
 1363            periodo.Estado = Periodo.EstadoCalculada;
 1364            periodo.FechaCalculo = DateTime.UtcNow;
 365        }
 366        else
 367        {
 1368            periodo.Estado = Periodo.EstadoListaCalculo;
 369        }
 370
 2371        await _periodosRepository.ActualizarAsync(periodo);
 372
 2373        await _auditoriaService.LogAuditoriaAsync(
 2374            usuarioId,
 2375            AccionEnum.IniciarCalculoPeriodo,
 2376            ContextoEnum.Liquidaciones,
 2377            _currentUser.Host,
 2378            entidad: nameof(Periodo),
 2379            entidadId: periodo.Id,
 2380            detalle: new { exitoso, mensajeError });
 381
 2382        return (true, periodo, null);
 2383    }
 384
 385    /// <summary>
 386    /// Reabre un período CALCULADA → ABIERTA para permitir correcciones y un nuevo cálculo.
 387    /// Los ítems de la versión anterior quedan en la BD como historial de auditoría.
 388    /// El próximo cálculo genera version = max_version_anterior + 1.
 389    /// </summary>
 390    public async Task<(bool Exito, Periodo? Periodo, string? Error)> ReabrirDesdeCalculadaAsync(long periodoId)
 391    {
 0392        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 0393        if (periodo is null)
 0394            return (false, null, "Período no encontrado");
 395
 0396        if (periodo.Estado != Periodo.EstadoCalculada)
 0397            return (false, null,
 0398                $"Solo se puede reabrir desde estado CALCULADA. Estado actual: {periodo.Estado}");
 399
 0400        var usuarioId = _currentUser.UserId ?? 0;
 0401        periodo.Estado = Periodo.EstadoAbierta;
 402        // Limpiamos la fecha de cálculo para que quede como un período nuevo en proceso
 0403        periodo.FechaCalculo = null;
 0404        periodo.UsuarioCalculoId = null;
 0405        await _periodosRepository.ActualizarAsync(periodo);
 406
 0407        await _auditoriaService.LogAuditoriaAsync(
 0408            usuarioId,
 0409            AccionEnum.AbrirPeriodoLiquidacion,
 0410            ContextoEnum.Liquidaciones,
 0411            _currentUser.Host,
 0412            entidad: nameof(Periodo),
 0413            entidadId: periodo.Id,
 0414            detalle: new { anio = periodo.Anio, mes = periodo.Mes, motivo = "reapertura desde CALCULADA para corrección"
 415
 0416        return (true, periodo, null);
 0417    }
 418
 419    public async Task<(bool Exito, Periodo? Periodo, string? Error)> CerrarAsync(long periodoId)
 420    {
 3421        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3422        if (periodo is null)
 0423            return (false, null, "Período no encontrado");
 424
 3425        if (periodo.Estado != Periodo.EstadoCalculada)
 1426            return (false, null, $"Solo se puede cerrar desde estado CALCULADA. Estado actual: {periodo.Estado}");
 427
 2428        var usuarioId = _currentUser.UserId ?? 0;
 2429        periodo.Estado = Periodo.EstadoCerrada;
 2430        periodo.FechaCierre = DateTime.UtcNow;
 2431        periodo.UsuarioCierreId = usuarioId;
 2432        await _periodosRepository.ActualizarAsync(periodo);
 433
 2434        await _auditoriaService.LogAuditoriaAsync(
 2435            usuarioId,
 2436            AccionEnum.CerrarPeriodo,
 2437            ContextoEnum.Liquidaciones,
 2438            _currentUser.Host,
 2439            entidad: nameof(Periodo),
 2440            entidadId: periodo.Id,
 2441            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 442
 2443        return (true, periodo, null);
 3444    }
 445
 446    private async Task<string> SerializarSnapshotAsync(
 447        short anio,
 448        short mes,
 449        ParametroPeriodo parametrosPeriodo,
 450        DateTime fechaInicioPeriodo,
 451        DateTime fechaFinPeriodo)
 452    {
 5453        var lotes = await _context.LotesCompensacion
 5454            .AsNoTracking()
 5455            .Where(l => l.Periodo.Anio == anio && l.Periodo.Mes == mes && l.Estado == "aprobado")
 5456            .Select(l => new
 5457            {
 5458                l.Id,
 5459                l.TipoCompensacionId,
 5460                l.PeriodoId,
 5461                l.Estado,
 5462                l.Fuente,
 5463                l.NombreArchivo,
 5464                l.HashArchivo,
 5465                l.UsuarioId,
 5466                l.AprobadoPor,
 5467                l.AprobadoEn
 5468            })
 5469            .ToListAsync();
 470
 5471        var beneficios = await _context.BeneficiosSociales
 5472            .AsNoTracking()
 5473            .Where(b => b.Estado == "activo" && b.VigenteDesde <= fechaFinPeriodo && (b.VigenteHasta == null || b.Vigent
 5474            .Select(b => new
 5475            {
 5476                b.Id,
 5477                b.PersonaId,
 5478                b.TipoBeneficioId,
 5479                b.Cantidad,
 5480                b.VigenteDesde,
 5481                b.VigenteHasta,
 5482                b.Estado,
 5483                b.ClaveEvento,
 5484                b.FechaEvento,
 5485                b.RetroactividadPendiente
 5486            })
 5487            .ToListAsync();
 488
 5489        var remuneraciones = await _context.RemuneracionesGrado
 5490            .AsNoTracking()
 5491            .Where(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaInicio
 5492            .Select(r => new
 5493            {
 5494                r.Id,
 5495                r.GradoId,
 5496                r.ItemId,
 5497                r.Monto,
 5498                r.VigenteDesde,
 5499                r.VigenteHasta
 5500            })
 5501            .ToListAsync();
 502
 5503        var franjasIrpf = await _context.FranjasIrpf
 5504            .AsNoTracking()
 5505            .Where(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaInicio
 5506            .Select(f => new
 5507            {
 5508                f.Id,
 5509                f.NumeroFranja,
 5510                f.DesdeBpc,
 5511                f.HastaBpc,
 5512                f.Tasa,
 5513                f.VigenteDesde,
 5514                f.VigenteHasta
 5515            })
 5516            .ToListAsync();
 517
 5518        var snapshot = new
 5519        {
 5520            anio,
 5521            mes,
 5522            fechaCreacion = DateTime.UtcNow,
 5523            parametros = new
 5524            {
 5525                parametrosPeriodo.Anio,
 5526                parametrosPeriodo.Mes,
 5527                parametrosPeriodo.BpcAnual,
 5528                parametrosPeriodo.TasaMontepioLeyVieja,
 5529                parametrosPeriodo.TasaMontepioLeyNueva,
 5530                parametrosPeriodo.TasaFonasa,
 5531                parametrosPeriodo.TasaFrl,
 5532                parametrosPeriodo.UmbralDeduccionBpc,
 5533                parametrosPeriodo.TasaDeduccionAlta,
 5534                parametrosPeriodo.TasaDeduccionBaja,
 5535                parametrosPeriodo.PorcentajeMinimoDeficit
 5536            },
 5537            lotesCompensacion = lotes,
 5538            beneficiosSociales = beneficios,
 5539            remuneracionesGrado = remuneraciones,
 5540            franjasIrpf = franjasIrpf
 5541        };
 542
 5543        return JsonSerializer.Serialize(snapshot, new JsonSerializerOptions
 5544        {
 5545            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 5546            WriteIndented = false
 5547        });
 5548    }
 549
 550    private static string CalcularHash(string contenidoJson)
 551    {
 5552        using var sha256 = SHA256.Create();
 5553        var bytes = Encoding.UTF8.GetBytes(contenidoJson);
 5554        return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
 5555    }
 556
 557    public async Task<List<Periodo>> ObtenerUltimosAsync(int cantidad = 6)
 558    {
 0559        return await _context.PeriodosLiquidacion
 0560            .AsNoTracking()
 0561            .OrderByDescending(p => p.Anio)
 0562            .ThenByDescending(p => p.Mes)
 0563            .Take(cantidad)
 0564            .ToListAsync();
 0565    }
 566}