< Summary

Information
Class: FAU.Logica.Services.LiquidacionService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/LiquidacionService.cs
Line coverage
85%
Covered lines: 462
Uncovered lines: 77
Coverable lines: 539
Total lines: 855
Line coverage: 85.7%
Branch coverage
79%
Covered branches: 148
Total branches: 186
Branch coverage: 79.5%
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/LiquidacionService.cs

#LineLine coverage
 1using FAU.DataAccess;
 2using FAU.DataAccess.Repositories;
 3using FAU.Entidades;
 4using FAU.Logica.Calculadores;
 5using FAU.Logica.Calculators;
 6using FAU.Logica.DTOs;
 7using Microsoft.EntityFrameworkCore;
 8
 9namespace FAU.Logica.Services;
 10
 11public class LiquidacionService : ILiquidacionService
 12{
 13    private const int CodigoIrpf = 517;
 14    private const int CodigoMontepioSubalterno = 550;
 15    private const int CodigoSanidad = 520;
 16    private const string CodigoFrl = "FRL";
 17
 18    // El OG de cada compensación viene de ConfigCompensacion.CatalogoItem
 19    // cargado via InsumosPeriodoLoader (TipoCompensacion → Configuraciones → CatalogoItem)
 20    private static int? ObtenerOGCompensacion(ItemLoteCompensacion comp)
 21    {
 222        var codigo = comp.LoteCompensacion?.TipoCompensacion?.Configuraciones
 223            .FirstOrDefault(c => c.CatalogoItem is not null)
 224            ?.CatalogoItem?.Codigo;
 225        return codigo;
 26    }
 27
 28    private readonly ApplicationDbContext _context;
 29    private readonly IAuditoriaService _auditoriaService;
 30    private readonly ICurrentUserContext _currentUser;
 31    private readonly IInsumosPeriodoLoader _insumosLoader;
 32    private readonly IProcesadorBeneficiosService _procesadorBeneficios;
 33    // Calculadoras de haberes base
 34    private readonly ICalculadorFactory _calculadorFactory;
 35    private readonly ICalcularDiferencia615Service _diferencia615Service;
 36    // Descuentos legales, IRPF y déficit
 37    private readonly IDescuentoPersonalRepository _descuentoPersonalRepository;
 38    private readonly IDependienteRepository _dependienteRepository;
 39    private readonly IDeficitCalculator _deficitCalculator;
 40    private readonly IDescuentosLegalesCalculator _descuentosLegalesCalculator;
 41    private readonly IIrpfService _irpfService;
 42
 3843    public LiquidacionService(
 3844        ApplicationDbContext context,
 3845        IAuditoriaService auditoriaService,
 3846        ICurrentUserContext currentUser,
 3847        IInsumosPeriodoLoader insumosLoader,
 3848        IProcesadorBeneficiosService procesadorBeneficios,
 3849        ICalculadorFactory calculadorFactory,
 3850        ICalcularDiferencia615Service diferencia615Service,
 3851        IDescuentoPersonalRepository descuentoPersonalRepository,
 3852        IDependienteRepository dependienteRepository,
 3853        IDeficitCalculator deficitCalculator,
 3854        IDescuentosLegalesCalculator descuentosLegalesCalculator,
 3855        IIrpfService irpfService)
 56    {
 3857        _context = context;
 3858        _auditoriaService = auditoriaService;
 3859        _currentUser = currentUser;
 3860        _insumosLoader = insumosLoader;
 3861        _procesadorBeneficios = procesadorBeneficios;
 3862        _calculadorFactory = calculadorFactory;
 3863        _diferencia615Service = diferencia615Service;
 3864        _descuentoPersonalRepository = descuentoPersonalRepository;
 3865        _dependienteRepository = dependienteRepository;
 3866        _deficitCalculator = deficitCalculator;
 3867        _descuentosLegalesCalculator = descuentosLegalesCalculator;
 3868        _irpfService = irpfService;
 3869    }
 70
 71    /// <summary>
 72    /// Genera la liquidación para el período. La versión se determina automáticamente:
 73    /// max_version_existente + 1, lo que permite recalcular después de una reapertura
 74    /// sin perder el historial de versiones anteriores.
 75    /// </summary>
 76    public async Task<(bool Exito, string? Error, int Version)> GenerarLiquidacionAsync(long periodoId)
 77    {
 3478        var periodo = await _context.PeriodosLiquidacion
 3479            .AsNoTracking()
 3480            .FirstOrDefaultAsync(p => p.Id == periodoId && p.Activo);
 81
 3482        if (periodo is null)
 183            return (false, "Período de liquidación no encontrado o no está activo", 0);
 84
 85        // Versión = max existente + 1 (1 si es la primera vez)
 3386        var maxVersion = await _context.LiquidacionItems
 3387            .Where(li => li.PeriodoId == periodoId)
 3388            .MaxAsync(li => (int?)li.Version) ?? 0;
 3389        var version = maxVersion + 1;
 90
 91        InsumosPeriodo insumos;
 92        try
 93        {
 3394            insumos = await _insumosLoader.CargarAsync(periodoId);
 3395        }
 096        catch (InvalidOperationException ex) when (ex.Message.Contains("arámetro"))
 97        {
 098            return (false, $"No se encontraron parámetros para el período {periodo.Anio}/{periodo.Mes}", 0);
 99        }
 100
 101        // Usamos el mismo patrón de Join que funciona con el HasConversion de Estado enum→string
 33102        var rlRaw = await _context.RelacionesLaborales
 33103            .AsNoTracking()
 33104            .Where(r => r.Estado == EstadoRelacion.Activo)
 33105            .Join(
 33106                _context.Personas.AsNoTracking(),
 33107                r => r.PersonaId,
 33108                p => p.Id,
 33109                (r, p) => new { RL = r, Persona = p })
 33110            .ToListAsync();
 111
 33112        if (!rlRaw.Any())
 2113            return (false, "No se encontraron relaciones laborales activas para generar la liquidación", version);
 114
 115        // Cargar navigation properties necesarias para el pipeline de calculadoras
 63116        var situacionIds = rlRaw.Select(x => x.RL.SituacionId).Distinct().ToHashSet();
 63117        var regimenIds   = rlRaw.Select(x => x.RL.RegimenId).Distinct().ToHashSet();
 118
 31119        var situacionesDict = await _context.Situaciones.AsNoTracking()
 31120            .Where(s => situacionIds.Contains(s.Id))
 47121            .ToDictionaryAsync(s => s.Id);
 122
 31123        var regimenesDict = await _context.Regimenes.AsNoTracking()
 31124            .Where(r => regimenIds.Contains(r.Id))
 49125            .ToDictionaryAsync(r => r.Id);
 126
 127        // Materializar RelacionLaboral con navigation properties resueltas en memoria
 31128        var relacionesLaborales = rlRaw.Select(x =>
 31129        {
 32130            x.RL.Persona = x.Persona;
 32131            x.RL.Situacion = situacionesDict.GetValueOrDefault(x.RL.SituacionId)!;
 32132            x.RL.Regimen   = regimenesDict.GetValueOrDefault(x.RL.RegimenId)!;
 32133            return x.RL;
 31134        }).ToList();
 135
 31136        var primerDiaPeriodo = new DateOnly(periodo.Anio, periodo.Mes, 1);
 31137        var itemsToSave = new List<LiquidacionItem>();
 31138        var incidenciasToSave = new List<IncidenciaCalculo>();
 31139        var estadosPersonal = new Dictionary<string, string>();
 140
 141        // Pre-load historico de primas únicas en una sola consulta
 63142        var cedulasPersonas = relacionesLaborales.Select(r => r.Persona.Cedula).ToHashSet();
 31143        var itemsHistoricosGlobal = await _context.LiquidacionItems
 31144            .AsNoTracking()
 31145            .Where(li => cedulasPersonas.Contains(li.Cedula) &&
 31146                         (li.CodigoConcepto == "340" || li.CodigoConcepto == "350" || li.CodigoConcepto == "370"))
 31147            .ToListAsync();
 148
 149        // Índice de catálogo por código (usado por helpers de aportes legales)
 31150        var catalogoPorCodigo = insumos.CatalogoItems
 223151            .GroupBy(i => i.Codigo)
 423152            .ToDictionary(g => g.Key, g => g.First());
 153
 126154        foreach (var rl in relacionesLaborales)
 155        {
 32156            var cedula = rl.Persona.Cedula;
 157            try
 158            {
 32159                var acumuladores = new Acumuladores();
 32160                var itemsHaberes = new List<ItemCalculado>();
 32161                var itemsBeneficios = new List<ItemBeneficioCalculado>();
 162
 163                // --- Paso 1-3: Calculador de haberes base ---
 32164                var calculador = rl.Situacion?.Calculador is not null
 32165                    ? _calculadorFactory.Resolver(rl.Situacion.Calculador)
 32166                    : null;
 167
 32168                if (calculador is not null)
 169                {
 170                    // Prorrateo por novedad 'dias_eximidos': factor = dias/30.
 171                    // Endurecido: dias_afectados debe estar en [1,30]. Fuera de rango (incl. 0)
 172                    // NO se prorratea (factor 1) y se registra incidencia, para no anular
 173                    // indebidamente los haberes ni romper el cálculo de aportes/IRPF.
 16174                    var novedadEximidos = insumos.ObtenerNovedadEximidos(rl.PersonaId);
 16175                    var factorProrrateo = 1m;
 16176                    if (novedadEximidos is not null)
 177                    {
 3178                        var diasAfectados = novedadEximidos.DiasAfectados;
 3179                        if (diasAfectados is < 1 or > 30)
 180                        {
 1181                            incidenciasToSave.Add(new IncidenciaCalculo
 1182                            {
 1183                                PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 1184                                Tipo = TipoIncidenciaCalculo.CalculoSimplificado,
 1185                                Descripcion = $"Novedad 'dias_eximidos' con dias_afectados fuera de rango [1,30]: {diasA
 1186                                CreadoEn = DateTime.UtcNow
 1187                            });
 188                        }
 2189                        else if (diasAfectados < 30)
 190                        {
 2191                            factorProrrateo = diasAfectados / 30m;
 192                        }
 193                    }
 194
 16195                    var resultadoCalculo = await calculador.CalcularAsync(rl, insumos, acumuladores);
 196
 16197                    if (factorProrrateo < 1m)
 198                    {
 24199                        foreach (var item in resultadoCalculo.Items)
 10200                            item.Importe = Math.Round(item.Importe * factorProrrateo, 2);
 201
 2202                        acumuladores.Reiniciar();
 24203                        foreach (var item in resultadoCalculo.Items)
 204                        {
 58205                            var ci = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == item.CodigoConcepto);
 19206                            if (ci is not null) acumuladores.AgregarItem(item.Importe, ci);
 207                        }
 208                    }
 209
 16210                    itemsHaberes.AddRange(resultadoCalculo.Items);
 211
 32212                    foreach (var inc in resultadoCalculo.Incidencias)
 0213                        incidenciasToSave.Add(new IncidenciaCalculo
 0214                        {
 0215                            PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 0216                            Tipo = TipoIncidenciaCalculo.CalculoSimplificado,
 0217                            Descripcion = inc, CreadoEn = DateTime.UtcNow
 0218                        });
 219                }
 220                else
 221                {
 16222                    incidenciasToSave.Add(new IncidenciaCalculo
 16223                    {
 16224                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 16225                        Tipo = TipoIncidenciaCalculo.ErrorCalculo,
 16226                        Descripcion = $"Calculador no registrado: {rl.Situacion?.Calculador}",
 16227                        CreadoEn = DateTime.UtcNow
 16228                    });
 229                }
 230
 231                // --- Paso 4: Beneficios sociales ---
 32232                var beneficios = insumos.ObtenerBeneficios(rl.PersonaId);
 32233                var itemsHistoricoPersona = itemsHistoricosGlobal.Where(h => h.Cedula == cedula).ToList();
 32234                var sueldoBasePersona = CalcularSueldoBaseParaTraslado(insumos, rl.GradoId);
 32235                var resultadoBeneficios = _procesadorBeneficios.ProcesarBeneficios(
 32236                    rl.PersonaId, beneficios, insumos.ParametroPeriodo, insumos.CatalogoItems,
 32237                    itemsHistoricoPersona, primerDiaPeriodo,
 32238                    insumos.BeneficiariosPorBeneficio, insumos.InstructivoAsigFamiliar,
 32239                    insumos.InstructivoPsf, insumos.TablaPorHogar,
 32240                    sueldoBaseParaTraslado: sueldoBasePersona);
 32241                itemsBeneficios.AddRange(resultadoBeneficios.Items);
 242
 64243                foreach (var inc in resultadoBeneficios.Incidencias)
 0244                    incidenciasToSave.Add(new IncidenciaCalculo
 0245                    {
 0246                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 0247                        Tipo = inc.Tipo, Descripcion = inc.Descripcion, CreadoEn = DateTime.UtcNow
 0248                    });
 249
 250                // --- Paso 5: Compensaciones por lote ---
 251                // El OG viene de ConfigCompensacion.CatalogoItem (no hardcodeado)
 32252                var compensaciones = insumos.ObtenerCompensaciones(rl.PersonaId);
 68253                foreach (var comp in compensaciones)
 254                {
 2255                    var ogCode = ObtenerOGCompensacion(comp);
 2256                    if (ogCode is null) continue;   // sin config de OG → saltear
 257
 2258                    var monto = comp.Monto ?? 0m;
 2259                    if (monto <= 0) continue;
 260
 18261                    var ci = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == ogCode.Value);
 2262                    itemsHaberes.Add(new ItemCalculado
 2263                    {
 2264                        CodigoConcepto = ogCode.Value,
 2265                        NombreConcepto = ci?.Nombre ?? comp.LoteCompensacion?.TipoCompensacion?.Nombre ?? "",
 2266                        Importe = monto
 2267                    });
 4268                    if (ci is not null) acumuladores.AgregarItem(monto, ci);
 269                }
 270
 271                // --- Paso 6: Diferencia por ascenso 042.615 ---
 32272                var diffAscenso = insumos.ObtenerDiferenciaAscenso(rl.PersonaId);
 32273                if (diffAscenso is not null && diffAscenso.Monto > 0)
 274                {
 275                    const int Cod615 = 42615;
 9276                    var ci615 = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == Cod615);
 1277                    itemsHaberes.Add(new ItemCalculado
 1278                    {
 1279                        CodigoConcepto = Cod615,
 1280                        NombreConcepto = ci615?.Nombre ?? "Diferencia por Ascenso (042.615)",
 1281                        Importe = diffAscenso.Monto
 1282                    });
 2283                    if (ci615 is not null) acumuladores.AgregarItem(diffAscenso.Monto, ci615);
 284                }
 285
 286                // --- Paso 7: Aportes legales (Montepío, FONASA, FRL) ---
 287                // baseSdlAportes = todos haberes positivos + beneficios gravados
 288                // nominalBruto   = todos haberes positivos + todos beneficios positivos
 289                // Se suma directamente desde itemsHaberes porque contribuye_w_nominal
 290                // puede no estar configurado en el catálogo (en ese caso WNominal=0).
 204291                var totalHaberesPositivos = itemsHaberes.Where(i => i.Importe > 0m).Sum(i => i.Importe);
 32292                var totalBeneficiosGravados = resultadoBeneficios.Items
 23293                    .Where(i => i.Importe > 0m
 23294                                && catalogoPorCodigo.TryGetValue(i.CodigoConcepto, out var ciBen)
 23295                                && (ciBen.EsGravado || ciBen.ContribuyeWNominal))
 48296                    .Sum(i => i.Importe);
 32297                var baseSdlAportes = totalHaberesPositivos + totalBeneficiosGravados;
 32298                var nominalBruto = totalHaberesPositivos
 74299                    + resultadoBeneficios.Items.Where(i => i.Importe > 0m).Sum(i => i.Importe);
 300
 32301                var esMilitar = !CalculadoresCiviles.Contains(rl.Situacion?.Calculador ?? string.Empty);
 32302                var aportes = await CalcularAportesLegalesAsync(rl.RegimenId, baseSdlAportes, insumos, esMilitar);
 32303                var itemsAportesLegales = CrearItemsAportesLegales(periodoId, cedula, version, aportes, catalogoPorCodig
 32304                itemsToSave.AddRange(itemsAportesLegales);
 305
 306                // --- Paso 8: IRPF ---
 307                // Eliminar registro anterior para que CalcularYPersistirAsync siempre inserte limpio
 32308                var irpfAnterior = await _context.Set<IrpfMensual>()
 32309                    .FirstOrDefaultAsync(i => i.LiquidacionId == periodoId && i.PersonaId == rl.PersonaId);
 32310                if (irpfAnterior is not null)
 311                {
 0312                    var detallesAnteriores = await _context.Set<IrpfDetalleFranjas>()
 0313                        .Where(d => d.IrpfMensualId == irpfAnterior.Id)
 0314                        .ToListAsync();
 0315                    _context.Set<IrpfDetalleFranjas>().RemoveRange(detallesAnteriores);
 0316                    _context.Set<IrpfMensual>().Remove(irpfAnterior);
 0317                    await _context.SaveChangesAsync();
 318                }
 319
 32320                var resultadoIrpf = await CalcularIrpfAsync(
 32321                    periodoId, rl.PersonaId, cedula, version,
 32322                    nominalBruto, aportes, insumos, catalogoPorCodigo);
 323
 324                // Descartar entidades IrpfMensual trackeadas si la persistencia falló,
 325                // para que no se reintentan en el SaveChangesAsync final.
 64326                foreach (var entry in _context.ChangeTracker.Entries<IrpfMensual>()
 0327                             .Where(e => e.State == Microsoft.EntityFrameworkCore.EntityState.Added)
 32328                             .ToList())
 0329                    entry.State = Microsoft.EntityFrameworkCore.EntityState.Detached;
 330
 32331                if (resultadoIrpf.Item is not null && resultadoIrpf.TotalIrpf != 0)
 3332                    itemsToSave.Add(resultadoIrpf.Item);
 333                else
 334                {
 335                    // Diagnóstico: registrar siempre el resultado del cálculo IRPF para debugging
 29336                    var tieneCatalogo = catalogoPorCodigo.ContainsKey(CodigoIrpf);
 29337                    incidenciasToSave.Add(new IncidenciaCalculo
 29338                    {
 29339                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 29340                        Tipo = TipoIncidenciaCalculo.CalculoSimplificado,
 29341                        Descripcion = $"IRPF diag: TotalIrpf={resultadoIrpf.TotalIrpf:F2} ItemNull={resultadoIrpf.Item i
 29342                        CreadoEn = DateTime.UtcNow
 29343                    });
 344                }
 345
 346                // --- Paso 9: Descuentos personales con déficit ---
 347                // Excluir códigos ya emitidos como aportes para no contarlos dos veces en el déficit
 32348                var codigosAportesEmitidos = itemsAportesLegales
 78349                    .Where(i => !string.IsNullOrWhiteSpace(i.CodigoConcepto))
 78350                    .Select(i => i.CodigoConcepto)
 32351                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
 32352                var descuentosLegalesEnBeneficios = resultadoBeneficios.Items
 23353                    .Where(i => i.Importe < 0m
 23354                                && catalogoPorCodigo.TryGetValue(i.CodigoConcepto, out var ci)
 23355                                && string.Equals(ci.Tipo, "descuento_legal", StringComparison.OrdinalIgnoreCase)
 23356                                && !codigosAportesEmitidos.Contains(i.CodigoConcepto.ToString()))
 35357                    .Sum(i => Math.Abs(i.Importe));
 32358                var totalDescuentosLegales = aportes.Total + resultadoIrpf.TotalIrpf + descuentosLegalesEnBeneficios;
 32359                var fechaPeriodo = new DateTime(insumos.ParametroPeriodo.Anio, insumos.ParametroPeriodo.Mes, 1);
 32360                var itemsDescuentosPersonales = await CrearItemsDescuentosPersonalesAsync(
 32361                    periodoId, rl.PersonaId, cedula, version,
 32362                    nominalBruto, totalDescuentosLegales, fechaPeriodo);
 32363                itemsToSave.AddRange(itemsDescuentosPersonales);
 364
 365                // --- Paso 10: Estado ---
 32366                var tieneError = incidenciasToSave.Any(i =>
 66367                    i.PersonaId == rl.PersonaId && i.Tipo == TipoIncidenciaCalculo.ErrorCalculo);
 32368                var liquido = nominalBruto - totalDescuentosLegales
 42369                    - itemsDescuentosPersonales.Sum(i => Math.Abs(i.Importe));
 32370                var estado = tieneError ? "ERROR" : (liquido < 0 ? "REVISAR" : "CALCULADO");
 32371                estadosPersonal[cedula] = estado;
 372
 373                // --- Paso 11: Persistir ítems de haberes y beneficios ---
 236374                foreach (var item in itemsHaberes)
 375                {
 86376                    itemsToSave.Add(new LiquidacionItem
 86377                    {
 86378                        PeriodoId = periodoId,
 86379                        Cedula = cedula,
 86380                        Version = version,
 86381                        CodigoConcepto = item.CodigoConcepto.ToString(),
 86382                        NombreConcepto = item.NombreConcepto,
 86383                        Importe = item.Importe,
 86384                        RubroContable = string.Empty,
 86385                        Rubro083 = false,
 86386                        FechaCreacion = DateTime.UtcNow
 86387                    });
 388                }
 110389                foreach (var item in itemsBeneficios)
 390                {
 23391                    itemsToSave.Add(new LiquidacionItem
 23392                    {
 23393                        PeriodoId = periodoId,
 23394                        Cedula = cedula,
 23395                        Version = version,
 23396                        CodigoConcepto = item.CodigoConcepto.ToString(),
 23397                        NombreConcepto = item.NombreConcepto,
 23398                        Importe = item.Importe,
 23399                        RubroContable = item.RubroContable,
 23400                        Rubro083 = false,
 23401                        ClaveEvento = item.ClaveEvento,
 23402                        FechaCreacion = DateTime.UtcNow
 23403                    });
 404                }
 32405            }
 0406            catch (Exception ex)
 407            {
 0408                estadosPersonal[cedula] = "ERROR";
 0409                incidenciasToSave.Add(new IncidenciaCalculo
 0410                {
 0411                    PeriodoId = periodoId,
 0412                    Version = version,
 0413                    PersonaId = rl.PersonaId,
 0414                    Tipo = TipoIncidenciaCalculo.ErrorCalculo,
 0415                    Descripcion = $"Error en pipeline de liquidación: {ex.Message}",
 0416                    CreadoEn = DateTime.UtcNow
 0417                });
 0418            }
 32419        }
 420
 63421        var estados = relacionesLaborales.Select(rl => new LiquidacionEstadoPersonal
 63422        {
 63423            PeriodoId = periodoId,
 63424            Cedula = rl.Persona.Cedula,
 63425            Version = version,
 63426            Estado = estadosPersonal.TryGetValue(rl.Persona.Cedula, out var e) ? e : "PENDIENTE",
 63427            FechaActualizacion = DateTime.UtcNow
 63428        }).ToList();
 429
 31430        await _context.LiquidacionEstadosPersonal.AddRangeAsync(estados);
 431
 31432        if (itemsToSave.Count > 0)
 31433            await _context.LiquidacionItems.AddRangeAsync(itemsToSave);
 434
 31435        if (incidenciasToSave.Count > 0)
 31436            await _context.IncidenciasCalculo.AddRangeAsync(incidenciasToSave);
 437
 231438        var totalImporte = itemsToSave.Sum(i => i.Importe);
 31439        var resumen = new LiquidacionResumen
 31440        {
 31441            PeriodoId = periodoId,
 31442            Version = version,
 31443            TotalImporte = totalImporte,
 31444            Rubro083 = false,
 31445            Descripcion = "Liquidación generada"
 31446        };
 31447        await _context.LiquidacionResumenes.AddAsync(resumen);
 448
 31449        await _context.SaveChangesAsync();
 450
 31451        await _auditoriaService.LogAuditoriaAsync(
 31452            _currentUser.UserId ?? 0,
 31453            AccionEnum.GenerarLiquidacion,
 31454            ContextoEnum.Liquidaciones,
 31455            _currentUser.Host,
 31456            entidad: nameof(LiquidacionEstadoPersonal),
 31457            entidadId: periodoId,
 31458            detalle: new { periodoId, version, registros = estados.Count, items = itemsToSave.Count, incidencias = incid
 459
 31460        return (true, null, version);
 34461    }
 462
 463    private async Task<int> ObtenerVersionMaximaAsync(long periodoId)
 464    {
 1465        return await _context.LiquidacionItems
 1466            .AsNoTracking()
 1467            .Where(i => i.PeriodoId == periodoId)
 1468            .MaxAsync(i => (int?)i.Version) ?? 0;
 1469    }
 470
 471    public async Task<IEnumerable<CalculoEstadoDto>> ObtenerEstadosAsync(long periodoId, int? version = null)
 472    {
 2473        var v = version ?? await ObtenerVersionMaximaAsync(periodoId);
 2474        return await _context.LiquidacionEstadosPersonal
 2475            .AsNoTracking()
 2476            .Where(e => e.PeriodoId == periodoId && e.Version == v)
 2477            .Select(e => new CalculoEstadoDto
 2478            {
 2479                Cedula = e.Cedula,
 2480                Version = e.Version,
 2481                Estado = e.Estado,
 2482                MensajeError = e.MensajeError
 2483            })
 2484            .ToListAsync();
 2485    }
 486
 487    public async Task<IEnumerable<CalculoItemDto>> ObtenerItemsAsync(long periodoId, int? version = null)
 488    {
 0489        var v = version ?? await ObtenerVersionMaximaAsync(periodoId);
 0490        return await _context.LiquidacionItems
 0491            .AsNoTracking()
 0492            .Where(i => i.PeriodoId == periodoId && i.Version == v)
 0493            .Select(i => new CalculoItemDto
 0494            {
 0495                Cedula = i.Cedula,
 0496                Version = i.Version,
 0497                CodigoConcepto = i.CodigoConcepto,
 0498                NombreConcepto = i.NombreConcepto,
 0499                Importe = i.Importe,
 0500                RubroContable = i.RubroContable,
 0501                ObjetoGasto = i.ObjetoGasto
 0502            })
 0503            .ToListAsync();
 0504    }
 505
 506    public async Task<IEnumerable<LiquidacionResumenDto>> ObtenerResumenAsync(long periodoId, int? version = null)
 507    {
 0508        var v = version ?? await ObtenerVersionMaximaAsync(periodoId);
 0509        return await _context.LiquidacionResumenes
 0510            .AsNoTracking()
 0511            .Where(r => r.PeriodoId == periodoId && r.Version == v)
 0512            .Select(r => new LiquidacionResumenDto
 0513            {
 0514                Version = r.Version,
 0515                ProgramaId = r.ProgramaId,
 0516                RegimenId = r.RegimenId,
 0517                Rubro083 = r.Rubro083,
 0518                TotalImporte = r.TotalImporte,
 0519                Descripcion = r.Descripcion
 0520            })
 0521            .ToListAsync();
 0522    }
 523
 524    public async Task<IEnumerable<int>> ObtenerVersionesAsync(long periodoId)
 525    {
 2526        return await _context.LiquidacionItems
 2527            .AsNoTracking()
 2528            .Where(i => i.PeriodoId == periodoId)
 2529            .Select(i => i.Version)
 2530            .Distinct()
 2531            .OrderByDescending(v => v)
 2532            .ToListAsync();
 2533    }
 534
 535    // Civiles aportan FONASA por tasa única; los militares aportan Sanidad Militar por tramos.
 1536    private static readonly HashSet<string> CalculadoresCiviles =
 1537        new(StringComparer.OrdinalIgnoreCase) { "Sit70Calculator", "CivilCalculator" };
 538
 539    private async Task<AportesLegales> CalcularAportesLegalesAsync(
 540        long regimenId, decimal baseSdl, InsumosPeriodo insumos, bool esMilitar)
 541    {
 32542        var regimen = await _context.Regimenes.AsNoTracking().FirstOrDefaultAsync(r => r.Id == regimenId);
 32543        var tasaMontepio = regimen is not null && regimen.EsLeyVieja
 32544            ? insumos.ParametroPeriodo.TasaMontepioLeyVieja
 32545            : insumos.ParametroPeriodo.TasaMontepioLeyNueva;
 546
 32547        var montepio = _descuentosLegalesCalculator.CalcularMontepio(baseSdl, tasaMontepio);
 32548        var frl = _descuentosLegalesCalculator.CalcularFrl(baseSdl, insumos.ParametroPeriodo.TasaFrl);
 549
 550        // Sanidad Militar (Art. 94 Ley 18.834) reemplaza a FONASA para militares.
 551        // Base = misma que el montepío (baseSdl). Civiles mantienen FONASA por tasa única.
 32552        var tramo = esMilitar ? insumos.ObtenerTramoSanidad(baseSdl) : null;
 32553        if (tramo is not null)
 554        {
 0555            var sanidad = _descuentosLegalesCalculator.CalcularSanidad(baseSdl, tramo.Porcentaje);
 0556            return new AportesLegales(montepio, sanidad, frl, tramo.CodigoRetencion);
 557        }
 558
 32559        var fonasa = _descuentosLegalesCalculator.CalcularFonasa(baseSdl, insumos.ParametroPeriodo.TasaFonasa);
 32560        return new AportesLegales(montepio, fonasa, frl, SanidadCodigo: null);
 32561    }
 562
 563    private async Task<ResultadoIrpfLiquidacion> CalcularIrpfAsync(
 564        long periodoId,
 565        long personaId,
 566        string cedula,
 567        int version,
 568        decimal nominalBruto,
 569        AportesLegales aportes,
 570        InsumosPeriodo insumos,
 571        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo)
 572    {
 32573        var request = new CalcularIrpfRequest
 32574        {
 32575            IngresoGravados = nominalBruto,
 32576            Montepio = aportes.Montepio,
 32577            Fonasa = aportes.Fonasa,
 32578            Frl = aportes.Frl,
 32579            Deducciones = await CrearDeduccionesIrpfAsync(personaId),
 32580            Parametros = new ParametrosIrpfDto
 32581            {
 32582                BpcMensual = insumos.ParametroPeriodo.BpcAnual / 12m,
 32583                UmbralDeduccionBpc = insumos.ParametroPeriodo.UmbralDeduccionBpc,
 32584                TasaDeduccionAlta = insumos.ParametroPeriodo.TasaDeduccionAlta,
 32585                TasaDeduccionBaja = insumos.ParametroPeriodo.TasaDeduccionBaja
 32586            },
 32587            Franjas = insumos.FranjasIrpf
 75588                .Select(f => new FranjaIrpfCalculoDto
 75589                {
 75590                    FranjaId = f.Id,
 75591                    NumeroFranja = f.NumeroFranja,
 75592                    DesdeBpc = f.DesdeBpc,
 75593                    HastaBpc = f.HastaBpc,
 75594                    Tasa = f.Tasa
 75595                })
 32596                .ToList()
 32597        };
 598
 32599        var (exito, idIrpf, _) = await _irpfService.CalcularYPersistirAsync(periodoId, personaId, request);
 32600        if (!exito || !idIrpf.HasValue)
 28601            return new ResultadoIrpfLiquidacion(0m, null);
 602
 4603        var irpf = await _irpfService.ObtenerPorIdAsync(idIrpf.Value);
 4604        if (irpf is null)
 0605            return new ResultadoIrpfLiquidacion(0m, null);
 606
 4607        var item = catalogoPorCodigo.TryGetValue(CodigoIrpf, out var catalogoIrpf)
 4608            ? CrearItemLiquidacion(periodoId, cedula, version, catalogoIrpf, -irpf.TotalIrpf)
 4609            : null;
 610
 4611        return new ResultadoIrpfLiquidacion(irpf.TotalIrpf, item);
 32612    }
 613
 614    private async Task<List<DeduccionDto>> CrearDeduccionesIrpfAsync(long personaId)
 615    {
 32616        var dependientes = await _dependienteRepository.GetDependientesByPersonaAsync(personaId);
 617
 32618        return dependientes
 3619            .Where(d => d.Activo)
 32620            .SelectMany(d =>
 32621            {
 3622                if (string.Equals(d.Tipo, "conyuge_sin_ingresos", StringComparison.OrdinalIgnoreCase))
 1623                    return [new DeduccionDto { Tipo = "conyuge_sin_ingresos", Cantidad = 1 }];
 32624
 2625                if (string.Equals(d.Tipo, "hijo", StringComparison.OrdinalIgnoreCase))
 32626                {
 2627                    var tipoHijo = d.Discapacitado ? "hijo_discapacidad" : "hijo_menor";
 2628                    return [new DeduccionDto { Tipo = tipoHijo, Cantidad = 1 }];
 32629                }
 32630
 0631                return Array.Empty<DeduccionDto>();
 32632            })
 32633            .ToList();
 32634    }
 635
 636    private async Task<IReadOnlyList<LiquidacionItem>> CrearItemsDescuentosPersonalesAsync(
 637        long periodoId,
 638        long personaId,
 639        string cedula,
 640        int version,
 641        decimal nominalBruto,
 642        decimal descuentosLegales,
 643        DateTime fechaPeriodo)
 644    {
 32645        var descuentosConfirmados = (await _descuentoPersonalRepository
 32646                .GetConfirmadosPorPersonaAsync(periodoId, personaId))
 10647            .Where(d => d.CatalogoItem is not null)
 32648            // Vigencia (ej. CUOTA_ING): se aplica solo si no venció antes del período
 10649            .Where(d => d.FechaHasta is null || d.FechaHasta >= fechaPeriodo)
 32650            .ToList();
 651
 32652        if (descuentosConfirmados.Count == 0)
 23653            return Array.Empty<LiquidacionItem>();
 654
 655        // Filtrar descuentos sin acreedor asignado (datos previos a la migración de acreedor_id)
 9656        var descuentosConAcreedor = descuentosConfirmados
 10657            .Where(d => d.AcreedorId is not null && d.Acreedor is not null)
 9658            .ToList();
 659
 9660        if (descuentosConAcreedor.Count == 0)
 0661            return Array.Empty<LiquidacionItem>();
 662
 9663        var entradasDeficit = descuentosConAcreedor
 10664            .Select(d => CrearEntradaDeficit(d, personaId))
 9665            .ToList();
 666
 9667        var resultadosDeficit = await _deficitCalculator.AplicarAsync(
 9668            nominalBruto,
 9669            descuentosLegales,
 9670            entradasDeficit);
 671
 19672        var descuentosPorId = descuentosConAcreedor.ToDictionary(d => d.Id);
 9673        var items = new List<LiquidacionItem>();
 674
 38675        foreach (var resultadoDeficit in resultadosDeficit)
 676        {
 10677            if (resultadoDeficit.DescuentoPeriodoId is null || resultadoDeficit.MontoAplicado <= 0m)
 678                continue;
 679
 10680            if (!descuentosPorId.TryGetValue(resultadoDeficit.DescuentoPeriodoId.Value, out var descuento))
 681                continue;
 682
 10683            items.Add(CrearItemLiquidacion(
 10684                periodoId,
 10685                cedula,
 10686                version,
 10687                descuento.CatalogoItem,
 10688                -resultadoDeficit.MontoAplicado));
 689        }
 690
 9691        return items;
 32692    }
 693
 694    private static DescuentoPersonalInput CrearEntradaDeficit(DescuentoPersonalPeriodo descuento, long personaId)
 695    {
 10696        if (descuento.AcreedorId is null)
 0697            throw new InvalidOperationException($"El descuento personal {descuento.Id} no tiene acreedor asignado.");
 698
 10699        if (descuento.Acreedor is null)
 0700            throw new InvalidOperationException($"El descuento personal {descuento.Id} no tiene acreedor cargado.");
 701
 10702        return new DescuentoPersonalInput(
 10703            descuento.Id,
 10704            personaId,
 10705            descuento.CatalogoItem.Codigo,
 10706            descuento.Importe,
 10707            descuento.AcreedorId.Value,
 10708            descuento.Acreedor.CategoriaDeficit,
 10709            descuento.Acreedor.OrdenLegalDeficit,
 10710            descuento.FechaComunicacion ?? descuento.FechaCreacion);
 711    }
 712
 713    private static IReadOnlyList<LiquidacionItem> CrearItemsAportesLegales(
 714        long periodoId,
 715        string cedula,
 716        int version,
 717        AportesLegales aportes,
 718        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo)
 719    {
 32720        var items = new List<LiquidacionItem>();
 721
 32722        AgregarItemAporteLegal(
 32723            items, periodoId, cedula, version, aportes.Montepio,
 32724            BuscarCatalogoItem(catalogoPorCodigo, CodigoMontepioSubalterno, 545, 555),
 32725            codigoFallback: CodigoMontepioSubalterno.ToString(),
 32726            nombreFallback: "MONTEPIO",
 32727            rubroFallback: "5.04");
 728
 729        // Salud: Sanidad Militar por tramos (código 5010/5011/5012) o FONASA por tasa única.
 32730        if (aportes.SanidadCodigo is int sanidadCodigo)
 731        {
 0732            AgregarItemAporteLegal(
 0733                items, periodoId, cedula, version, aportes.Fonasa,
 0734                BuscarCatalogoItem(catalogoPorCodigo, sanidadCodigo),
 0735                codigoFallback: sanidadCodigo.ToString(),
 0736                nombreFallback: "SANIDAD",
 0737                rubroFallback: $"5.{sanidadCodigo % 100:D2}");
 738        }
 739        else
 740        {
 32741            AgregarItemAporteLegal(
 32742                items, periodoId, cedula, version, aportes.Fonasa,
 32743                BuscarCatalogoItem(catalogoPorCodigo, CodigoSanidad, 521, 522, 523, 524, 525, 526, 527, 528),
 32744                codigoFallback: CodigoSanidad.ToString(),
 32745                nombreFallback: "FONASA",
 32746                rubroFallback: "5.010");
 747        }
 748
 32749        AgregarItemAporteLegal(
 32750            items, periodoId, cedula, version, aportes.Frl,
 32751            null,
 32752            codigoFallback: CodigoFrl,
 32753            nombreFallback: "FRL",
 32754            rubroFallback: "000.000");
 755
 32756        return items;
 757    }
 758
 759    private static void AgregarItemAporteLegal(
 760        ICollection<LiquidacionItem> items,
 761        long periodoId,
 762        string cedula,
 763        int version,
 764        decimal importe,
 765        CatalogoItem? catalogoItem,
 766        string codigoFallback,
 767        string nombreFallback,
 768        string rubroFallback)
 769    {
 96770        if (importe <= 0m)
 18771            return;
 772
 78773        if (catalogoItem is not null)
 774        {
 7775            items.Add(CrearItemLiquidacion(periodoId, cedula, version, catalogoItem, -importe));
 7776            return;
 777        }
 778
 71779        items.Add(CrearItemLiquidacion(periodoId, cedula, version, codigoFallback, nombreFallback, -importe, rubroFallba
 71780    }
 781
 782    private static CatalogoItem? BuscarCatalogoItem(
 783        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo,
 784        params int[] codigos)
 785    {
 833786        foreach (var codigo in codigos)
 787        {
 356788            if (catalogoPorCodigo.TryGetValue(codigo, out var catalogoItem))
 7789                return catalogoItem;
 790        }
 791
 57792        return null;
 793    }
 794
 795    private static LiquidacionItem CrearItemLiquidacion(
 796        long periodoId,
 797        string cedula,
 798        int version,
 799        CatalogoItem catalogoItem,
 800        decimal importe)
 801    {
 20802        return CrearItemLiquidacion(
 20803            periodoId, cedula, version,
 20804            catalogoItem.Codigo.ToString(),
 20805            catalogoItem.Nombre,
 20806            importe,
 20807            catalogoItem.ClasificacionSiif ?? "000.000");
 808    }
 809
 810    private static LiquidacionItem CrearItemLiquidacion(
 811        long periodoId,
 812        string cedula,
 813        int version,
 814        string codigoConcepto,
 815        string nombreConcepto,
 816        decimal importe,
 817        string rubroContable)
 818    {
 91819        return new LiquidacionItem
 91820        {
 91821            PeriodoId = periodoId,
 91822            Cedula = cedula,
 91823            Version = version,
 91824            CodigoConcepto = codigoConcepto,
 91825            NombreConcepto = nombreConcepto,
 91826            Importe = importe,
 91827            RubroContable = rubroContable,
 91828            Rubro083 = false,
 91829            FechaCreacion = DateTime.UtcNow
 91830        };
 831    }
 832
 833    private static decimal CalcularSueldoBaseParaTraslado(InsumosPeriodo insumos, long gradoId)
 834    {
 835        const int CodigoSueldoBase     = 11000;
 836        const int CodigoCompOficial    = 42012;
 837        const int CodigoCompSubalterno = 42022;
 838
 32839        var sueldoBase     = insumos.ObtenerRemuneracion(gradoId, CodigoSueldoBase)?.Monto ?? 0m;
 32840        var compOficial    = insumos.ObtenerRemuneracion(gradoId, CodigoCompOficial)?.Monto ?? 0m;
 32841        var compSubalterno = insumos.ObtenerRemuneracion(gradoId, CodigoCompSubalterno)?.Monto ?? 0m;
 842
 32843        return sueldoBase + compOficial + compSubalterno;
 844    }
 845
 846    // Fonasa carries el monto del aporte de salud: FONASA por tasa única (civiles) o
 847    // Sanidad Militar por tramos (militares). SanidadCodigo indica el código del tramo
 848    // aplicado (5010/5011/5012) cuando corresponde a Sanidad Militar; null para FONASA.
 352849    private sealed record AportesLegales(decimal Montepio, decimal Fonasa, decimal Frl, int? SanidadCodigo = null)
 850    {
 32851        public decimal Total => Montepio + Fonasa + Frl;
 852    }
 853
 160854    private sealed record ResultadoIrpfLiquidacion(decimal TotalIrpf, LiquidacionItem? Item);
 855}

Methods/Properties

ObtenerOGCompensacion(FAU.Entidades.ItemLoteCompensacion)
.ctor(FAU.DataAccess.ApplicationDbContext,FAU.Logica.Services.IAuditoriaService,FAU.Logica.Services.ICurrentUserContext,FAU.Logica.Services.IInsumosPeriodoLoader,FAU.Logica.Services.IProcesadorBeneficiosService,FAU.Logica.Calculadores.ICalculadorFactory,FAU.Logica.Services.ICalcularDiferencia615Service,FAU.DataAccess.Repositories.IDescuentoPersonalRepository,FAU.DataAccess.Repositories.IDependienteRepository,FAU.Logica.Calculators.IDeficitCalculator,FAU.Logica.Services.IDescuentosLegalesCalculator,FAU.Logica.Services.IIrpfService)
GenerarLiquidacionAsync()
ObtenerVersionMaximaAsync()
ObtenerEstadosAsync()
ObtenerItemsAsync()
ObtenerResumenAsync()
ObtenerVersionesAsync()
.cctor()
CalcularAportesLegalesAsync()
CalcularIrpfAsync()
CrearDeduccionesIrpfAsync()
CrearItemsDescuentosPersonalesAsync()
CrearEntradaDeficit(FAU.Entidades.DescuentoPersonalPeriodo,System.Int64)
CrearItemsAportesLegales(System.Int64,System.String,System.Int32,FAU.Logica.Services.LiquidacionService/AportesLegales,System.Collections.Generic.IReadOnlyDictionary`2<System.Int32,FAU.Entidades.CatalogoItem>)
AgregarItemAporteLegal(System.Collections.Generic.ICollection`1<FAU.Entidades.LiquidacionItem>,System.Int64,System.String,System.Int32,System.Decimal,FAU.Entidades.CatalogoItem,System.String,System.String,System.String)
BuscarCatalogoItem(System.Collections.Generic.IReadOnlyDictionary`2<System.Int32,FAU.Entidades.CatalogoItem>,System.Int32[])
CrearItemLiquidacion(System.Int64,System.String,System.Int32,FAU.Entidades.CatalogoItem,System.Decimal)
CrearItemLiquidacion(System.Int64,System.String,System.Int32,System.String,System.String,System.Decimal,System.String)
CalcularSueldoBaseParaTraslado(FAU.Logica.DTOs.InsumosPeriodo,System.Int64)
get_Montepio()
get_Total()
get_TotalIrpf()