< Summary

Information
Class: FAU.Logica.Services.AguinaldoBajaService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/AguinaldoBajaService.cs
Line coverage
52%
Covered lines: 149
Uncovered lines: 137
Coverable lines: 286
Total lines: 433
Line coverage: 52%
Branch coverage
32%
Covered branches: 23
Total branches: 70
Branch coverage: 32.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/AguinaldoBajaService.cs

#LineLine coverage
 1using FAU.DataAccess;
 2using FAU.DataAccess.Repositories;
 3using FAU.Entidades;
 4using FAU.Logica.DTOs;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace FAU.Logica.Services;
 8
 9public class AguinaldoBajaService : IAguinaldoBajaService
 10{
 11    private readonly IAguinaldoBajaRepository _aguinaldoBajaRepository;
 12    private readonly ApplicationDbContext _context;
 13    private readonly AguinaldoBajaCalculator _calculator;
 14    private readonly IAuditoriaService _auditoriaService;
 15    private readonly ICurrentUserContext _currentUser;
 16
 217    public AguinaldoBajaService(
 218        IAguinaldoBajaRepository aguinaldoBajaRepository,
 219        ApplicationDbContext context,
 220        AguinaldoBajaCalculator calculator,
 221        IAuditoriaService auditoriaService,
 222        ICurrentUserContext currentUser)
 23    {
 224        _aguinaldoBajaRepository = aguinaldoBajaRepository;
 225        _context = context;
 226        _calculator = calculator;
 227        _auditoriaService = auditoriaService;
 228        _currentUser = currentUser;
 229    }
 30
 31    public async Task<AguinaldoValidacionDto> ValidarHistoricoAsync(CalcularAguinaldoBajaRequest request)
 32    {
 033        var resultado = new AguinaldoValidacionDto();
 34
 035        if (!EsMesProcesable(request.Mes))
 36        {
 037            resultado.PersonasConHistoricoIncompleto = 1;
 038            resultado.Incidencias.Add(new AguinaldoIncidenciaDto
 039            {
 040                Tipo = "MES_INVALIDO",
 041                Cedula = string.Empty,
 042                NombreCompleto = string.Empty,
 043                Descripcion = "El cálculo de aguinaldo por baja no aplica en los meses 6 o 12 (cubiertos por el proceso 
 044            });
 045            return resultado;
 46        }
 47
 048        var parametros = await ObtenerParametrosPeriodoAsync(request);
 049        if (parametros is null)
 50        {
 051            resultado.PersonasConHistoricoIncompleto = 1;
 052            resultado.Incidencias.Add(new AguinaldoIncidenciaDto
 053            {
 054                Tipo = "PARAMETROS_INEXISTENTES",
 055                Cedula = string.Empty,
 056                NombreCompleto = string.Empty,
 057                Descripcion = "No existe configuración de parámetros para el período solicitado."
 058            });
 059            return resultado;
 60        }
 61
 062        var bajas = (await ObtenerBajasParaPeriodoAsync(request)).ToList();
 063        resultado.TotalPersonasActivas = bajas.Count;
 64
 065        var semestre = CalcularSemestre(request.Mes);
 066        var personaIds = bajas.Select(b => b.RelacionLaboral.PersonaId).ToList();
 67
 68        // Pre-cargar historico de todo el período para evitar N+1
 069        var historicosPorPersona = await CargarHistoricoSemestreAsync(request, personaIds);
 70
 071        foreach (var baja in bajas)
 72        {
 073            var personaId = baja.RelacionLaboral.PersonaId;
 74
 075            var yaProcesado = await _aguinaldoBajaRepository.GetByPersonaFechaBajaAsync(
 076                personaId, baja.FechaMovimiento);
 77
 078            if (yaProcesado is not null)
 79            {
 080                resultado.PersonasConHistoricoIncompleto++;
 081                resultado.Incidencias.Add(new AguinaldoIncidenciaDto
 082                {
 083                    Tipo = "YA_PROCESADO",
 084                    Cedula = baja.RelacionLaboral.Persona.Cedula,
 085                    NombreCompleto = baja.RelacionLaboral.Persona.NombreCompleto,
 086                    Descripcion = "Ya existe un aguinaldo por baja para esta fecha de baja."
 087                });
 088                continue;
 89            }
 90
 091            var cubiertoPorSemestral = await _context.Set<Aguinaldo>()
 092                .AnyAsync(a => a.PersonaId == personaId
 093                    && a.Anio == request.Anio
 094                    && a.Semestre == semestre);
 95
 096            if (cubiertoPorSemestral)
 97            {
 098                resultado.PersonasConHistoricoIncompleto++;
 099                resultado.Incidencias.Add(new AguinaldoIncidenciaDto
 0100                {
 0101                    Tipo = "CUBIERTO_POR_SEMESTRAL",
 0102                    Cedula = baja.RelacionLaboral.Persona.Cedula,
 0103                    NombreCompleto = baja.RelacionLaboral.Persona.NombreCompleto,
 0104                    Descripcion = $"El semestre {semestre}/{request.Anio} ya fue cubierto por el proceso de aguinaldo se
 0105                });
 0106                continue;
 107            }
 108
 0109            var mesesComputados = CalcularMesesComputados(request, baja.RelacionLaboral.FechaInicio);
 0110            if (mesesComputados <= 0)
 111            {
 0112                resultado.PersonasConHistoricoIncompleto++;
 0113                resultado.Incidencias.Add(new AguinaldoIncidenciaDto
 0114                {
 0115                    Tipo = "MESES_INVALIDOS",
 0116                    Cedula = baja.RelacionLaboral.Persona.Cedula,
 0117                    NombreCompleto = baja.RelacionLaboral.Persona.NombreCompleto,
 0118                    Descripcion = "No se pudo calcular el período de meses válidos para la baja."
 0119                });
 0120                continue;
 121            }
 122
 123            // Verificar meses faltantes en historico_liquidaciones
 0124            var mesesEsperados = ObtenerMesesEsperados(request, baja.RelacionLaboral.FechaInicio);
 0125            var encontrados = historicosPorPersona.GetValueOrDefault(personaId) ?? new HashSet<(short, short)>();
 0126            var faltantes = mesesEsperados.Where(m => !encontrados.Contains(m)).ToList();
 127
 0128            if (faltantes.Any())
 129            {
 0130                resultado.PersonasConHistoricoIncompleto++;
 0131                resultado.Incidencias.Add(new AguinaldoIncidenciaDto
 0132                {
 0133                    Tipo = "HistoricoIncompleto",
 0134                    Cedula = baja.RelacionLaboral.Persona.Cedula,
 0135                    NombreCompleto = baja.RelacionLaboral.Persona.NombreCompleto,
 0136                    MesesFaltantes = faltantes.Select(m => FormatearMesAnio(m.Anio, m.Mes)).ToList(),
 0137                    Descripcion = "Faltan meses en el histórico de liquidaciones para el período de baja."
 0138                });
 139            }
 140            else
 141            {
 0142                resultado.PersonasListasParaCalcular++;
 143            }
 0144        }
 145
 0146        return resultado;
 0147    }
 148
 149    public async Task<(bool Success, IEnumerable<AguinaldoBajaDto> Resultados, string? Error)> CalcularAguinaldoBajaAsyn
 150    {
 2151        if (!EsMesProcesable(request.Mes))
 152        {
 1153            return (false, Array.Empty<AguinaldoBajaDto>(), "El cálculo de aguinaldo por baja no aplica en los meses 6 o
 154        }
 155
 1156        var parametros = await ObtenerParametrosPeriodoAsync(request);
 1157        if (parametros is null)
 158        {
 0159            return (false, Array.Empty<AguinaldoBajaDto>(), "No existe configuración de parámetros para el período solic
 160        }
 161
 1162        var bajas = await ObtenerBajasParaPeriodoAsync(request);
 1163        var resultados = new List<AguinaldoBajaDto>();
 164
 1165        var semestre = CalcularSemestre(request.Mes);
 1166        var esPrimerSemestre = request.Mes <= 6;
 1167        var mesInicioPeriodo = esPrimerSemestre ? (short)12 : (short)6;
 1168        var mesFinPeriodo = (short)request.Mes;
 1169        var mesLiquidacion = (short)request.Mes;
 170
 4171        foreach (var baja in bajas)
 172        {
 1173            var existe = await _aguinaldoBajaRepository.GetByPersonaFechaBajaAsync(
 1174                baja.RelacionLaboral.PersonaId,
 1175                baja.FechaMovimiento);
 176
 1177            if (existe is not null)
 178                continue;
 179
 180            // D6: No procesar si el semestre ya fue cubierto por aguinaldo semestral
 1181            var cubiertoPorSemestral = await _context.Set<Aguinaldo>()
 1182                .AnyAsync(a => a.PersonaId == baja.RelacionLaboral.PersonaId
 1183                    && a.Anio == request.Anio
 1184                    && a.Semestre == semestre);
 185
 1186            if (cubiertoPorSemestral)
 187                continue;
 188
 1189            var mesesComputados = CalcularMesesComputados(request, baja.RelacionLaboral.FechaInicio);
 1190            if (mesesComputados <= 0)
 191                continue;
 192
 1193            var totalHaberesGravados = await ObtenerTotalHaberesGravadosAsync(request, baja.RelacionLaboral.PersonaId);
 1194            var tasaMontepio = baja.RelacionLaboral.Regimen.EsLeyVieja
 1195                ? parametros.TasaMontepioLeyVieja
 1196                : parametros.TasaMontepioLeyNueva;
 1197            var tasaSanidad = parametros.TasaFonasa;
 198
 1199            var resultadoCalculo = _calculator.Calcular(
 1200                totalHaberesGravados,
 1201                mesesComputados,
 1202                tasaMontepio,
 1203                tasaSanidad);
 204
 1205            var estado = totalHaberesGravados == 0m
 1206                ? "SIN_DERECHO"
 1207                : resultadoCalculo.Liquido <= 0m
 1208                    ? "INCIDENCIA"
 1209                    : "VALIDO";
 210
 1211            var aguinaldo = new AguinaldoBaja
 1212            {
 1213                PersonaId = baja.RelacionLaboral.PersonaId,
 1214                Cedula = baja.RelacionLaboral.Persona.Cedula,
 1215                NombreCompleto = baja.RelacionLaboral.Persona.NombreCompleto,
 1216                FechaBaja = baja.FechaMovimiento,
 1217                MotivoBajaCodigo = baja.RelacionLaboral.MotivoBaja?.Codigo,
 1218                Anio = (short)request.Anio,
 1219                Mes = (short)request.Mes,
 1220                Semestre = semestre,
 1221                TotalHaberesGravados = totalHaberesGravados,
 1222                MesesComputados = mesesComputados,
 1223                MesInicioPeriodo = mesInicioPeriodo,
 1224                MesFinPeriodo = mesFinPeriodo,
 1225                MesLiquidacion = mesLiquidacion,
 1226                Nominal = resultadoCalculo.Nominal,
 1227                Montepio = resultadoCalculo.Montepio,
 1228                Sanidad = resultadoCalculo.Sanidad,
 1229                TotalDescuentos = resultadoCalculo.TotalDescuentos,
 1230                Liquido = resultadoCalculo.Liquido,
 1231                Redondeo = resultadoCalculo.Redondeo,
 1232                Estado = estado,
 1233                Items = new List<AguinaldoBajaItem>
 1234                {
 1235                    new AguinaldoBajaItem
 1236                    {
 1237                        CodigoConcepto = "AGUINALDO_NOMINAL",
 1238                        NombreConcepto = "Nominal aguinaldo por baja",
 1239                        Importe = resultadoCalculo.Nominal,
 1240                        Tipo = "haber"
 1241                    },
 1242                    new AguinaldoBajaItem
 1243                    {
 1244                        CodigoConcepto = "AGUINALDO_MONTEPIO",
 1245                        NombreConcepto = "Montepío aguinaldo por baja",
 1246                        Importe = -resultadoCalculo.Montepio,
 1247                        Tipo = "descuento"
 1248                    },
 1249                    new AguinaldoBajaItem
 1250                    {
 1251                        CodigoConcepto = "AGUINALDO_DNS",
 1252                        NombreConcepto = "D.N.S.FF.AA. aguinaldo por baja",
 1253                        Importe = -resultadoCalculo.Sanidad,
 1254                        Tipo = "descuento"
 1255                    }
 1256                }
 1257            };
 258
 1259            var creado = await _aguinaldoBajaRepository.CreateAsync(aguinaldo);
 1260            resultados.Add(AguinaldoBajaDto.FromEntity(creado));
 261
 262            try
 263            {
 1264                await _auditoriaService.LogAuditoriaAsync(
 1265                    _currentUser.UserId ?? 0,
 1266                    AccionEnum.GenerarLiquidacion,
 1267                    ContextoEnum.Liquidaciones,
 1268                    entidad: "aguinaldo_baja",
 1269                    entidadId: creado.Id,
 1270                    detalle: new { Resultado = resultadoCalculo, Estado = estado });
 1271            }
 0272            catch
 273            {
 274                // No bloquear el proceso por fallos en auditoría
 0275            }
 1276        }
 277
 1278        return (true, resultados, null);
 2279    }
 280
 281    public async Task<IEnumerable<AguinaldoBajaDto>> ObtenerPorPeriodoAsync(int anio, int mes)
 282    {
 0283        if (anio <= 0 || mes < 1 || mes > 12)
 0284            return Enumerable.Empty<AguinaldoBajaDto>();
 285
 0286        var registros = await _aguinaldoBajaRepository.GetByPeriodoAsync((short)anio, (short)mes);
 0287        return registros.Select(AguinaldoBajaDto.FromEntity).ToList();
 0288    }
 289
 290    private async Task<IEnumerable<MovimientoLaboral>> ObtenerBajasParaPeriodoAsync(CalcularAguinaldoBajaRequest request
 291    {
 1292        return await _context.MovimientosLaborales
 1293            .Include(m => m.RelacionLaboral)
 1294                .ThenInclude(rl => rl.Persona)
 1295            .Include(m => m.RelacionLaboral)
 1296                .ThenInclude(rl => rl.Regimen)
 1297            .Include(m => m.RelacionLaboral)
 1298                .ThenInclude(rl => rl.Situacion)
 1299            .Include(m => m.RelacionLaboral)
 1300                .ThenInclude(rl => rl.MotivoBaja)
 1301            .Include(m => m.TipoMovimiento)
 1302            .Where(m => m.FechaMovimiento.Year == request.Anio
 1303                && m.FechaMovimiento.Month == request.Mes
 1304                && !m.TipoMovimiento.EsAlta)
 1305            .ToListAsync();
 1306    }
 307
 308    // D3: el cálculo de baja aplica en todos los meses EXCEPTO junio y diciembre,
 309    // que quedan cubiertos por el proceso semestral.
 310    private static bool EsMesProcesable(int mes)
 311    {
 2312        return mes != 6 && mes != 12;
 313    }
 314
 315    private static short CalcularSemestre(int mes)
 316    {
 1317        return mes <= 6 ? (short)1 : (short)2;
 318    }
 319
 320    private static short CalcularMesesComputados(CalcularAguinaldoBajaRequest request, DateTime fechaInicio)
 321    {
 322        // S1: comienza en diciembre del año anterior
 323        // S2: comienza en junio del año actual
 1324        var esPrimerSemestre = request.Mes <= 6;
 1325        var inicioSemestre = esPrimerSemestre
 1326            ? new DateTime(request.Anio - 1, 12, 1)
 1327            : new DateTime(request.Anio, 6, 1);
 328
 1329        var fechaInicioValida = fechaInicio > inicioSemestre ? fechaInicio : inicioSemestre;
 1330        var fechaFin = new DateTime(request.Anio, request.Mes, 1);
 331
 1332        if (fechaInicioValida > fechaFin)
 0333            return 0;
 334
 1335        return (short)((fechaFin.Year - fechaInicioValida.Year) * 12 + fechaFin.Month - fechaInicioValida.Month + 1);
 336    }
 337
 338    private async Task<decimal> ObtenerTotalHaberesGravadosAsync(CalcularAguinaldoBajaRequest request, long personaId)
 339    {
 1340        var esPrimerSemestre = request.Mes <= 6;
 341
 1342        if (esPrimerSemestre)
 343        {
 1344            return await _context.HistoricoLiquidaciones
 1345                .Where(h => h.PersonaId == personaId
 1346                    && ((h.Anio == request.Anio - 1 && h.Mes == 12)
 1347                        || (h.Anio == request.Anio && h.Mes >= 1 && h.Mes <= request.Mes)))
 1348                .SumAsync(h => (decimal?)h.HaberesGravados) ?? 0m;
 349        }
 350
 0351        return await _context.HistoricoLiquidaciones
 0352            .Where(h => h.PersonaId == personaId
 0353                && h.Anio == request.Anio
 0354                && h.Mes >= 6 && h.Mes <= request.Mes)
 0355            .SumAsync(h => (decimal?)h.HaberesGravados) ?? 0m;
 1356    }
 357
 358    private async Task<ParametroPeriodo?> ObtenerParametrosPeriodoAsync(CalcularAguinaldoBajaRequest request)
 359    {
 1360        return await _context.ParametrosPeriodo
 1361            .Where(p => p.Anio < request.Anio || (p.Anio == request.Anio && p.Mes <= request.Mes))
 1362            .OrderByDescending(p => p.Anio)
 1363            .ThenByDescending(p => p.Mes)
 1364            .FirstOrDefaultAsync();
 1365    }
 366
 367    private async Task<Dictionary<long, HashSet<(short Anio, short Mes)>>> CargarHistoricoSemestreAsync(
 368        CalcularAguinaldoBajaRequest request, List<long> personaIds)
 369    {
 0370        if (personaIds.Count == 0)
 0371            return new Dictionary<long, HashSet<(short Anio, short Mes)>>();
 372
 373        IQueryable<HistoricoLiquidacion> query;
 374
 0375        if (request.Mes <= 6)
 376        {
 377            // 1er semestre: Dic año anterior + Ene hasta mes de baja del año actual
 0378            var anioAnterior = (short)(request.Anio - 1);
 0379            var anioActual = (short)request.Anio;
 0380            var mesHasta = (short)request.Mes;
 0381            query = _context.HistoricoLiquidaciones
 0382                .Where(h => personaIds.Contains(h.PersonaId)
 0383                    && ((h.Anio == anioAnterior && h.Mes == 12)
 0384                        || (h.Anio == anioActual && h.Mes >= 1 && h.Mes <= mesHasta)));
 385        }
 386        else
 387        {
 388            // 2do semestre: Jun hasta mes de baja del año actual
 0389            var anioActual = (short)request.Anio;
 0390            var mesHasta = (short)request.Mes;
 0391            query = _context.HistoricoLiquidaciones
 0392                .Where(h => personaIds.Contains(h.PersonaId)
 0393                    && h.Anio == anioActual
 0394                    && h.Mes >= 6 && h.Mes <= mesHasta);
 395        }
 396
 0397        var registros = await query
 0398            .Select(h => new { h.PersonaId, h.Anio, h.Mes })
 0399            .ToListAsync();
 400
 0401        return registros
 0402            .GroupBy(h => h.PersonaId)
 0403            .ToDictionary(
 0404                g => g.Key,
 0405                g => g.Select(h => (h.Anio, h.Mes)).ToHashSet());
 0406    }
 407
 408    private static List<(short Anio, short Mes)> ObtenerMesesEsperados(
 409        CalcularAguinaldoBajaRequest request, DateTime fechaInicio)
 410    {
 0411        var esPrimerSemestre = request.Mes <= 6;
 0412        var inicioSemestre = esPrimerSemestre
 0413            ? new DateTime(request.Anio - 1, 12, 1)
 0414            : new DateTime(request.Anio, 6, 1);
 415
 0416        var fechaInicioValida = fechaInicio > inicioSemestre ? fechaInicio : inicioSemestre;
 0417        var fechaFin = new DateTime(request.Anio, request.Mes, 1);
 418
 0419        if (fechaInicioValida > fechaFin)
 0420            return new List<(short Anio, short Mes)>();
 421
 0422        var meses = new List<(short Anio, short Mes)>();
 0423        var current = new DateTime(fechaInicioValida.Year, fechaInicioValida.Month, 1);
 0424        while (current <= fechaFin)
 425        {
 0426            meses.Add(((short)current.Year, (short)current.Month));
 0427            current = current.AddMonths(1);
 428        }
 0429        return meses;
 430    }
 431
 0432    private static string FormatearMesAnio(short anio, short mes) => $"{mes:D2}/{anio:D4}";
 433}