< 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
79%
Covered lines: 283
Uncovered lines: 72
Coverable lines: 355
Total lines: 501
Line coverage: 79.7%
Branch coverage
47%
Covered branches: 43
Total branches: 90
Branch coverage: 47.7%
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()70.83%242492.64%
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()75%4495%
FinalizarCalculoAsync()66.66%6690.47%
CerrarAsync()75%4495%
SerializarSnapshotAsync()100%11100%
CalcularHash(...)100%11100%

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
 2521    public PeriodoService(
 2522        IPeriodosRepository periodosRepository,
 2523        ISnapshotsRepository snapshotsRepository,
 2524        ApplicationDbContext context,
 2525        IAuditoriaService auditoriaService,
 2526        ICurrentUserContext currentUser,
 2527        IInsumoEvaluador evaluador)
 28    {
 2529        _periodosRepository = periodosRepository;
 2530        _snapshotsRepository = snapshotsRepository;
 2531        _context = context;
 2532        _auditoriaService = auditoriaService;
 2533        _currentUser = currentUser;
 2534        _evaluador = evaluador;
 2535    }
 36
 37    public async Task<(bool Exito, Periodo? Periodo, string? Error)> AbrirPeriodoAsync(short anio, short mes)
 38    {
 939        if (anio < 2000 || anio > 2100)
 40        {
 041            return (false, null, "El año debe estar entre 2000 y 2100");
 42        }
 43
 944        if (mes < 1 || mes > 12)
 45        {
 046            return (false, null, "El mes debe estar entre 1 y 12");
 47        }
 48
 949        var periodoExistente = await _periodosRepository.ObtenerPorAnioMesAsync(anio, mes);
 950        if (periodoExistente is not null && periodoExistente.Estado == Periodo.EstadoAbierta && periodoExistente.Activo)
 51        {
 152            return (true, periodoExistente, null);
 53        }
 54
 855        var periodoEnCurso = await _periodosRepository.ObtenerEnCursoAsync();
 856        if (periodoEnCurso is not null)
 57        {
 158            return (false, null, $"Ya existe un período activo en estado {periodoEnCurso.Estado}");
 59        }
 60
 761        var parametrosPeriodo = await _context.ParametrosPeriodo
 762            .AsNoTracking()
 763            .FirstOrDefaultAsync(p => p.Anio == anio && p.Mes == mes);
 64
 765        if (parametrosPeriodo is null)
 66        {
 267            return (false, null, "No existen parámetros de liquidación para el período indicado");
 68        }
 69
 570        var fechaInicioPeriodo = new DateTime(anio, mes, 1, 0, 0, 0, DateTimeKind.Utc);
 571        var fechaFinPeriodo = fechaInicioPeriodo.AddMonths(1).AddTicks(-1);
 72
 573        var existeRemuneraciones = await _context.RemuneracionesGrado
 574            .AsNoTracking()
 575            .AnyAsync(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaIni
 76
 577        if (!existeRemuneraciones)
 78        {
 079            return (false, null, "No existen remuneraciones de grado vigentes para el período");
 80        }
 81
 582        var existeFranjaIrpf = await _context.FranjasIrpf
 583            .AsNoTracking()
 584            .AnyAsync(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaIni
 85
 586        if (!existeFranjaIrpf)
 87        {
 088            return (false, null, "No existen franjas IRPF vigentes para el período");
 89        }
 90
 591        var existeRelacionLaboral = await _context.RelacionesLaborales
 592            .AsNoTracking()
 593            .AnyAsync(r => r.Estado == EstadoRelacion.Activo);
 94
 595        if (!existeRelacionLaboral)
 96        {
 097            return (false, null, "No hay funcionarios activos en el padrón");
 98        }
 99
 5100        var snapshotJson = await SerializarSnapshotAsync(anio, mes, parametrosPeriodo, fechaInicioPeriodo, fechaFinPerio
 5101        var hashSnapshot = CalcularHash(snapshotJson);
 102
 5103        var snapshot = new SnapshotInsumos
 5104        {
 5105            ContenidoJson = snapshotJson,
 5106            HashIntegridad = hashSnapshot
 5107        };
 108
 5109        var snapshotCreado = await _snapshotsRepository.CrearAsync(snapshot);
 110
 5111        var periodo = new Periodo
 5112        {
 5113            Anio = anio,
 5114            Mes = mes,
 5115            Estado = Periodo.EstadoAbierta,
 5116            SnapshotId = snapshotCreado.Id,
 5117            FechaApertura = DateTime.UtcNow,
 5118            UsuarioApertura = _currentUser.UserId ?? 0,
 5119            HashSnapshot = hashSnapshot,
 5120            Activo = true
 5121        };
 122
 5123        var periodoCreado = await _periodosRepository.CrearAsync(periodo);
 124
 5125        await _auditoriaService.LogAuditoriaAsync(
 5126            _currentUser.UserId ?? 0,
 5127            AccionEnum.AbrirPeriodoLiquidacion,
 5128            ContextoEnum.Liquidaciones,
 5129            _currentUser.Host,
 5130            entidad: nameof(Periodo),
 5131            entidadId: periodoCreado.Id,
 5132            detalle: new
 5133            {
 5134                anio,
 5135                mes,
 5136                snapshotId = snapshotCreado.Id,
 5137                hashSnapshot
 5138            });
 139
 5140        return (true, periodoCreado, null);
 9141    }
 142
 143    public Task<Periodo?> ObtenerActivoAsync()
 144    {
 0145        return _periodosRepository.ObtenerActivoAsync();
 146    }
 147
 148    public Task<Periodo?> ObtenerEnCursoAsync()
 149    {
 0150        return _periodosRepository.ObtenerEnCursoAsync();
 151    }
 152
 153    public Task<Periodo?> ObtenerPorIdAsync(long id)
 154    {
 0155        return _periodosRepository.ObtenerPorIdAsync(id);
 156    }
 157
 158    public async Task<PrecondicionesDto?> ObtenerPrecondicionesAsync(long periodoId)
 159    {
 0160        var validacion = await ValidarInsumosAsync(periodoId);
 0161        if (validacion is null) return null;
 162
 0163        bool Ok(string tipo) => validacion.ItemsIncompletos.All(i => i.Tipo != tipo);
 164        string Detail(string tipo, string okMsg, string failMsg) =>
 0165            Ok(tipo) ? okMsg : (validacion.ItemsIncompletos.FirstOrDefault(i => i.Tipo == tipo)?.Descripcion ?? failMsg)
 166
 0167        var lotesOk = Ok("lote_faltante");
 0168        var novedadesOk = Ok("novedades_pendientes");
 0169        var beneficiosOk = Ok("beneficios_faltantes");
 0170        var remuneracionesOk = Ok("tabla_referencia") ||
 0171            !validacion.ItemsIncompletos.Any(i => i.Clave == "SinRemuneraciones");
 0172        var franjasOk = !validacion.ItemsIncompletos.Any(i => i.Clave == "SinFranjasIrpf");
 0173        var funcionariosOk = !validacion.ItemsIncompletos.Any(i => i.Clave == "SinFuncionarios");
 174
 0175        return new PrecondicionesDto
 0176        {
 0177            LotesAprobados = new PrecondicionItemDto
 0178            {
 0179                Ok = lotesOk,
 0180                Detail = lotesOk ? "Lotes aprobados" : "Sin lotes aprobados para el período"
 0181            },
 0182            NovedadesSinConfirmar = new PrecondicionItemDto
 0183            {
 0184                Ok = novedadesOk,
 0185                Detail = novedadesOk ? "Sin novedades pendientes" :
 0186                    validacion.ItemsIncompletos.FirstOrDefault(i => i.Tipo == "novedades_pendientes")?.Descripcion ?? "N
 0187            },
 0188            BeneficiosVigentes = new PrecondicionItemDto
 0189            {
 0190                Ok = beneficiosOk,
 0191                Detail = beneficiosOk ? "Beneficios sociales activos" : "Sin beneficios sociales activos vigentes"
 0192            },
 0193            RemuneracionesVigentes = new PrecondicionItemDto
 0194            {
 0195                Ok = remuneracionesOk,
 0196                Detail = remuneracionesOk ? "Tablas de remuneración vigentes" : "Sin tablas de remuneración para el perí
 0197            },
 0198            FranjasIrpf = new PrecondicionItemDto
 0199            {
 0200                Ok = franjasOk,
 0201                Detail = franjasOk ? "Franjas IRPF vigentes" : "Sin franjas IRPF para el período"
 0202            },
 0203            FuncionariosActivos = new PrecondicionItemDto
 0204            {
 0205                Ok = funcionariosOk,
 0206                Detail = funcionariosOk ? "Funcionarios activos en el padrón" : "No hay funcionarios activos"
 0207            }
 0208        };
 0209    }
 210
 211    public async Task<ValidacionInsumosDto?> ValidarInsumosAsync(long periodoId)
 212    {
 0213        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 0214        if (periodo is null) return null;
 0215        return await _evaluador.EvaluarAsync(periodo);
 0216    }
 217
 218    public async Task<(bool Exito, Periodo? Periodo, ValidacionInsumosDto? Validacion, string? Error)> CerrarInsumosAsyn
 219        long periodoId, List<string>? confirmaciones = null)
 220    {
 8221        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 8222        if (periodo is null)
 0223            return (false, null, null, "Período no encontrado");
 224
 8225        if (periodo.Estado != Periodo.EstadoAbierta)
 1226            return (false, null, null, $"Solo se puede cerrar insumos desde estado ABIERTA. Estado actual: {periodo.Esta
 227
 7228        var validacion = await _evaluador.EvaluarAsync(periodo);
 229
 7230        if (!validacion.TodosCompletos)
 231        {
 5232            if (confirmaciones is null || confirmaciones.Count == 0)
 4233                return (false, null, validacion, "Hay insumos incompletos. Revisá el detalle y confirmá cada omisión par
 234
 2235            var bloqueantes = validacion.ItemsIncompletos.Where(i => !i.EsConfirmable).ToList();
 1236            if (bloqueantes.Count > 0)
 0237                return (false, null, validacion, "Existen problemas bloqueantes que no pueden omitirse.");
 238
 1239            var clavesSinConfirmar = validacion.ItemsIncompletos
 1240                .Where(i => !confirmaciones.Contains(i.Clave))
 1241                .ToList();
 1242            if (clavesSinConfirmar.Count > 0)
 0243                return (false, null, new ValidacionInsumosDto
 0244                {
 0245                    TodosCompletos = false,
 0246                    PuedeConfirmar = true,
 0247                    ItemsIncompletos = clavesSinConfirmar
 0248                }, "Faltan confirmar algunos items incompletos.");
 249        }
 250
 3251        var usuarioId = _currentUser.UserId ?? 0;
 3252        periodo.Estado = Periodo.EstadoListaCalculo;
 3253        periodo.FechaCierreInsumos = DateTime.UtcNow;
 3254        periodo.UsuarioCierreInsumosId = usuarioId;
 3255        await _periodosRepository.ActualizarAsync(periodo);
 256
 3257        var omisiones = confirmaciones ?? new List<string>();
 3258        await _auditoriaService.LogAuditoriaAsync(
 3259            usuarioId,
 3260            AccionEnum.CerrarInsumosPeriodo,
 3261            ContextoEnum.Liquidaciones,
 3262            _currentUser.Host,
 3263            entidad: nameof(Periodo),
 3264            entidadId: periodo.Id,
 3265            detalle: new
 3266            {
 3267                anio = periodo.Anio,
 3268                mes = periodo.Mes,
 3269                omisionesConfirmadas = omisiones.Select(c =>
 0270                    validacion.ItemsIncompletos.FirstOrDefault(i => i.Clave == c)?.Descripcion ?? c)
 3271            });
 272
 3273        return (true, periodo, null, null);
 8274    }
 275
 276    public async Task<(bool Exito, Periodo? Periodo, string? Error)> ReabrirInsumosAsync(long periodoId)
 277    {
 3278        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3279        if (periodo is null)
 0280            return (false, null, "Período no encontrado");
 281
 3282        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1283            return (false, null, $"Solo se puede reabrir desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 284
 2285        var usuarioId = _currentUser.UserId ?? 0;
 2286        periodo.Estado = Periodo.EstadoAbierta;
 2287        periodo.FechaCierreInsumos = null;
 2288        periodo.UsuarioCierreInsumosId = null;
 2289        await _periodosRepository.ActualizarAsync(periodo);
 290
 2291        await _auditoriaService.LogAuditoriaAsync(
 2292            usuarioId,
 2293            AccionEnum.ReabrirInsumosPeriodo,
 2294            ContextoEnum.Liquidaciones,
 2295            _currentUser.Host,
 2296            entidad: nameof(Periodo),
 2297            entidadId: periodo.Id,
 2298            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 299
 2300        return (true, periodo, null);
 3301    }
 302
 303    public async Task<(bool Exito, Periodo? Periodo, string? Error)> IniciarCalculoAsync(long periodoId)
 304    {
 2305        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2306        if (periodo is null)
 0307            return (false, null, "Período no encontrado");
 308
 2309        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1310            return (false, null, $"Solo se puede calcular desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 311
 1312        var usuarioId = _currentUser.UserId ?? 0;
 1313        periodo.Estado = Periodo.EstadoEnCalculo;
 1314        periodo.FechaCalculo = DateTime.UtcNow;
 1315        periodo.UsuarioCalculoId = usuarioId;
 1316        await _periodosRepository.ActualizarAsync(periodo);
 317
 1318        await _auditoriaService.LogAuditoriaAsync(
 1319            usuarioId,
 1320            AccionEnum.IniciarCalculoPeriodo,
 1321            ContextoEnum.Liquidaciones,
 1322            _currentUser.Host,
 1323            entidad: nameof(Periodo),
 1324            entidadId: periodo.Id,
 1325            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 326
 1327        return (true, periodo, null);
 2328    }
 329
 330    public async Task<(bool Exito, Periodo? Periodo, string? Error)> FinalizarCalculoAsync(long periodoId, bool exitoso,
 331    {
 2332        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2333        if (periodo is null)
 0334            return (false, null, "Período no encontrado");
 335
 2336        if (periodo.Estado != Periodo.EstadoEnCalculo)
 0337            return (false, null, $"Solo se puede finalizar el cálculo desde estado EN_CALCULO. Estado actual: {periodo.E
 338
 2339        var usuarioId = _currentUser.UserId ?? 0;
 2340        if (exitoso)
 341        {
 1342            periodo.Estado = Periodo.EstadoCalculada;
 1343            periodo.FechaCalculo = DateTime.UtcNow;
 344        }
 345        else
 346        {
 1347            periodo.Estado = Periodo.EstadoListaCalculo;
 348        }
 349
 2350        await _periodosRepository.ActualizarAsync(periodo);
 351
 2352        await _auditoriaService.LogAuditoriaAsync(
 2353            usuarioId,
 2354            AccionEnum.IniciarCalculoPeriodo,
 2355            ContextoEnum.Liquidaciones,
 2356            _currentUser.Host,
 2357            entidad: nameof(Periodo),
 2358            entidadId: periodo.Id,
 2359            detalle: new { exitoso, mensajeError });
 360
 2361        return (true, periodo, null);
 2362    }
 363
 364    public async Task<(bool Exito, Periodo? Periodo, string? Error)> CerrarAsync(long periodoId)
 365    {
 3366        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3367        if (periodo is null)
 0368            return (false, null, "Período no encontrado");
 369
 3370        if (periodo.Estado != Periodo.EstadoCalculada)
 1371            return (false, null, $"Solo se puede cerrar desde estado CALCULADA. Estado actual: {periodo.Estado}");
 372
 2373        var usuarioId = _currentUser.UserId ?? 0;
 2374        periodo.Estado = Periodo.EstadoCerrada;
 2375        periodo.FechaCierre = DateTime.UtcNow;
 2376        periodo.UsuarioCierreId = usuarioId;
 2377        await _periodosRepository.ActualizarAsync(periodo);
 378
 2379        await _auditoriaService.LogAuditoriaAsync(
 2380            usuarioId,
 2381            AccionEnum.CerrarPeriodo,
 2382            ContextoEnum.Liquidaciones,
 2383            _currentUser.Host,
 2384            entidad: nameof(Periodo),
 2385            entidadId: periodo.Id,
 2386            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 387
 2388        return (true, periodo, null);
 3389    }
 390
 391    private async Task<string> SerializarSnapshotAsync(
 392        short anio,
 393        short mes,
 394        ParametroPeriodo parametrosPeriodo,
 395        DateTime fechaInicioPeriodo,
 396        DateTime fechaFinPeriodo)
 397    {
 5398        var lotes = await _context.LotesCompensacion
 5399            .AsNoTracking()
 5400            .Where(l => l.Periodo.Year == anio && l.Periodo.Month == mes && l.Estado == "aprobado")
 5401            .Select(l => new
 5402            {
 5403                l.Id,
 5404                l.TipoCompensacionId,
 5405                l.Periodo,
 5406                l.Estado,
 5407                l.Fuente,
 5408                l.NombreArchivo,
 5409                l.HashArchivo,
 5410                l.UsuarioId,
 5411                l.AprobadoPor,
 5412                l.AprobadoEn
 5413            })
 5414            .ToListAsync();
 415
 5416        var beneficios = await _context.BeneficiosSociales
 5417            .AsNoTracking()
 5418            .Where(b => b.Estado == "activo" && b.VigenteDesde <= fechaFinPeriodo && (b.VigenteHasta == null || b.Vigent
 5419            .Select(b => new
 5420            {
 5421                b.Id,
 5422                b.PersonaId,
 5423                b.TipoBeneficioId,
 5424                b.Cantidad,
 5425                b.VigenteDesde,
 5426                b.VigenteHasta,
 5427                b.Estado,
 5428                b.ClaveEvento,
 5429                b.FechaEvento,
 5430                b.RetroactividadPendiente
 5431            })
 5432            .ToListAsync();
 433
 5434        var remuneraciones = await _context.RemuneracionesGrado
 5435            .AsNoTracking()
 5436            .Where(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaInicio
 5437            .Select(r => new
 5438            {
 5439                r.Id,
 5440                r.GradoId,
 5441                r.ItemId,
 5442                r.Monto,
 5443                r.VigenteDesde,
 5444                r.VigenteHasta
 5445            })
 5446            .ToListAsync();
 447
 5448        var franjasIrpf = await _context.FranjasIrpf
 5449            .AsNoTracking()
 5450            .Where(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaInicio
 5451            .Select(f => new
 5452            {
 5453                f.Id,
 5454                f.NumeroFranja,
 5455                f.DesdeBpc,
 5456                f.HastaBpc,
 5457                f.Tasa,
 5458                f.VigenteDesde,
 5459                f.VigenteHasta
 5460            })
 5461            .ToListAsync();
 462
 5463        var snapshot = new
 5464        {
 5465            anio,
 5466            mes,
 5467            fechaCreacion = DateTime.UtcNow,
 5468            parametros = new
 5469            {
 5470                parametrosPeriodo.Anio,
 5471                parametrosPeriodo.Mes,
 5472                parametrosPeriodo.BpcAnual,
 5473                parametrosPeriodo.TasaMontepioLeyVieja,
 5474                parametrosPeriodo.TasaMontepioLeyNueva,
 5475                parametrosPeriodo.TasaFonasa,
 5476                parametrosPeriodo.TasaFrl,
 5477                parametrosPeriodo.UmbralDeduccionBpc,
 5478                parametrosPeriodo.TasaDeduccionAlta,
 5479                parametrosPeriodo.TasaDeduccionBaja,
 5480                parametrosPeriodo.PorcentajeMinimoDeficit
 5481            },
 5482            lotesCompensacion = lotes,
 5483            beneficiosSociales = beneficios,
 5484            remuneracionesGrado = remuneraciones,
 5485            franjasIrpf = franjasIrpf
 5486        };
 487
 5488        return JsonSerializer.Serialize(snapshot, new JsonSerializerOptions
 5489        {
 5490            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 5491            WriteIndented = false
 5492        });
 5493    }
 494
 495    private static string CalcularHash(string contenidoJson)
 496    {
 5497        using var sha256 = SHA256.Create();
 5498        var bytes = Encoding.UTF8.GetBytes(contenidoJson);
 5499        return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
 5500    }
 501}