< 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: 121
Coverable lines: 409
Total lines: 581
Line coverage: 70.4%
Branch coverage
42%
Covered branches: 45
Total branches: 106
Branch coverage: 42.4%
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()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);
 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.Now;
 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.Now;
 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.Now;
 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        periodo.FechaCierreInsumos = null;
 0406        periodo.UsuarioCierreInsumosId = null;
 0407        await _periodosRepository.ActualizarAsync(periodo);
 408
 0409        await _auditoriaService.LogAuditoriaAsync(
 0410            usuarioId,
 0411            AccionEnum.AbrirPeriodoLiquidacion,
 0412            ContextoEnum.Liquidaciones,
 0413            _currentUser.Host,
 0414            entidad: nameof(Periodo),
 0415            entidadId: periodo.Id,
 0416            detalle: new { anio = periodo.Anio, mes = periodo.Mes, motivo = "reapertura desde CALCULADA para corrección"
 417
 0418        return (true, periodo, null);
 0419    }
 420
 421    public async Task<(bool Exito, Periodo? Periodo, string? Error)> CerrarAsync(long periodoId)
 422    {
 3423        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3424        if (periodo is null)
 0425            return (false, null, "Período no encontrado");
 426
 3427        if (periodo.Estado != Periodo.EstadoCalculada)
 1428            return (false, null, $"Solo se puede cerrar desde estado CALCULADA. Estado actual: {periodo.Estado}");
 429
 2430        var usuarioId = _currentUser.UserId ?? 0;
 2431        periodo.Estado = Periodo.EstadoCerrada;
 2432        periodo.FechaCierre = DateTime.Now;
 2433        periodo.UsuarioCierreId = usuarioId;
 2434        await _periodosRepository.ActualizarAsync(periodo);
 435
 2436        await _auditoriaService.LogAuditoriaAsync(
 2437            usuarioId,
 2438            AccionEnum.CerrarPeriodo,
 2439            ContextoEnum.Liquidaciones,
 2440            _currentUser.Host,
 2441            entidad: nameof(Periodo),
 2442            entidadId: periodo.Id,
 2443            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 444
 2445        return (true, periodo, null);
 3446    }
 447
 448    private async Task<string> SerializarSnapshotAsync(
 449        short anio,
 450        short mes,
 451        ParametroPeriodo parametrosPeriodo,
 452        DateTime fechaInicioPeriodo,
 453        DateTime fechaFinPeriodo)
 454    {
 5455        var lotes = await _context.LotesCompensacion
 5456            .AsNoTracking()
 5457            .Where(l => l.Periodo.Anio == anio && l.Periodo.Mes == mes && l.Estado == "aprobado")
 5458            .Select(l => new
 5459            {
 5460                l.Id,
 5461                l.TipoCompensacionId,
 5462                l.PeriodoId,
 5463                l.Estado,
 5464                l.Fuente,
 5465                l.NombreArchivo,
 5466                l.HashArchivo,
 5467                l.UsuarioId,
 5468                l.AprobadoPor,
 5469                l.AprobadoEn
 5470            })
 5471            .ToListAsync();
 472
 5473        var beneficios = await _context.BeneficiosSociales
 5474            .AsNoTracking()
 5475            .Where(b => b.Estado == "activo" && b.VigenteDesde <= fechaFinPeriodo && (b.VigenteHasta == null || b.Vigent
 5476            .Select(b => new
 5477            {
 5478                b.Id,
 5479                b.PersonaId,
 5480                b.TipoBeneficioId,
 5481                b.Cantidad,
 5482                b.VigenteDesde,
 5483                b.VigenteHasta,
 5484                b.Estado,
 5485                b.ClaveEvento,
 5486                b.FechaEvento,
 5487                b.RetroactividadPendiente
 5488            })
 5489            .ToListAsync();
 490
 5491        var remuneraciones = await _context.RemuneracionesGrado
 5492            .AsNoTracking()
 5493            .Where(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaInicio
 5494            .Select(r => new
 5495            {
 5496                r.Id,
 5497                r.GradoId,
 5498                r.ItemId,
 5499                r.Monto,
 5500                r.VigenteDesde,
 5501                r.VigenteHasta
 5502            })
 5503            .ToListAsync();
 504
 5505        var franjasIrpf = await _context.FranjasIrpf
 5506            .AsNoTracking()
 5507            .Where(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaInicio
 5508            .Select(f => new
 5509            {
 5510                f.Id,
 5511                f.NumeroFranja,
 5512                f.DesdeBpc,
 5513                f.HastaBpc,
 5514                f.Tasa,
 5515                f.VigenteDesde,
 5516                f.VigenteHasta
 5517            })
 5518            .ToListAsync();
 519
 5520        var snapshot = new
 5521        {
 5522            anio,
 5523            mes,
 5524            fechaCreacion = DateTime.Now,
 5525            parametros = new
 5526            {
 5527                parametrosPeriodo.Anio,
 5528                parametrosPeriodo.Mes,
 5529                parametrosPeriodo.BpcAnual,
 5530                parametrosPeriodo.TasaMontepioLeyVieja,
 5531                parametrosPeriodo.TasaMontepioLeyNueva,
 5532                parametrosPeriodo.TasaFonasa,
 5533                parametrosPeriodo.TasaFrl,
 5534                parametrosPeriodo.UmbralDeduccionBpc,
 5535                parametrosPeriodo.TasaDeduccionAlta,
 5536                parametrosPeriodo.TasaDeduccionBaja,
 5537                parametrosPeriodo.PorcentajeMinimoDeficit
 5538            },
 5539            lotesCompensacion = lotes,
 5540            beneficiosSociales = beneficios,
 5541            remuneracionesGrado = remuneraciones,
 5542            franjasIrpf = franjasIrpf
 5543        };
 544
 5545        return JsonSerializer.Serialize(snapshot, new JsonSerializerOptions
 5546        {
 5547            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 5548            WriteIndented = false
 5549        });
 5550    }
 551
 552    private static string CalcularHash(string contenidoJson)
 553    {
 5554        using var sha256 = SHA256.Create();
 5555        var bytes = Encoding.UTF8.GetBytes(contenidoJson);
 5556        return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
 5557    }
 558
 559    public async Task<List<Periodo>> ObtenerUltimosAsync(int cantidad = 6)
 560    {
 0561        var periodos = await _context.PeriodosLiquidacion
 0562            .AsNoTracking()
 0563            .Include(p => p.UsuarioCierreInsumos)
 0564            .Include(p => p.UsuarioCalculo)
 0565            .Include(p => p.UsuarioCierre)
 0566            .OrderByDescending(p => p.Anio)
 0567            .ThenByDescending(p => p.Mes)
 0568            .Take(cantidad)
 0569            .ToListAsync();
 570
 0571        var userIds = periodos.Select(p => p.UsuarioApertura).Distinct().ToList();
 0572        var usernames = await _context.Usuarios
 0573            .Where(u => userIds.Contains(u.Id))
 0574            .ToDictionaryAsync(u => u.Id, u => u.Username);
 575
 0576        foreach (var p in periodos)
 0577            p.UsuarioAperturaUsername = usernames.GetValueOrDefault(p.UsuarioApertura);
 578
 0579        return periodos;
 0580    }
 581}