< 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: 286
Uncovered lines: 73
Coverable lines: 359
Total lines: 507
Line coverage: 79.6%
Branch coverage
47%
Covered branches: 44
Total branches: 92
Branch coverage: 47.8%
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()66.66%6690.9%
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    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 && periodoExistente.Estado == Periodo.EstadoAbierta && periodoExistente.Activo)
 54        {
 155            return (true, periodoExistente, null);
 56        }
 57
 858        var periodoEnCurso = await _periodosRepository.ObtenerEnCursoAsync();
 859        if (periodoEnCurso is not null)
 60        {
 161            return (false, null, $"Ya existe un período activo en estado {periodoEnCurso.Estado}");
 62        }
 63
 764        var parametrosPeriodo = await _context.ParametrosPeriodo
 765            .AsNoTracking()
 766            .FirstOrDefaultAsync(p => p.Anio == anio && p.Mes == mes);
 67
 768        if (parametrosPeriodo is null)
 69        {
 270            return (false, null, "No existen parámetros de liquidación para el período indicado");
 71        }
 72
 573        var fechaInicioPeriodo = new DateTime(anio, mes, 1, 0, 0, 0, DateTimeKind.Utc);
 574        var fechaFinPeriodo = fechaInicioPeriodo.AddMonths(1).AddTicks(-1);
 75
 576        var existeRemuneraciones = await _context.RemuneracionesGrado
 577            .AsNoTracking()
 578            .AnyAsync(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaIni
 79
 580        if (!existeRemuneraciones)
 81        {
 082            return (false, null, "No existen remuneraciones de grado vigentes para el período");
 83        }
 84
 585        var existeFranjaIrpf = await _context.FranjasIrpf
 586            .AsNoTracking()
 587            .AnyAsync(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaIni
 88
 589        if (!existeFranjaIrpf)
 90        {
 091            return (false, null, "No existen franjas IRPF vigentes para el período");
 92        }
 93
 594        var existeRelacionLaboral = await _context.RelacionesLaborales
 595            .AsNoTracking()
 596            .AnyAsync(r => r.Estado == EstadoRelacion.Activo);
 97
 598        if (!existeRelacionLaboral)
 99        {
 0100            return (false, null, "No hay funcionarios activos en el padrón");
 101        }
 102
 5103        var snapshotJson = await SerializarSnapshotAsync(anio, mes, parametrosPeriodo, fechaInicioPeriodo, fechaFinPerio
 5104        var hashSnapshot = CalcularHash(snapshotJson);
 105
 5106        var snapshot = new SnapshotInsumos
 5107        {
 5108            ContenidoJson = snapshotJson,
 5109            HashIntegridad = hashSnapshot
 5110        };
 111
 5112        var snapshotCreado = await _snapshotsRepository.CrearAsync(snapshot);
 113
 5114        var periodo = new Periodo
 5115        {
 5116            Anio = anio,
 5117            Mes = mes,
 5118            Estado = Periodo.EstadoAbierta,
 5119            SnapshotId = snapshotCreado.Id,
 5120            FechaApertura = DateTime.UtcNow,
 5121            UsuarioApertura = _currentUser.UserId ?? 0,
 5122            HashSnapshot = hashSnapshot,
 5123            Activo = true
 5124        };
 125
 5126        var periodoCreado = await _periodosRepository.CrearAsync(periodo);
 127
 5128        await _auditoriaService.LogAuditoriaAsync(
 5129            _currentUser.UserId ?? 0,
 5130            AccionEnum.AbrirPeriodoLiquidacion,
 5131            ContextoEnum.Liquidaciones,
 5132            _currentUser.Host,
 5133            entidad: nameof(Periodo),
 5134            entidadId: periodoCreado.Id,
 5135            detalle: new
 5136            {
 5137                anio,
 5138                mes,
 5139                snapshotId = snapshotCreado.Id,
 5140                hashSnapshot
 5141            });
 142
 5143        return (true, periodoCreado, null);
 9144    }
 145
 146    public Task<Periodo?> ObtenerActivoAsync()
 147    {
 0148        return _periodosRepository.ObtenerActivoAsync();
 149    }
 150
 151    public Task<Periodo?> ObtenerEnCursoAsync()
 152    {
 0153        return _periodosRepository.ObtenerEnCursoAsync();
 154    }
 155
 156    public Task<Periodo?> ObtenerPorIdAsync(long id)
 157    {
 0158        return _periodosRepository.ObtenerPorIdAsync(id);
 159    }
 160
 161    public async Task<PrecondicionesDto?> ObtenerPrecondicionesAsync(long periodoId)
 162    {
 0163        var validacion = await ValidarInsumosAsync(periodoId);
 0164        if (validacion is null) return null;
 165
 0166        bool Ok(string tipo) => validacion.ItemsIncompletos.All(i => i.Tipo != tipo);
 167        string Detail(string tipo, string okMsg, string failMsg) =>
 0168            Ok(tipo) ? okMsg : (validacion.ItemsIncompletos.FirstOrDefault(i => i.Tipo == tipo)?.Descripcion ?? failMsg)
 169
 0170        var lotesOk = Ok("lote_faltante");
 0171        var novedadesOk = Ok("novedades_pendientes");
 0172        var beneficiosOk = Ok("beneficios_faltantes");
 0173        var remuneracionesOk = Ok("tabla_referencia") ||
 0174            !validacion.ItemsIncompletos.Any(i => i.Clave == "SinRemuneraciones");
 0175        var franjasOk = !validacion.ItemsIncompletos.Any(i => i.Clave == "SinFranjasIrpf");
 0176        var funcionariosOk = !validacion.ItemsIncompletos.Any(i => i.Clave == "SinFuncionarios");
 177
 0178        return new PrecondicionesDto
 0179        {
 0180            LotesAprobados = new PrecondicionItemDto
 0181            {
 0182                Ok = lotesOk,
 0183                Detail = lotesOk ? "Lotes aprobados" : "Sin lotes aprobados para el período"
 0184            },
 0185            NovedadesSinConfirmar = new PrecondicionItemDto
 0186            {
 0187                Ok = novedadesOk,
 0188                Detail = novedadesOk ? "Sin novedades pendientes" :
 0189                    validacion.ItemsIncompletos.FirstOrDefault(i => i.Tipo == "novedades_pendientes")?.Descripcion ?? "N
 0190            },
 0191            BeneficiosVigentes = new PrecondicionItemDto
 0192            {
 0193                Ok = beneficiosOk,
 0194                Detail = beneficiosOk ? "Beneficios sociales activos" : "Sin beneficios sociales activos vigentes"
 0195            },
 0196            RemuneracionesVigentes = new PrecondicionItemDto
 0197            {
 0198                Ok = remuneracionesOk,
 0199                Detail = remuneracionesOk ? "Tablas de remuneración vigentes" : "Sin tablas de remuneración para el perí
 0200            },
 0201            FranjasIrpf = new PrecondicionItemDto
 0202            {
 0203                Ok = franjasOk,
 0204                Detail = franjasOk ? "Franjas IRPF vigentes" : "Sin franjas IRPF para el período"
 0205            },
 0206            FuncionariosActivos = new PrecondicionItemDto
 0207            {
 0208                Ok = funcionariosOk,
 0209                Detail = funcionariosOk ? "Funcionarios activos en el padrón" : "No hay funcionarios activos"
 0210            }
 0211        };
 0212    }
 213
 214    public async Task<ValidacionInsumosDto?> ValidarInsumosAsync(long periodoId)
 215    {
 0216        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 0217        if (periodo is null) return null;
 0218        return await _evaluador.EvaluarAsync(periodo);
 0219    }
 220
 221    public async Task<(bool Exito, Periodo? Periodo, ValidacionInsumosDto? Validacion, string? Error)> CerrarInsumosAsyn
 222        long periodoId, List<string>? confirmaciones = null)
 223    {
 8224        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 8225        if (periodo is null)
 0226            return (false, null, null, "Período no encontrado");
 227
 8228        if (periodo.Estado != Periodo.EstadoAbierta)
 1229            return (false, null, null, $"Solo se puede cerrar insumos desde estado ABIERTA. Estado actual: {periodo.Esta
 230
 7231        var validacion = await _evaluador.EvaluarAsync(periodo);
 232
 7233        if (!validacion.TodosCompletos)
 234        {
 5235            if (confirmaciones is null || confirmaciones.Count == 0)
 4236                return (false, null, validacion, "Hay insumos incompletos. Revisá el detalle y confirmá cada omisión par
 237
 2238            var bloqueantes = validacion.ItemsIncompletos.Where(i => !i.EsConfirmable).ToList();
 1239            if (bloqueantes.Count > 0)
 0240                return (false, null, validacion, "Existen problemas bloqueantes que no pueden omitirse.");
 241
 1242            var clavesSinConfirmar = validacion.ItemsIncompletos
 1243                .Where(i => !confirmaciones.Contains(i.Clave))
 1244                .ToList();
 1245            if (clavesSinConfirmar.Count > 0)
 0246                return (false, null, new ValidacionInsumosDto
 0247                {
 0248                    TodosCompletos = false,
 0249                    PuedeConfirmar = true,
 0250                    ItemsIncompletos = clavesSinConfirmar
 0251                }, "Faltan confirmar algunos items incompletos.");
 252        }
 253
 3254        var usuarioId = _currentUser.UserId ?? 0;
 3255        periodo.Estado = Periodo.EstadoListaCalculo;
 3256        periodo.FechaCierreInsumos = DateTime.UtcNow;
 3257        periodo.UsuarioCierreInsumosId = usuarioId;
 3258        await _periodosRepository.ActualizarAsync(periodo);
 259
 3260        var omisiones = confirmaciones ?? new List<string>();
 3261        await _auditoriaService.LogAuditoriaAsync(
 3262            usuarioId,
 3263            AccionEnum.CerrarInsumosPeriodo,
 3264            ContextoEnum.Liquidaciones,
 3265            _currentUser.Host,
 3266            entidad: nameof(Periodo),
 3267            entidadId: periodo.Id,
 3268            detalle: new
 3269            {
 3270                anio = periodo.Anio,
 3271                mes = periodo.Mes,
 3272                omisionesConfirmadas = omisiones.Select(c =>
 0273                    validacion.ItemsIncompletos.FirstOrDefault(i => i.Clave == c)?.Descripcion ?? c)
 3274            });
 275
 3276        return (true, periodo, null, null);
 8277    }
 278
 279    public async Task<(bool Exito, Periodo? Periodo, string? Error)> ReabrirInsumosAsync(long periodoId)
 280    {
 3281        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3282        if (periodo is null)
 0283            return (false, null, "Período no encontrado");
 284
 3285        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1286            return (false, null, $"Solo se puede reabrir desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 287
 2288        var usuarioId = _currentUser.UserId ?? 0;
 2289        periodo.Estado = Periodo.EstadoAbierta;
 2290        periodo.FechaCierreInsumos = null;
 2291        periodo.UsuarioCierreInsumosId = null;
 2292        await _periodosRepository.ActualizarAsync(periodo);
 293
 2294        await _auditoriaService.LogAuditoriaAsync(
 2295            usuarioId,
 2296            AccionEnum.ReabrirInsumosPeriodo,
 2297            ContextoEnum.Liquidaciones,
 2298            _currentUser.Host,
 2299            entidad: nameof(Periodo),
 2300            entidadId: periodo.Id,
 2301            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 302
 2303        return (true, periodo, null);
 3304    }
 305
 306    public async Task<(bool Exito, Periodo? Periodo, string? Error)> IniciarCalculoAsync(long periodoId)
 307    {
 2308        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2309        if (periodo is null)
 0310            return (false, null, "Período no encontrado");
 311
 2312        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1313            return (false, null, $"Solo se puede calcular desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 314
 1315        if (await _descuentoPersonalRepo.TieneBorradoresAsync(periodoId))
 0316            return (false, null, "Existen descuentos personales sin confirmar. Confirme todos los borradores antes de in
 317
 1318        var usuarioId = _currentUser.UserId ?? 0;
 1319        periodo.Estado = Periodo.EstadoEnCalculo;
 1320        periodo.FechaCalculo = DateTime.UtcNow;
 1321        periodo.UsuarioCalculoId = usuarioId;
 1322        await _periodosRepository.ActualizarAsync(periodo);
 323
 1324        await _auditoriaService.LogAuditoriaAsync(
 1325            usuarioId,
 1326            AccionEnum.IniciarCalculoPeriodo,
 1327            ContextoEnum.Liquidaciones,
 1328            _currentUser.Host,
 1329            entidad: nameof(Periodo),
 1330            entidadId: periodo.Id,
 1331            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 332
 1333        return (true, periodo, null);
 2334    }
 335
 336    public async Task<(bool Exito, Periodo? Periodo, string? Error)> FinalizarCalculoAsync(long periodoId, bool exitoso,
 337    {
 2338        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2339        if (periodo is null)
 0340            return (false, null, "Período no encontrado");
 341
 2342        if (periodo.Estado != Periodo.EstadoEnCalculo)
 0343            return (false, null, $"Solo se puede finalizar el cálculo desde estado EN_CALCULO. Estado actual: {periodo.E
 344
 2345        var usuarioId = _currentUser.UserId ?? 0;
 2346        if (exitoso)
 347        {
 1348            periodo.Estado = Periodo.EstadoCalculada;
 1349            periodo.FechaCalculo = DateTime.UtcNow;
 350        }
 351        else
 352        {
 1353            periodo.Estado = Periodo.EstadoListaCalculo;
 354        }
 355
 2356        await _periodosRepository.ActualizarAsync(periodo);
 357
 2358        await _auditoriaService.LogAuditoriaAsync(
 2359            usuarioId,
 2360            AccionEnum.IniciarCalculoPeriodo,
 2361            ContextoEnum.Liquidaciones,
 2362            _currentUser.Host,
 2363            entidad: nameof(Periodo),
 2364            entidadId: periodo.Id,
 2365            detalle: new { exitoso, mensajeError });
 366
 2367        return (true, periodo, null);
 2368    }
 369
 370    public async Task<(bool Exito, Periodo? Periodo, string? Error)> CerrarAsync(long periodoId)
 371    {
 3372        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3373        if (periodo is null)
 0374            return (false, null, "Período no encontrado");
 375
 3376        if (periodo.Estado != Periodo.EstadoCalculada)
 1377            return (false, null, $"Solo se puede cerrar desde estado CALCULADA. Estado actual: {periodo.Estado}");
 378
 2379        var usuarioId = _currentUser.UserId ?? 0;
 2380        periodo.Estado = Periodo.EstadoCerrada;
 2381        periodo.FechaCierre = DateTime.UtcNow;
 2382        periodo.UsuarioCierreId = usuarioId;
 2383        await _periodosRepository.ActualizarAsync(periodo);
 384
 2385        await _auditoriaService.LogAuditoriaAsync(
 2386            usuarioId,
 2387            AccionEnum.CerrarPeriodo,
 2388            ContextoEnum.Liquidaciones,
 2389            _currentUser.Host,
 2390            entidad: nameof(Periodo),
 2391            entidadId: periodo.Id,
 2392            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 393
 2394        return (true, periodo, null);
 3395    }
 396
 397    private async Task<string> SerializarSnapshotAsync(
 398        short anio,
 399        short mes,
 400        ParametroPeriodo parametrosPeriodo,
 401        DateTime fechaInicioPeriodo,
 402        DateTime fechaFinPeriodo)
 403    {
 5404        var lotes = await _context.LotesCompensacion
 5405            .AsNoTracking()
 5406            .Where(l => l.Periodo.Year == anio && l.Periodo.Month == mes && l.Estado == "aprobado")
 5407            .Select(l => new
 5408            {
 5409                l.Id,
 5410                l.TipoCompensacionId,
 5411                l.Periodo,
 5412                l.Estado,
 5413                l.Fuente,
 5414                l.NombreArchivo,
 5415                l.HashArchivo,
 5416                l.UsuarioId,
 5417                l.AprobadoPor,
 5418                l.AprobadoEn
 5419            })
 5420            .ToListAsync();
 421
 5422        var beneficios = await _context.BeneficiosSociales
 5423            .AsNoTracking()
 5424            .Where(b => b.Estado == "activo" && b.VigenteDesde <= fechaFinPeriodo && (b.VigenteHasta == null || b.Vigent
 5425            .Select(b => new
 5426            {
 5427                b.Id,
 5428                b.PersonaId,
 5429                b.TipoBeneficioId,
 5430                b.Cantidad,
 5431                b.VigenteDesde,
 5432                b.VigenteHasta,
 5433                b.Estado,
 5434                b.ClaveEvento,
 5435                b.FechaEvento,
 5436                b.RetroactividadPendiente
 5437            })
 5438            .ToListAsync();
 439
 5440        var remuneraciones = await _context.RemuneracionesGrado
 5441            .AsNoTracking()
 5442            .Where(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaInicio
 5443            .Select(r => new
 5444            {
 5445                r.Id,
 5446                r.GradoId,
 5447                r.ItemId,
 5448                r.Monto,
 5449                r.VigenteDesde,
 5450                r.VigenteHasta
 5451            })
 5452            .ToListAsync();
 453
 5454        var franjasIrpf = await _context.FranjasIrpf
 5455            .AsNoTracking()
 5456            .Where(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaInicio
 5457            .Select(f => new
 5458            {
 5459                f.Id,
 5460                f.NumeroFranja,
 5461                f.DesdeBpc,
 5462                f.HastaBpc,
 5463                f.Tasa,
 5464                f.VigenteDesde,
 5465                f.VigenteHasta
 5466            })
 5467            .ToListAsync();
 468
 5469        var snapshot = new
 5470        {
 5471            anio,
 5472            mes,
 5473            fechaCreacion = DateTime.UtcNow,
 5474            parametros = new
 5475            {
 5476                parametrosPeriodo.Anio,
 5477                parametrosPeriodo.Mes,
 5478                parametrosPeriodo.BpcAnual,
 5479                parametrosPeriodo.TasaMontepioLeyVieja,
 5480                parametrosPeriodo.TasaMontepioLeyNueva,
 5481                parametrosPeriodo.TasaFonasa,
 5482                parametrosPeriodo.TasaFrl,
 5483                parametrosPeriodo.UmbralDeduccionBpc,
 5484                parametrosPeriodo.TasaDeduccionAlta,
 5485                parametrosPeriodo.TasaDeduccionBaja,
 5486                parametrosPeriodo.PorcentajeMinimoDeficit
 5487            },
 5488            lotesCompensacion = lotes,
 5489            beneficiosSociales = beneficios,
 5490            remuneracionesGrado = remuneraciones,
 5491            franjasIrpf = franjasIrpf
 5492        };
 493
 5494        return JsonSerializer.Serialize(snapshot, new JsonSerializerOptions
 5495        {
 5496            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 5497            WriteIndented = false
 5498        });
 5499    }
 500
 501    private static string CalcularHash(string contenidoJson)
 502    {
 5503        using var sha256 = SHA256.Create();
 5504        var bytes = Encoding.UTF8.GetBytes(contenidoJson);
 5505        return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
 5506    }
 507}