< 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
94%
Covered lines: 296
Uncovered lines: 16
Coverable lines: 312
Total lines: 442
Line coverage: 94.8%
Branch coverage
72%
Covered branches: 42
Total branches: 58
Branch coverage: 72.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()70.83%242492.64%
ObtenerActivoAsync()100%210%
ObtenerPorIdAsync(...)100%210%
CerrarInsumosAsync()75%161692.59%
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 Microsoft.EntityFrameworkCore;
 8
 9namespace FAU.Logica.Services;
 10
 11public class PeriodoService : IPeriodoService
 12{
 13    private readonly IPeriodosRepository _periodosRepository;
 14    private readonly ISnapshotsRepository _snapshotsRepository;
 15    private readonly ApplicationDbContext _context;
 16    private readonly IAuditoriaService _auditoriaService;
 17    private readonly ICurrentUserContext _currentUser;
 18
 2319    public PeriodoService(
 2320        IPeriodosRepository periodosRepository,
 2321        ISnapshotsRepository snapshotsRepository,
 2322        ApplicationDbContext context,
 2323        IAuditoriaService auditoriaService,
 2324        ICurrentUserContext currentUser)
 25    {
 2326        _periodosRepository = periodosRepository;
 2327        _snapshotsRepository = snapshotsRepository;
 2328        _context = context;
 2329        _auditoriaService = auditoriaService;
 2330        _currentUser = currentUser;
 2331    }
 32
 33    public async Task<(bool Exito, Periodo? Periodo, string? Error)> AbrirPeriodoAsync(short anio, short mes)
 34    {
 935        if (anio < 2000 || anio > 2100)
 36        {
 037            return (false, null, "El año debe estar entre 2000 y 2100");
 38        }
 39
 940        if (mes < 1 || mes > 12)
 41        {
 042            return (false, null, "El mes debe estar entre 1 y 12");
 43        }
 44
 945        var periodoExistente = await _periodosRepository.ObtenerPorAnioMesAsync(anio, mes);
 946        if (periodoExistente is not null && periodoExistente.Estado == Periodo.EstadoAbierta && periodoExistente.Activo)
 47        {
 148            return (true, periodoExistente, null);
 49        }
 50
 851        var periodoEnCurso = await _periodosRepository.ObtenerEnCursoAsync();
 852        if (periodoEnCurso is not null)
 53        {
 154            return (false, null, $"Ya existe un período activo en estado {periodoEnCurso.Estado}");
 55        }
 56
 757        var parametrosPeriodo = await _context.ParametrosPeriodo
 758            .AsNoTracking()
 759            .FirstOrDefaultAsync(p => p.Anio == anio && p.Mes == mes);
 60
 761        if (parametrosPeriodo is null)
 62        {
 263            return (false, null, "No existen parámetros de liquidación para el período indicado");
 64        }
 65
 566        var fechaInicioPeriodo = new DateTime(anio, mes, 1, 0, 0, 0, DateTimeKind.Utc);
 567        var fechaFinPeriodo = fechaInicioPeriodo.AddMonths(1).AddTicks(-1);
 68
 569        var existeRemuneraciones = await _context.RemuneracionesGrado
 570            .AsNoTracking()
 571            .AnyAsync(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaIni
 72
 573        if (!existeRemuneraciones)
 74        {
 075            return (false, null, "No existen remuneraciones de grado vigentes para el período");
 76        }
 77
 578        var existeFranjaIrpf = await _context.FranjasIrpf
 579            .AsNoTracking()
 580            .AnyAsync(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaIni
 81
 582        if (!existeFranjaIrpf)
 83        {
 084            return (false, null, "No existen franjas IRPF vigentes para el período");
 85        }
 86
 587        var existeRelacionLaboral = await _context.RelacionesLaborales
 588            .AsNoTracking()
 589            .AnyAsync(r => r.Estado == EstadoRelacion.Activo);
 90
 591        if (!existeRelacionLaboral)
 92        {
 093            return (false, null, "No hay funcionarios activos en el padrón");
 94        }
 95
 596        var snapshotJson = await SerializarSnapshotAsync(anio, mes, parametrosPeriodo, fechaInicioPeriodo, fechaFinPerio
 597        var hashSnapshot = CalcularHash(snapshotJson);
 98
 599        var snapshot = new SnapshotInsumos
 5100        {
 5101            ContenidoJson = snapshotJson,
 5102            HashIntegridad = hashSnapshot
 5103        };
 104
 5105        var snapshotCreado = await _snapshotsRepository.CrearAsync(snapshot);
 106
 5107        var periodo = new Periodo
 5108        {
 5109            Anio = anio,
 5110            Mes = mes,
 5111            Estado = Periodo.EstadoAbierta,
 5112            SnapshotId = snapshotCreado.Id,
 5113            FechaApertura = DateTime.UtcNow,
 5114            UsuarioApertura = _currentUser.UserId ?? 0,
 5115            HashSnapshot = hashSnapshot,
 5116            Activo = true
 5117        };
 118
 5119        var periodoCreado = await _periodosRepository.CrearAsync(periodo);
 120
 5121        await _auditoriaService.LogAuditoriaAsync(
 5122            _currentUser.UserId ?? 0,
 5123            AccionEnum.AbrirPeriodoLiquidacion,
 5124            ContextoEnum.Liquidaciones,
 5125            _currentUser.Host,
 5126            entidad: nameof(Periodo),
 5127            entidadId: periodoCreado.Id,
 5128            detalle: new
 5129            {
 5130                anio,
 5131                mes,
 5132                snapshotId = snapshotCreado.Id,
 5133                hashSnapshot
 5134            });
 135
 5136        return (true, periodoCreado, null);
 9137    }
 138
 139    public Task<Periodo?> ObtenerActivoAsync()
 140    {
 0141        return _periodosRepository.ObtenerActivoAsync();
 142    }
 143
 144    public Task<Periodo?> ObtenerPorIdAsync(long id)
 145    {
 0146        return _periodosRepository.ObtenerPorIdAsync(id);
 147    }
 148
 149    public async Task<(bool Exito, Periodo? Periodo, string? Error)> CerrarInsumosAsync(long periodoId)
 150    {
 6151        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 6152        if (periodo is null)
 0153            return (false, null, "Período no encontrado");
 154
 6155        if (periodo.Estado != Periodo.EstadoAbierta)
 1156            return (false, null, $"Solo se puede cerrar insumos desde estado ABIERTA. Estado actual: {periodo.Estado}");
 157
 5158        var anio = periodo.Anio;
 5159        var mes = periodo.Mes;
 5160        var fechaInicio = new DateTime(anio, mes, 1, 0, 0, 0, DateTimeKind.Utc);
 5161        var fechaFin = fechaInicio.AddMonths(1).AddTicks(-1);
 162
 5163        var tieneLosteAprobado = await _context.LotesCompensacion
 5164            .AsNoTracking()
 5165            .AnyAsync(l => l.Periodo.Year == anio && l.Periodo.Month == mes && l.Estado == "aprobado");
 5166        if (!tieneLosteAprobado)
 1167            return (false, null, "Debe existir al menos un lote de compensación aprobado");
 168
 4169        var countNovedadesBorrador = await _context.NovedadesPeriodo
 4170            .AsNoTracking()
 4171            .CountAsync(n => n.PeriodoId == periodoId && n.Estado == "borrador");
 4172        if (countNovedadesBorrador > 0)
 1173            return (false, null, $"Existen {countNovedadesBorrador} novedad(es) sin confirmar para el período");
 174
 3175        var tieneBeneficios = await _context.BeneficiosSociales
 3176            .AsNoTracking()
 3177            .AnyAsync(b => b.Estado == "activo" && b.VigenteDesde <= fechaFin && (b.VigenteHasta == null || b.VigenteHas
 3178        if (!tieneBeneficios)
 1179            return (false, null, "Debe existir al menos un beneficio social activo vigente para el período");
 180
 2181        var tieneRemuneraciones = await _context.RemuneracionesGrado
 2182            .AsNoTracking()
 2183            .AnyAsync(r => r.VigenteDesde <= fechaInicio && (r.VigenteHasta == null || r.VigenteHasta >= fechaInicio));
 2184        if (!tieneRemuneraciones)
 0185            return (false, null, "No existen remuneraciones de grado vigentes para el período");
 186
 2187        var tieneFranjas = await _context.FranjasIrpf
 2188            .AsNoTracking()
 2189            .AnyAsync(f => f.VigenteDesde <= fechaInicio && (f.VigenteHasta == null || f.VigenteHasta >= fechaInicio));
 2190        if (!tieneFranjas)
 0191            return (false, null, "No existen franjas IRPF vigentes para el período");
 192
 2193        var tieneFuncionarios = await _context.RelacionesLaborales
 2194            .AsNoTracking()
 2195            .AnyAsync(r => r.Estado == EstadoRelacion.Activo);
 2196        if (!tieneFuncionarios)
 0197            return (false, null, "No hay funcionarios activos en el padrón");
 198
 2199        var usuarioId = _currentUser.UserId ?? 0;
 2200        periodo.Estado = Periodo.EstadoListaCalculo;
 2201        periodo.FechaCierreInsumos = DateTime.UtcNow;
 2202        periodo.UsuarioCierreInsumosId = usuarioId;
 2203        await _periodosRepository.ActualizarAsync(periodo);
 204
 2205        await _auditoriaService.LogAuditoriaAsync(
 2206            usuarioId,
 2207            AccionEnum.CerrarInsumosPeriodo,
 2208            ContextoEnum.Liquidaciones,
 2209            _currentUser.Host,
 2210            entidad: nameof(Periodo),
 2211            entidadId: periodo.Id,
 2212            detalle: new { anio, mes });
 213
 2214        return (true, periodo, null);
 6215    }
 216
 217    public async Task<(bool Exito, Periodo? Periodo, string? Error)> ReabrirInsumosAsync(long periodoId)
 218    {
 3219        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3220        if (periodo is null)
 0221            return (false, null, "Período no encontrado");
 222
 3223        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1224            return (false, null, $"Solo se puede reabrir desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 225
 2226        var usuarioId = _currentUser.UserId ?? 0;
 2227        periodo.Estado = Periodo.EstadoAbierta;
 2228        periodo.FechaCierreInsumos = null;
 2229        periodo.UsuarioCierreInsumosId = null;
 2230        await _periodosRepository.ActualizarAsync(periodo);
 231
 2232        await _auditoriaService.LogAuditoriaAsync(
 2233            usuarioId,
 2234            AccionEnum.ReabrirInsumosPeriodo,
 2235            ContextoEnum.Liquidaciones,
 2236            _currentUser.Host,
 2237            entidad: nameof(Periodo),
 2238            entidadId: periodo.Id,
 2239            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 240
 2241        return (true, periodo, null);
 3242    }
 243
 244    public async Task<(bool Exito, Periodo? Periodo, string? Error)> IniciarCalculoAsync(long periodoId)
 245    {
 2246        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2247        if (periodo is null)
 0248            return (false, null, "Período no encontrado");
 249
 2250        if (periodo.Estado != Periodo.EstadoListaCalculo)
 1251            return (false, null, $"Solo se puede calcular desde estado LISTA_CALCULO. Estado actual: {periodo.Estado}");
 252
 1253        var usuarioId = _currentUser.UserId ?? 0;
 1254        periodo.Estado = Periodo.EstadoEnCalculo;
 1255        periodo.FechaCalculo = DateTime.UtcNow;
 1256        periodo.UsuarioCalculoId = usuarioId;
 1257        await _periodosRepository.ActualizarAsync(periodo);
 258
 1259        await _auditoriaService.LogAuditoriaAsync(
 1260            usuarioId,
 1261            AccionEnum.IniciarCalculoPeriodo,
 1262            ContextoEnum.Liquidaciones,
 1263            _currentUser.Host,
 1264            entidad: nameof(Periodo),
 1265            entidadId: periodo.Id,
 1266            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 267
 1268        return (true, periodo, null);
 2269    }
 270
 271    public async Task<(bool Exito, Periodo? Periodo, string? Error)> FinalizarCalculoAsync(long periodoId, bool exitoso,
 272    {
 2273        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 2274        if (periodo is null)
 0275            return (false, null, "Período no encontrado");
 276
 2277        if (periodo.Estado != Periodo.EstadoEnCalculo)
 0278            return (false, null, $"Solo se puede finalizar el cálculo desde estado EN_CALCULO. Estado actual: {periodo.E
 279
 2280        var usuarioId = _currentUser.UserId ?? 0;
 2281        if (exitoso)
 282        {
 1283            periodo.Estado = Periodo.EstadoCalculada;
 1284            periodo.FechaCalculo = DateTime.UtcNow;
 285        }
 286        else
 287        {
 1288            periodo.Estado = Periodo.EstadoListaCalculo;
 289        }
 290
 2291        await _periodosRepository.ActualizarAsync(periodo);
 292
 2293        await _auditoriaService.LogAuditoriaAsync(
 2294            usuarioId,
 2295            AccionEnum.IniciarCalculoPeriodo,
 2296            ContextoEnum.Liquidaciones,
 2297            _currentUser.Host,
 2298            entidad: nameof(Periodo),
 2299            entidadId: periodo.Id,
 2300            detalle: new { exitoso, mensajeError });
 301
 2302        return (true, periodo, null);
 2303    }
 304
 305    public async Task<(bool Exito, Periodo? Periodo, string? Error)> CerrarAsync(long periodoId)
 306    {
 3307        var periodo = await _periodosRepository.ObtenerPorIdAsync(periodoId);
 3308        if (periodo is null)
 0309            return (false, null, "Período no encontrado");
 310
 3311        if (periodo.Estado != Periodo.EstadoCalculada)
 1312            return (false, null, $"Solo se puede cerrar desde estado CALCULADA. Estado actual: {periodo.Estado}");
 313
 2314        var usuarioId = _currentUser.UserId ?? 0;
 2315        periodo.Estado = Periodo.EstadoCerrada;
 2316        periodo.FechaCierre = DateTime.UtcNow;
 2317        periodo.UsuarioCierreId = usuarioId;
 2318        await _periodosRepository.ActualizarAsync(periodo);
 319
 2320        await _auditoriaService.LogAuditoriaAsync(
 2321            usuarioId,
 2322            AccionEnum.CerrarPeriodo,
 2323            ContextoEnum.Liquidaciones,
 2324            _currentUser.Host,
 2325            entidad: nameof(Periodo),
 2326            entidadId: periodo.Id,
 2327            detalle: new { anio = periodo.Anio, mes = periodo.Mes });
 328
 2329        return (true, periodo, null);
 3330    }
 331
 332    private async Task<string> SerializarSnapshotAsync(
 333        short anio,
 334        short mes,
 335        ParametroPeriodo parametrosPeriodo,
 336        DateTime fechaInicioPeriodo,
 337        DateTime fechaFinPeriodo)
 338    {
 5339        var lotes = await _context.LotesCompensacion
 5340            .AsNoTracking()
 5341            .Where(l => l.Periodo.Year == anio && l.Periodo.Month == mes && l.Estado == "aprobado")
 5342            .Select(l => new
 5343            {
 5344                l.Id,
 5345                l.TipoCompensacionId,
 5346                l.Periodo,
 5347                l.Estado,
 5348                l.Fuente,
 5349                l.NombreArchivo,
 5350                l.HashArchivo,
 5351                l.UsuarioId,
 5352                l.AprobadoPor,
 5353                l.AprobadoEn
 5354            })
 5355            .ToListAsync();
 356
 5357        var beneficios = await _context.BeneficiosSociales
 5358            .AsNoTracking()
 5359            .Where(b => b.Estado == "activo" && b.VigenteDesde <= fechaFinPeriodo && (b.VigenteHasta == null || b.Vigent
 5360            .Select(b => new
 5361            {
 5362                b.Id,
 5363                b.PersonaId,
 5364                b.TipoBeneficioId,
 5365                b.Cantidad,
 5366                b.VigenteDesde,
 5367                b.VigenteHasta,
 5368                b.Estado,
 5369                b.ClaveEvento,
 5370                b.FechaEvento,
 5371                b.RetroactividadPendiente
 5372            })
 5373            .ToListAsync();
 374
 5375        var remuneraciones = await _context.RemuneracionesGrado
 5376            .AsNoTracking()
 5377            .Where(r => r.VigenteDesde <= fechaInicioPeriodo && (r.VigenteHasta == null || r.VigenteHasta >= fechaInicio
 5378            .Select(r => new
 5379            {
 5380                r.Id,
 5381                r.GradoId,
 5382                r.ItemId,
 5383                r.Monto,
 5384                r.VigenteDesde,
 5385                r.VigenteHasta
 5386            })
 5387            .ToListAsync();
 388
 5389        var franjasIrpf = await _context.FranjasIrpf
 5390            .AsNoTracking()
 5391            .Where(f => f.VigenteDesde <= fechaInicioPeriodo && (f.VigenteHasta == null || f.VigenteHasta >= fechaInicio
 5392            .Select(f => new
 5393            {
 5394                f.Id,
 5395                f.NumeroFranja,
 5396                f.DesdeBpc,
 5397                f.HastaBpc,
 5398                f.Tasa,
 5399                f.VigenteDesde,
 5400                f.VigenteHasta
 5401            })
 5402            .ToListAsync();
 403
 5404        var snapshot = new
 5405        {
 5406            anio,
 5407            mes,
 5408            fechaCreacion = DateTime.UtcNow,
 5409            parametros = new
 5410            {
 5411                parametrosPeriodo.Anio,
 5412                parametrosPeriodo.Mes,
 5413                parametrosPeriodo.BpcAnual,
 5414                parametrosPeriodo.TasaMontepioLeyVieja,
 5415                parametrosPeriodo.TasaMontepioLeyNueva,
 5416                parametrosPeriodo.TasaFonasa,
 5417                parametrosPeriodo.TasaFrl,
 5418                parametrosPeriodo.UmbralDeduccionBpc,
 5419                parametrosPeriodo.TasaDeduccionAlta,
 5420                parametrosPeriodo.TasaDeduccionBaja,
 5421                parametrosPeriodo.PorcentajeMinimoDeficit
 5422            },
 5423            lotesCompensacion = lotes,
 5424            beneficiosSociales = beneficios,
 5425            remuneracionesGrado = remuneraciones,
 5426            franjasIrpf = franjasIrpf
 5427        };
 428
 5429        return JsonSerializer.Serialize(snapshot, new JsonSerializerOptions
 5430        {
 5431            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 5432            WriteIndented = false
 5433        });
 5434    }
 435
 436    private static string CalcularHash(string contenidoJson)
 437    {
 5438        using var sha256 = SHA256.Create();
 5439        var bytes = Encoding.UTF8.GetBytes(contenidoJson);
 5440        return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
 5441    }
 442}