< 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
87%
Covered lines: 541
Uncovered lines: 75
Coverable lines: 616
Total lines: 988
Line coverage: 87.8%
Branch coverage
81%
Covered branches: 187
Total branches: 230
Branch coverage: 81.3%
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 System.Runtime.CompilerServices;
 2using FAU.DataAccess;
 3using FAU.DataAccess.Repositories;
 4using FAU.Entidades;
 5using FAU.Logica.Calculadores;
 6using FAU.Logica.Calculators;
 7using FAU.Logica.DTOs;
 8using Microsoft.EntityFrameworkCore;
 9
 10[assembly: InternalsVisibleTo("FAU.Tests")]
 11
 12namespace FAU.Logica.Services;
 13
 14public class LiquidacionService : ILiquidacionService
 15{
 16    private const int CodigoIrpf = 517;
 17    private const int CodigoMontepioSubalterno = 550;
 18    private const int CodigoSanidad = 520;
 19    private const string CodigoFrl = "FRL";
 20    private const int CodigoTutelaSocial = 540;
 21    private const int CodigoAjusteRedondeo = 90000;
 22
 23    // El OG de cada compensación viene de ConfigCompensacion.CatalogoItem
 24    // cargado via InsumosPeriodoLoader (TipoCompensacion → Configuraciones → CatalogoItem)
 25    private static int? ObtenerOGCompensacion(ItemLoteCompensacion comp)
 26    {
 227        var codigo = comp.LoteCompensacion?.TipoCompensacion?.Configuraciones
 228            .FirstOrDefault(c => c.CatalogoItem is not null)
 229            ?.CatalogoItem?.Codigo;
 230        return codigo;
 31    }
 32
 33    private readonly ApplicationDbContext _context;
 34    private readonly IAuditoriaService _auditoriaService;
 35    private readonly ICurrentUserContext _currentUser;
 36    private readonly IInsumosPeriodoLoader _insumosLoader;
 37    private readonly IProcesadorBeneficiosService _procesadorBeneficios;
 38    // Calculadoras de haberes base
 39    private readonly ICalculadorFactory _calculadorFactory;
 40    private readonly ICalcularDiferencia615Service _diferencia615Service;
 41    // Descuentos legales, IRPF y déficit
 42    private readonly IDescuentoPersonalRepository _descuentoPersonalRepository;
 43    private readonly IDependienteRepository _dependienteRepository;
 44    private readonly IDeficitCalculator _deficitCalculator;
 45    private readonly IDescuentosLegalesCalculator _descuentosLegalesCalculator;
 46    private readonly IIrpfService _irpfService;
 47
 4348    public LiquidacionService(
 4349        ApplicationDbContext context,
 4350        IAuditoriaService auditoriaService,
 4351        ICurrentUserContext currentUser,
 4352        IInsumosPeriodoLoader insumosLoader,
 4353        IProcesadorBeneficiosService procesadorBeneficios,
 4354        ICalculadorFactory calculadorFactory,
 4355        ICalcularDiferencia615Service diferencia615Service,
 4356        IDescuentoPersonalRepository descuentoPersonalRepository,
 4357        IDependienteRepository dependienteRepository,
 4358        IDeficitCalculator deficitCalculator,
 4359        IDescuentosLegalesCalculator descuentosLegalesCalculator,
 4360        IIrpfService irpfService)
 61    {
 4362        _context = context;
 4363        _auditoriaService = auditoriaService;
 4364        _currentUser = currentUser;
 4365        _insumosLoader = insumosLoader;
 4366        _procesadorBeneficios = procesadorBeneficios;
 4367        _calculadorFactory = calculadorFactory;
 4368        _diferencia615Service = diferencia615Service;
 4369        _descuentoPersonalRepository = descuentoPersonalRepository;
 4370        _dependienteRepository = dependienteRepository;
 4371        _deficitCalculator = deficitCalculator;
 4372        _descuentosLegalesCalculator = descuentosLegalesCalculator;
 4373        _irpfService = irpfService;
 4374    }
 75
 76    /// <summary>
 77    /// Genera la liquidación para el período. La versión se determina automáticamente:
 78    /// max_version_existente + 1, lo que permite recalcular después de una reapertura
 79    /// sin perder el historial de versiones anteriores.
 80    /// </summary>
 81    public async Task<(bool Exito, string? Error, int Version)> GenerarLiquidacionAsync(long periodoId)
 82    {
 4083        var periodo = await _context.PeriodosLiquidacion
 4084            .AsNoTracking()
 4085            .FirstOrDefaultAsync(p => p.Id == periodoId && p.Activo);
 86
 4087        if (periodo is null)
 188            return (false, "Período de liquidación no encontrado o no está activo", 0);
 89
 90        // Versión = max existente + 1 (1 si es la primera vez)
 3991        var maxVersion = await _context.LiquidacionItems
 3992            .Where(li => li.PeriodoId == periodoId)
 3993            .MaxAsync(li => (int?)li.Version) ?? 0;
 3994        var version = maxVersion + 1;
 95
 96        InsumosPeriodo insumos;
 97        try
 98        {
 3999            insumos = await _insumosLoader.CargarAsync(periodoId);
 39100        }
 0101        catch (InvalidOperationException ex) when (ex.Message.Contains("arámetro"))
 102        {
 0103            return (false, $"No se encontraron parámetros para el período {periodo.Anio}/{periodo.Mes}", 0);
 104        }
 105
 106        // Usamos el mismo patrón de Join que funciona con el HasConversion de Estado enum→string
 39107        var rlRaw = await _context.RelacionesLaborales
 39108            .AsNoTracking()
 39109            .Where(r => r.Estado == EstadoRelacion.Activo)
 39110            .Join(
 39111                _context.Personas.AsNoTracking(),
 39112                r => r.PersonaId,
 39113                p => p.Id,
 39114                (r, p) => new { RL = r, Persona = p })
 39115            .ToListAsync();
 116
 39117        if (!rlRaw.Any())
 2118            return (false, "No se encontraron relaciones laborales activas para generar la liquidación", version);
 119
 120        // Cargar navigation properties necesarias para el pipeline de calculadoras
 75121        var situacionIds = rlRaw.Select(x => x.RL.SituacionId).Distinct().ToHashSet();
 75122        var regimenIds   = rlRaw.Select(x => x.RL.RegimenId).Distinct().ToHashSet();
 123
 37124        var situacionesDict = await _context.Situaciones.AsNoTracking()
 37125            .Where(s => situacionIds.Contains(s.Id))
 54126            .ToDictionaryAsync(s => s.Id);
 127
 37128        var regimenesDict = await _context.Regimenes.AsNoTracking()
 37129            .Where(r => regimenIds.Contains(r.Id))
 55130            .ToDictionaryAsync(r => r.Id);
 131
 132        // Materializar RelacionLaboral con navigation properties resueltas en memoria
 37133        var relacionesLaborales = rlRaw.Select(x =>
 37134        {
 38135            x.RL.Persona = x.Persona;
 38136            x.RL.Situacion = situacionesDict.GetValueOrDefault(x.RL.SituacionId)!;
 38137            x.RL.Regimen   = regimenesDict.GetValueOrDefault(x.RL.RegimenId)!;
 38138            return x.RL;
 37139        }).ToList();
 140
 37141        var primerDiaPeriodo = new DateOnly(periodo.Anio, periodo.Mes, 1);
 37142        var itemsToSave = new List<LiquidacionItem>();
 37143        var incidenciasToSave = new List<IncidenciaCalculo>();
 37144        var estadosPersonal = new Dictionary<string, string>();
 37145        var erroresPersonal = new Dictionary<string, string>();
 146
 147        // Pre-load historico de primas únicas en una sola consulta
 75148        var cedulasPersonas = relacionesLaborales.Select(r => r.Persona.Cedula).ToHashSet();
 37149        var itemsHistoricosGlobal = await _context.LiquidacionItems
 37150            .AsNoTracking()
 37151            .Where(li => cedulasPersonas.Contains(li.Cedula) &&
 37152                         (li.CodigoConcepto == "340" || li.CodigoConcepto == "350" || li.CodigoConcepto == "370"))
 37153            .ToListAsync();
 154
 155        // Índice de catálogo por código (usado por helpers de aportes legales)
 37156        var catalogoPorCodigo = insumos.CatalogoItems
 211157            .GroupBy(i => i.Codigo)
 455158            .ToDictionary(g => g.Key, g => g.First());
 159
 150160        foreach (var rl in relacionesLaborales)
 161        {
 38162            var cedula = rl.Persona.Cedula;
 163            try
 164            {
 38165                var acumuladores = new Acumuladores();
 38166                var itemsHaberes = new List<ItemCalculado>();
 38167                var itemsBeneficios = new List<ItemBeneficioCalculado>();
 168
 169                // --- Paso 1-3: Calculador de haberes base ---
 38170                var calculador = rl.Situacion?.Calculador is not null
 38171                    ? _calculadorFactory.Resolver(rl.Situacion.Calculador)
 38172                    : null;
 173
 38174                if (calculador is not null)
 175                {
 176                    // Prorrateo por novedad 'dias_eximidos': factor = dias/30.
 177                    // Endurecido: dias_afectados debe estar en [1,30]. Fuera de rango (incl. 0)
 178                    // NO se prorratea (factor 1) y se registra incidencia, para no anular
 179                    // indebidamente los haberes ni romper el cálculo de aportes/IRPF.
 17180                    var novedadEximidos = insumos.ObtenerNovedadEximidos(rl.PersonaId);
 17181                    var factorProrrateo = 1m;
 17182                    if (novedadEximidos is not null)
 183                    {
 3184                        var diasAfectados = novedadEximidos.DiasAfectados;
 3185                        if (diasAfectados is < 1 or > 30)
 186                        {
 1187                            incidenciasToSave.Add(new IncidenciaCalculo
 1188                            {
 1189                                PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 1190                                Tipo = TipoIncidenciaCalculo.CalculoSimplificado,
 1191                                Descripcion = $"Novedad 'dias_eximidos' con dias_afectados fuera de rango [1,30]: {diasA
 1192                                CreadoEn = DateTime.Now
 1193                            });
 194                        }
 2195                        else if (diasAfectados < 30)
 196                        {
 2197                            factorProrrateo = diasAfectados / 30m;
 198                        }
 199                    }
 200
 17201                    var resultadoCalculo = await calculador.CalcularAsync(rl, insumos, acumuladores);
 202
 17203                    if (factorProrrateo < 1m)
 204                    {
 20205                        foreach (var item in resultadoCalculo.Items)
 8206                            item.Importe = Math.Round(item.Importe * factorProrrateo, 2);
 207
 2208                        acumuladores.Reiniciar();
 20209                        foreach (var item in resultadoCalculo.Items)
 210                        {
 36211                            var ci = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == item.CodigoConcepto);
 16212                            if (ci is not null) acumuladores.AgregarItem(item.Importe, ci);
 213                        }
 214                    }
 215
 17216                    itemsHaberes.AddRange(resultadoCalculo.Items);
 217
 34218                    foreach (var inc in resultadoCalculo.Incidencias)
 0219                        incidenciasToSave.Add(new IncidenciaCalculo
 0220                        {
 0221                            PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 0222                            Tipo = TipoIncidenciaCalculo.CalculoSimplificado,
 0223                            Descripcion = inc, CreadoEn = DateTime.Now
 0224                        });
 225                }
 226                else
 227                {
 21228                    incidenciasToSave.Add(new IncidenciaCalculo
 21229                    {
 21230                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 21231                        Tipo = TipoIncidenciaCalculo.ErrorCalculo,
 21232                        Descripcion = $"Calculador no registrado: {rl.Situacion?.Calculador}",
 21233                        CreadoEn = DateTime.Now
 21234                    });
 235                }
 236
 237                // --- Paso 4: Beneficios sociales ---
 38238                var beneficios = insumos.ObtenerBeneficios(rl.PersonaId);
 38239                var itemsHistoricoPersona = itemsHistoricosGlobal.Where(h => h.Cedula == cedula).ToList();
 38240                var sueldoBasePersona = CalcularSueldoBaseParaTraslado(insumos, rl.GradoId);
 38241                var resultadoBeneficios = _procesadorBeneficios.ProcesarBeneficios(
 38242                    rl.PersonaId, beneficios, insumos.ParametroPeriodo, insumos.CatalogoItems,
 38243                    itemsHistoricoPersona, primerDiaPeriodo,
 38244                    insumos.BeneficiariosPorBeneficio, insumos.InstructivoAsigFamiliar,
 38245                    insumos.InstructivoPsf, insumos.TablaPorHogar,
 38246                    sueldoBaseParaTraslado: sueldoBasePersona);
 37247                itemsBeneficios.AddRange(resultadoBeneficios.Items);
 248
 74249                foreach (var inc in resultadoBeneficios.Incidencias)
 0250                    incidenciasToSave.Add(new IncidenciaCalculo
 0251                    {
 0252                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 0253                        Tipo = inc.Tipo, Descripcion = inc.Descripcion, CreadoEn = DateTime.Now
 0254                    });
 255
 256                // --- Paso 5: Compensaciones por lote ---
 257                // El OG viene de ConfigCompensacion.CatalogoItem (no hardcodeado)
 37258                var compensaciones = insumos.ObtenerCompensaciones(rl.PersonaId);
 78259                foreach (var comp in compensaciones)
 260                {
 2261                    var ogCode = ObtenerOGCompensacion(comp);
 2262                    if (ogCode is null) continue;   // sin config de OG → saltear
 263
 2264                    var monto = comp.Monto ?? 0m;
 2265                    if (monto <= 0) continue;
 266
 18267                    var ci = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == ogCode.Value);
 2268                    itemsHaberes.Add(new ItemCalculado
 2269                    {
 2270                        CodigoConcepto = ogCode.Value,
 2271                        NombreConcepto = ci?.Nombre ?? comp.LoteCompensacion?.TipoCompensacion?.Nombre ?? "",
 2272                        Importe = monto
 2273                    });
 4274                    if (ci is not null) acumuladores.AgregarItem(monto, ci);
 275                }
 276
 277                // --- Paso 5.5: Mod. Aportes Ley 19.695 (042.413) ---
 278                // Se calcula AQUÍ, después de compensaciones (paso 5), para que ASIDUIDAD
 279                // y otras compensaciones entren en WNominal antes del cálculo.
 280                // Replica el orden del legacy: SUM33315.PRG corre después de SUM33304.PRG.
 281                // Fórmula: (WNominal - 048.042 - 042.403) × 2.35%
 37282                if (rl.Regimen is not null && !rl.Regimen.EsLeyVieja)
 283                {
 80284                    var monto048042 = itemsHaberes.Where(i => i.CodigoConcepto == 48042).Sum(i => i.Importe);
 80285                    var monto042403 = itemsHaberes.Where(i => i.CodigoConcepto == 42403).Sum(i => i.Importe);
 15286                    var base413 = acumuladores.WNominal - monto048042 - monto042403;
 15287                    if (base413 > 0)
 288                    {
 15289                        var monto413 = Math.Round(base413 * 0.0235m, 2);
 290                        const int Cod413 = 42413;
 15291                        var ci413 = catalogoPorCodigo.TryGetValue(Cod413, out var c413) ? c413 : null;
 15292                        itemsHaberes.Add(new ItemCalculado
 15293                        {
 15294                            CodigoConcepto = Cod413,
 15295                            NombreConcepto = ci413?.Nombre ?? "MOD. APORTES LEY 19.695 MDN (042.413)",
 15296                            Importe = monto413
 15297                        });
 25298                        if (ci413 is not null) acumuladores.AgregarItem(monto413, ci413);
 299                    }
 300                }
 301
 302                // --- Paso 6: Diferencia por ascenso 042.615 ---
 37303                var diffAscenso = insumos.ObtenerDiferenciaAscenso(rl.PersonaId);
 37304                if (diffAscenso is not null && diffAscenso.Monto > 0)
 305                {
 306                    const int Cod615 = 42615;
 9307                    var ci615 = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == Cod615);
 1308                    itemsHaberes.Add(new ItemCalculado
 1309                    {
 1310                        CodigoConcepto = Cod615,
 1311                        NombreConcepto = ci615?.Nombre ?? "Diferencia por Ascenso (042.615)",
 1312                        Importe = diffAscenso.Monto
 1313                    });
 2314                    if (ci615 is not null) acumuladores.AgregarItem(diffAscenso.Monto, ci615);
 315                }
 316
 317                // --- Paso 7: Aportes legales (Montepío, FONASA, FRL) ---
 318                // baseSdlAportes = todos haberes positivos + beneficios gravados
 319                // nominalBruto   = todos haberes positivos + todos beneficios positivos
 320                // Se suma directamente desde itemsHaberes porque contribuye_w_nominal
 321                // puede no estar configurado en el catálogo (en ese caso WNominal=0).
 207322                var totalHaberesPositivos = itemsHaberes.Where(i => i.Importe > 0m).Sum(i => i.Importe);
 37323                var totalBeneficiosGravados = resultadoBeneficios.Items
 29324                    .Where(i => i.Importe > 0m
 29325                                && catalogoPorCodigo.TryGetValue(i.CodigoConcepto, out var ciBen)
 29326                                && (ciBen.EsGravado || ciBen.ContribuyeWNominal))
 58327                    .Sum(i => i.Importe);
 37328                var baseSdlAportes = totalHaberesPositivos + totalBeneficiosGravados;
 37329                var nominalBruto = totalHaberesPositivos
 90330                    + resultadoBeneficios.Items.Where(i => i.Importe > 0m).Sum(i => i.Importe);
 331
 332                // --- Paso 6.5: Subsidio por enfermedad ---
 333                // Si existe novedad confirmada, el nominal va íntegro al fondo de subsidio.
 334                // No se calculan aportes legales, IRPF ni descuentos personales — replica el
 335                // comportamiento del legacy donde DESCUENTOS = NOMINAL y A COBRAR = 0.
 37336                var novedadSubsidio = insumos.ObtenerNovedades(rl.PersonaId)
 40337                    .FirstOrDefault(n => n.Tipo == "subsidio_enfermedad" && n.Estado == "confirmada");
 37338                var tieneSubsidio = novedadSubsidio is not null;
 37339                if (tieneSubsidio)
 340                {
 0341                    var factor = Math.Min((int)novedadSubsidio!.DiasAfectados, 30) / 30m
 0342                                 * (novedadSubsidio.PorcentajeSubsidio ?? 100m) / 100m;
 0343                    var montoSubsidio = Math.Round(nominalBruto * factor, 2);
 0344                    itemsToSave.Add(CrearItemLiquidacion(periodoId, cedula, version,
 0345                        "SUBSIDIO", "Subsidio por Enfermedad", -montoSubsidio, "SUBSIDIO"));
 0346                    estadosPersonal[cedula] = "CALCULADO";
 347                }
 348
 37349                if (!tieneSubsidio)
 350                {
 37351                var esMilitar = !CalculadoresCiviles.Contains(rl.Situacion?.Calculador ?? string.Empty);
 352                // UsaFonasa=true anula Sanidad Militar aunque sea militar (SS_FF_AA=0 en legacy)
 37353                var usaSanidadMilitar = esMilitar && !rl.UsaFonasa;
 37354                var aportes = await CalcularAportesLegalesAsync(rl.RegimenId, baseSdlAportes, insumos, usaSanidadMilitar
 37355                var itemsAportesLegales = CrearItemsAportesLegales(periodoId, cedula, version, aportes, catalogoPorCodig
 37356                itemsToSave.AddRange(itemsAportesLegales);
 357
 358                // --- Paso 8: IRPF ---
 359                // Eliminar registro anterior para que CalcularYPersistirAsync siempre inserte limpio
 37360                var irpfAnterior = await _context.Set<IrpfMensual>()
 37361                    .FirstOrDefaultAsync(i => i.LiquidacionId == periodoId && i.PersonaId == rl.PersonaId);
 37362                if (irpfAnterior is not null)
 363                {
 0364                    var detallesAnteriores = await _context.Set<IrpfDetalleFranjas>()
 0365                        .Where(d => d.IrpfMensualId == irpfAnterior.Id)
 0366                        .ToListAsync();
 0367                    _context.Set<IrpfDetalleFranjas>().RemoveRange(detallesAnteriores);
 0368                    _context.Set<IrpfMensual>().Remove(irpfAnterior);
 0369                    await _context.SaveChangesAsync();
 370                }
 371
 37372                var resultadoIrpf = await CalcularIrpfAsync(
 37373                    periodoId, rl.PersonaId, cedula, version,
 37374                    nominalBruto, aportes, insumos, catalogoPorCodigo);
 375
 376                // Descartar entidades IrpfMensual trackeadas si la persistencia falló,
 377                // para que no se reintentan en el SaveChangesAsync final.
 74378                foreach (var entry in _context.ChangeTracker.Entries<IrpfMensual>()
 0379                             .Where(e => e.State == Microsoft.EntityFrameworkCore.EntityState.Added)
 37380                             .ToList())
 0381                    entry.State = Microsoft.EntityFrameworkCore.EntityState.Detached;
 382
 37383                if (resultadoIrpf.Item is not null && resultadoIrpf.TotalIrpf != 0)
 3384                    itemsToSave.Add(resultadoIrpf.Item);
 385                else
 386                {
 387                    // Diagnóstico: registrar siempre el resultado del cálculo IRPF para debugging
 34388                    var tieneCatalogo = catalogoPorCodigo.ContainsKey(CodigoIrpf);
 34389                    incidenciasToSave.Add(new IncidenciaCalculo
 34390                    {
 34391                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 34392                        Tipo = TipoIncidenciaCalculo.CalculoSimplificado,
 34393                        Descripcion = $"IRPF diag: TotalIrpf={resultadoIrpf.TotalIrpf:F2} ItemNull={resultadoIrpf.Item i
 34394                        CreadoEn = DateTime.Now
 34395                    });
 396                }
 397
 398                // --- Paso 9: Descuentos personales con déficit ---
 399                // Excluir códigos ya emitidos como aportes para no contarlos dos veces en el déficit
 37400                var codigosAportesEmitidos = itemsAportesLegales
 102401                    .Where(i => !string.IsNullOrWhiteSpace(i.CodigoConcepto))
 102402                    .Select(i => i.CodigoConcepto)
 37403                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
 37404                var descuentosLegalesEnBeneficios = resultadoBeneficios.Items
 29405                    .Where(i => i.Importe < 0m
 29406                                && catalogoPorCodigo.TryGetValue(i.CodigoConcepto, out var ci)
 29407                                && string.Equals(ci.Tipo, "descuento_legal", StringComparison.OrdinalIgnoreCase)
 29408                                && !codigosAportesEmitidos.Contains(i.CodigoConcepto.ToString()))
 41409                    .Sum(i => Math.Abs(i.Importe));
 37410                var totalDescuentosLegales = aportes.Total + resultadoIrpf.TotalIrpf + descuentosLegalesEnBeneficios;
 37411                var fechaPeriodo = new DateTime(insumos.ParametroPeriodo.Anio, insumos.ParametroPeriodo.Mes, 1);
 37412                var itemsDescuentosPersonales = await CrearItemsDescuentosPersonalesAsync(
 37413                    periodoId, rl.PersonaId, cedula, version,
 37414                    nominalBruto, totalDescuentosLegales, fechaPeriodo);
 37415                itemsToSave.AddRange(itemsDescuentosPersonales);
 416
 417                // --- Paso 9.5: Ajuste de redondeo ---
 418                // El liquido real (lo que el funcionario ve en el recibo) es la suma de
 419                // todos los items que se van a persistir, no `nominalBruto` (que filtra
 420                // solo positivos y se usa como base de aportes/IRPF).
 122421                var totalSinAjuste = itemsHaberes.Sum(i => i.Importe)
 29422                    + itemsBeneficios.Sum(i => i.Importe)
 102423                    + itemsAportesLegales.Sum(i => i.Importe)
 37424                    + (resultadoIrpf.Item?.Importe ?? 0m)
 47425                    + itemsDescuentosPersonales.Sum(i => i.Importe);
 426
 37427                var ajusteRedondeo = CalcularAjusteRedondeo(totalSinAjuste);
 37428                if (ajusteRedondeo != 0m && catalogoPorCodigo.TryGetValue(CodigoAjusteRedondeo, out var ciAjuste))
 4429                    itemsToSave.Add(CrearItemLiquidacion(periodoId, cedula, version, ciAjuste, ajusteRedondeo));
 430
 431                // --- Paso 10: Estado ---
 37432                var tieneError = incidenciasToSave.Any(i =>
 76433                    i.PersonaId == rl.PersonaId && i.Tipo == TipoIncidenciaCalculo.ErrorCalculo);
 37434                var liquido = nominalBruto - totalDescuentosLegales
 47435                    - itemsDescuentosPersonales.Sum(i => Math.Abs(i.Importe));
 37436                var estado = tieneError ? "ERROR" : (liquido < 0 ? "REVISAR" : "CALCULADO");
 37437                estadosPersonal[cedula] = estado;
 37438                } // end if (!tieneSubsidio)
 439
 440                // --- Paso 11: Persistir ítems de haberes y beneficios ---
 244441                foreach (var item in itemsHaberes)
 442                {
 85443                    catalogoPorCodigo.TryGetValue(item.CodigoConcepto, out var catalogoItem);
 85444                    itemsToSave.Add(new LiquidacionItem
 85445                    {
 85446                        PeriodoId = periodoId,
 85447                        Cedula = cedula,
 85448                        Version = version,
 85449                        CodigoConcepto = item.CodigoConcepto.ToString(),
 85450                        NombreConcepto = item.NombreConcepto,
 85451                        Importe = item.Importe,
 85452                        RubroContable = catalogoItem?.ClasificacionSiif ?? string.Empty,
 85453                        Rubro083 = false,
 85454                        FechaCreacion = DateTime.Now
 85455                    });
 456                }
 132457                foreach (var item in itemsBeneficios)
 458                {
 29459                    itemsToSave.Add(new LiquidacionItem
 29460                    {
 29461                        PeriodoId = periodoId,
 29462                        Cedula = cedula,
 29463                        Version = version,
 29464                        CodigoConcepto = item.CodigoConcepto.ToString(),
 29465                        NombreConcepto = item.NombreConcepto,
 29466                        Importe = item.Importe,
 29467                        RubroContable = item.RubroContable,
 29468                        Rubro083 = false,
 29469                        ClaveEvento = item.ClaveEvento,
 29470                        FechaCreacion = DateTime.Now
 29471                    });
 472                }
 37473            }
 1474            catch (Exception ex)
 475            {
 1476                estadosPersonal[cedula] = "ERROR";
 1477                erroresPersonal[cedula] = ex.Message;
 1478                incidenciasToSave.Add(new IncidenciaCalculo
 1479                {
 1480                    PeriodoId = periodoId,
 1481                    Version = version,
 1482                    PersonaId = rl.PersonaId,
 1483                    Tipo = TipoIncidenciaCalculo.ErrorCalculo,
 1484                    Descripcion = $"Error en pipeline de liquidación: {ex.Message}",
 1485                    CreadoEn = DateTime.Now
 1486                });
 1487            }
 38488        }
 489
 75490        var estados = relacionesLaborales.Select(rl => new LiquidacionEstadoPersonal
 75491        {
 75492            PeriodoId = periodoId,
 75493            Cedula = rl.Persona.Cedula,
 75494            Version = version,
 75495            Estado = estadosPersonal.TryGetValue(rl.Persona.Cedula, out var e) ? e : "PENDIENTE",
 75496            MensajeError = erroresPersonal.TryGetValue(rl.Persona.Cedula, out var err) ? err : null,
 75497            FechaActualizacion = DateTime.Now
 75498        }).ToList();
 499
 37500        await _context.LiquidacionEstadosPersonal.AddRangeAsync(estados);
 501
 37502        if (itemsToSave.Count > 0)
 36503            await _context.LiquidacionItems.AddRangeAsync(itemsToSave);
 504
 37505        if (incidenciasToSave.Count > 0)
 37506            await _context.IncidenciasCalculo.AddRangeAsync(incidenciasToSave);
 507
 270508        var totalImporte = itemsToSave.Sum(i => i.Importe);
 37509        var resumen = new LiquidacionResumen
 37510        {
 37511            PeriodoId = periodoId,
 37512            Version = version,
 37513            TotalImporte = totalImporte,
 37514            Rubro083 = false,
 37515            Descripcion = "Liquidación generada"
 37516        };
 37517        await _context.LiquidacionResumenes.AddAsync(resumen);
 518
 37519        await _context.SaveChangesAsync();
 520
 37521        await _auditoriaService.LogAuditoriaAsync(
 37522            _currentUser.UserId ?? 0,
 37523            AccionEnum.GenerarLiquidacion,
 37524            ContextoEnum.Liquidaciones,
 37525            _currentUser.Host,
 37526            entidad: nameof(LiquidacionEstadoPersonal),
 37527            entidadId: periodoId,
 37528            detalle: new { periodoId, version, registros = estados.Count, items = itemsToSave.Count, incidencias = incid
 529
 37530        return (true, null, version);
 40531    }
 532
 533    private async Task<int> ObtenerVersionMaximaAsync(long periodoId)
 534    {
 1535        return await _context.LiquidacionItems
 1536            .AsNoTracking()
 1537            .Where(i => i.PeriodoId == periodoId)
 1538            .MaxAsync(i => (int?)i.Version) ?? 0;
 1539    }
 540
 541    public async Task<IEnumerable<CalculoEstadoDto>> ObtenerEstadosAsync(long periodoId, int? version = null)
 542    {
 2543        var v = version ?? await ObtenerVersionMaximaAsync(periodoId);
 544
 2545        var totalesPorCedula = await _context.LiquidacionItems
 2546            .AsNoTracking()
 2547            .Where(i => i.PeriodoId == periodoId && i.Version == v)
 2548            .GroupBy(i => i.Cedula)
 2549            .Select(g => new
 2550            {
 2551                Cedula = g.Key,
 2552                Nominal    = g.Where(i => i.Importe > 0).Sum(i => i.Importe),
 2553                Descuentos = g.Where(i => i.Importe < 0).Sum(i => i.Importe),
 2554            })
 3555            .ToDictionaryAsync(x => x.Cedula);
 556
 2557        var estados = await _context.LiquidacionEstadosPersonal
 2558            .AsNoTracking()
 2559            .Where(e => e.PeriodoId == periodoId && e.Version == v)
 2560            .Select(e => new CalculoEstadoDto
 2561            {
 2562                Cedula       = e.Cedula,
 2563                Version      = e.Version,
 2564                Estado       = e.Estado,
 2565                MensajeError = e.MensajeError
 2566            })
 2567            .ToListAsync();
 568
 8569        foreach (var dto in estados)
 2570            if (totalesPorCedula.TryGetValue(dto.Cedula, out var tot))
 571            {
 1572                dto.Nominal    = tot.Nominal;
 1573                dto.Descuentos = tot.Descuentos;
 1574                dto.Liquido    = tot.Nominal + tot.Descuentos;
 575            }
 576
 2577        return estados;
 2578    }
 579
 580    public async Task<IEnumerable<CalculoItemDto>> ObtenerItemsAsync(long periodoId, int? version = null, string? cedula
 581    {
 0582        var v = version ?? await ObtenerVersionMaximaAsync(periodoId);
 0583        var query = _context.LiquidacionItems
 0584            .AsNoTracking()
 0585            .Where(i => i.PeriodoId == periodoId && i.Version == v);
 586
 0587        if (!string.IsNullOrEmpty(cedula))
 0588            query = query.Where(i => i.Cedula == cedula);
 589
 0590        return await query
 0591            .Select(i => new CalculoItemDto
 0592            {
 0593                Cedula         = i.Cedula,
 0594                Version        = i.Version,
 0595                CodigoConcepto = i.CodigoConcepto,
 0596                NombreConcepto = i.NombreConcepto,
 0597                Importe        = i.Importe,
 0598                RubroContable  = i.RubroContable,
 0599                ObjetoGasto    = i.ObjetoGasto
 0600            })
 0601            .ToListAsync();
 0602    }
 603
 604    public async Task<IEnumerable<LiquidacionResumenDto>> ObtenerResumenAsync(long periodoId, int? version = null)
 605    {
 0606        var v = version ?? await ObtenerVersionMaximaAsync(periodoId);
 0607        return await _context.LiquidacionResumenes
 0608            .AsNoTracking()
 0609            .Where(r => r.PeriodoId == periodoId && r.Version == v)
 0610            .Select(r => new LiquidacionResumenDto
 0611            {
 0612                Version = r.Version,
 0613                ProgramaId = r.ProgramaId,
 0614                RegimenId = r.RegimenId,
 0615                Rubro083 = r.Rubro083,
 0616                TotalImporte = r.TotalImporte,
 0617                Descripcion = r.Descripcion
 0618            })
 0619            .ToListAsync();
 0620    }
 621
 622    public async Task<IEnumerable<int>> ObtenerVersionesAsync(long periodoId)
 623    {
 2624        return await _context.LiquidacionItems
 2625            .AsNoTracking()
 2626            .Where(i => i.PeriodoId == periodoId)
 2627            .Select(i => i.Version)
 2628            .Distinct()
 2629            .OrderByDescending(v => v)
 2630            .ToListAsync();
 2631    }
 632
 633    // Civiles aportan FONASA por tasa única; los militares aportan Sanidad Militar por tramos.
 1634    private static readonly HashSet<string> CalculadoresCiviles =
 1635        new(StringComparer.OrdinalIgnoreCase) { "Sit70Calculator", "CivilCalculator" };
 636
 637    private async Task<AportesLegales> CalcularAportesLegalesAsync(
 638        long regimenId, decimal baseSdl, InsumosPeriodo insumos, bool esMilitar)
 639    {
 37640        var regimen = await _context.Regimenes.AsNoTracking().FirstOrDefaultAsync(r => r.Id == regimenId);
 37641        var tasaMontepio = regimen is not null && regimen.EsLeyVieja
 37642            ? insumos.ParametroPeriodo.TasaMontepioLeyVieja
 37643            : insumos.ParametroPeriodo.TasaMontepioLeyNueva;
 644
 37645        var montepio = _descuentosLegalesCalculator.CalcularMontepio(baseSdl, tasaMontepio);
 37646        var frl = _descuentosLegalesCalculator.CalcularFrl(baseSdl, insumos.ParametroPeriodo.TasaFrl);
 37647        var tutelaSocial = _descuentosLegalesCalculator.CalcularTutelaSocial(baseSdl, insumos.ParametroPeriodo.TasaTutel
 648
 649        // Sanidad Militar (Art. 94 Ley 18.834) reemplaza a FONASA para militares.
 650        // Base = misma que el montepío (baseSdl). Civiles mantienen FONASA por tasa única.
 37651        var tramo = esMilitar ? insumos.ObtenerTramoSanidad(baseSdl) : null;
 37652        if (tramo is not null)
 653        {
 0654            var sanidad = _descuentosLegalesCalculator.CalcularSanidad(baseSdl, tramo.Porcentaje);
 0655            return new AportesLegales(montepio, sanidad, frl, tutelaSocial, tramo.CodigoRetencion);
 656        }
 657
 37658        var fonasa = _descuentosLegalesCalculator.CalcularFonasa(baseSdl, insumos.ParametroPeriodo.TasaFonasa);
 37659        return new AportesLegales(montepio, fonasa, frl, tutelaSocial, SanidadCodigo: null);
 37660    }
 661
 662    private async Task<ResultadoIrpfLiquidacion> CalcularIrpfAsync(
 663        long periodoId,
 664        long personaId,
 665        string cedula,
 666        int version,
 667        decimal nominalBruto,
 668        AportesLegales aportes,
 669        InsumosPeriodo insumos,
 670        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo)
 671    {
 37672        var request = new CalcularIrpfRequest
 37673        {
 37674            IngresoGravados = nominalBruto,
 37675            Montepio = aportes.Montepio,
 37676            Fonasa = aportes.Fonasa,
 37677            Frl = aportes.Frl,
 37678            Deducciones = await CrearDeduccionesIrpfAsync(personaId),
 37679            Parametros = new ParametrosIrpfDto
 37680            {
 37681                BpcMensual = insumos.ParametroPeriodo.BpcAnual / 12m,
 37682                UmbralDeduccionBpc = insumos.ParametroPeriodo.UmbralDeduccionBpc,
 37683                TasaDeduccionAlta = insumos.ParametroPeriodo.TasaDeduccionAlta,
 37684                TasaDeduccionBaja = insumos.ParametroPeriodo.TasaDeduccionBaja
 37685            },
 37686            Franjas = insumos.FranjasIrpf
 75687                .Select(f => new FranjaIrpfCalculoDto
 75688                {
 75689                    FranjaId = f.Id,
 75690                    NumeroFranja = f.NumeroFranja,
 75691                    DesdeBpc = f.DesdeBpc,
 75692                    HastaBpc = f.HastaBpc,
 75693                    Tasa = f.Tasa
 75694                })
 37695                .ToList()
 37696        };
 697
 37698        var (exito, idIrpf, _) = await _irpfService.CalcularYPersistirAsync(periodoId, personaId, request);
 37699        if (!exito || !idIrpf.HasValue)
 33700            return new ResultadoIrpfLiquidacion(0m, null);
 701
 4702        var irpf = await _irpfService.ObtenerPorIdAsync(idIrpf.Value);
 4703        if (irpf is null)
 0704            return new ResultadoIrpfLiquidacion(0m, null);
 705
 4706        var item = catalogoPorCodigo.TryGetValue(CodigoIrpf, out var catalogoIrpf)
 4707            ? CrearItemLiquidacion(periodoId, cedula, version, catalogoIrpf, -irpf.TotalIrpf)
 4708            : null;
 709
 4710        return new ResultadoIrpfLiquidacion(irpf.TotalIrpf, item);
 37711    }
 712
 713    private async Task<List<DeduccionDto>> CrearDeduccionesIrpfAsync(long personaId)
 714    {
 37715        var dependientes = await _dependienteRepository.GetDependientesByPersonaAsync(personaId);
 716
 37717        return dependientes
 3718            .Where(d => d.Activo)
 37719            .SelectMany(d =>
 37720            {
 3721                if (string.Equals(d.Tipo, "conyuge_sin_ingresos", StringComparison.OrdinalIgnoreCase))
 1722                    return [new DeduccionDto { Tipo = "conyuge_sin_ingresos", Cantidad = 1 }];
 37723
 2724                if (string.Equals(d.Tipo, "hijo", StringComparison.OrdinalIgnoreCase))
 37725                {
 2726                    var tipoHijo = d.Discapacitado ? "hijo_discapacidad" : "hijo_menor";
 2727                    return [new DeduccionDto { Tipo = tipoHijo, Cantidad = 1 }];
 37728                }
 37729
 0730                return Array.Empty<DeduccionDto>();
 37731            })
 37732            .ToList();
 37733    }
 734
 735    private async Task<IReadOnlyList<LiquidacionItem>> CrearItemsDescuentosPersonalesAsync(
 736        long periodoId,
 737        long personaId,
 738        string cedula,
 739        int version,
 740        decimal nominalBruto,
 741        decimal descuentosLegales,
 742        DateTime fechaPeriodo)
 743    {
 37744        var descuentosConfirmados = (await _descuentoPersonalRepository
 37745                .GetConfirmadosPorPersonaAsync(periodoId, personaId))
 10746            .Where(d => d.CatalogoItem is not null)
 37747            // Vigencia (ej. CUOTA_ING): se aplica solo si no venció antes del período
 10748            .Where(d => d.FechaHasta is null || d.FechaHasta >= fechaPeriodo)
 37749            .ToList();
 750
 37751        if (descuentosConfirmados.Count == 0)
 28752            return Array.Empty<LiquidacionItem>();
 753
 754        // Filtrar descuentos sin acreedor asignado (datos previos a la migración de acreedor_id)
 9755        var descuentosConAcreedor = descuentosConfirmados
 10756            .Where(d => d.AcreedorId is not null && d.Acreedor is not null)
 9757            .ToList();
 758
 9759        if (descuentosConAcreedor.Count == 0)
 0760            return Array.Empty<LiquidacionItem>();
 761
 9762        var entradasDeficit = descuentosConAcreedor
 10763            .Select(d => CrearEntradaDeficit(d, personaId))
 9764            .ToList();
 765
 9766        var resultadosDeficit = (await _deficitCalculator.AplicarAsync(
 9767            nominalBruto,
 9768            descuentosLegales,
 9769            entradasDeficit)).ToList();
 770
 19771        var descuentosPorId = descuentosConAcreedor.ToDictionary(d => d.Id);
 9772        var items = new List<LiquidacionItem>();
 773
 38774        foreach (var resultadoDeficit in resultadosDeficit)
 775        {
 10776            if (resultadoDeficit.DescuentoPeriodoId is null)
 777                continue;
 778
 10779            if (!descuentosPorId.TryGetValue(resultadoDeficit.DescuentoPeriodoId.Value, out var descuento))
 780                continue;
 781
 10782            descuento.MontoAplicado = resultadoDeficit.MontoAplicado;
 10783            descuento.MontoNoAplicado = resultadoDeficit.MontoNoAplicado;
 10784            descuento.Estado = resultadoDeficit.Estado;
 785
 10786            if (!string.IsNullOrWhiteSpace(resultadoDeficit.MotivoNoAplicado) &&
 10787                string.IsNullOrWhiteSpace(descuento.Observaciones))
 788            {
 0789                descuento.Observaciones = resultadoDeficit.MotivoNoAplicado;
 790            }
 791
 10792            if (resultadoDeficit.MontoAplicado <= 0m)
 793                continue;
 794
 10795            items.Add(CrearItemLiquidacion(
 10796                periodoId,
 10797                cedula,
 10798                version,
 10799                descuento.CatalogoItem,
 10800                -resultadoDeficit.MontoAplicado));
 801        }
 802
 9803        return items;
 37804    }
 805
 806    private static DescuentoPersonalInput CrearEntradaDeficit(DescuentoPersonalPeriodo descuento, long personaId)
 807    {
 10808        if (descuento.AcreedorId is null)
 0809            throw new InvalidOperationException($"El descuento personal {descuento.Id} no tiene acreedor asignado.");
 810
 10811        if (descuento.Acreedor is null)
 0812            throw new InvalidOperationException($"El descuento personal {descuento.Id} no tiene acreedor cargado.");
 813
 10814        return new DescuentoPersonalInput(
 10815            descuento.Id,
 10816            personaId,
 10817            descuento.CatalogoItem.Codigo,
 10818            descuento.Importe,
 10819            descuento.AcreedorId.Value,
 10820            descuento.Acreedor.CategoriaDeficit,
 10821            descuento.Acreedor.OrdenLegalDeficit,
 10822            descuento.FechaComunicacion ?? descuento.FechaCreacion);
 823    }
 824
 825    private static IReadOnlyList<LiquidacionItem> CrearItemsAportesLegales(
 826        long periodoId,
 827        string cedula,
 828        int version,
 829        AportesLegales aportes,
 830        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo)
 831    {
 37832        var items = new List<LiquidacionItem>();
 833
 37834        AgregarItemAporteLegal(
 37835            items, periodoId, cedula, version, aportes.Montepio,
 37836            BuscarCatalogoItem(catalogoPorCodigo, CodigoMontepioSubalterno, 545, 555),
 37837            codigoFallback: CodigoMontepioSubalterno.ToString(),
 37838            nombreFallback: "MONTEPIO",
 37839            rubroFallback: "5.04");
 840
 841        // Salud: Sanidad Militar por tramos (código 5010/5011/5012) o FONASA por tasa única.
 37842        if (aportes.SanidadCodigo is int sanidadCodigo)
 843        {
 0844            AgregarItemAporteLegal(
 0845                items, periodoId, cedula, version, aportes.Fonasa,
 0846                BuscarCatalogoItem(catalogoPorCodigo, sanidadCodigo),
 0847                codigoFallback: sanidadCodigo.ToString(),
 0848                nombreFallback: "SANIDAD",
 0849                rubroFallback: $"5.{sanidadCodigo % 100:D2}");
 850        }
 851        else
 852        {
 37853            AgregarItemAporteLegal(
 37854                items, periodoId, cedula, version, aportes.Fonasa,
 37855                BuscarCatalogoItem(catalogoPorCodigo, CodigoSanidad, 521, 522, 523, 524, 525, 526, 527, 528),
 37856                codigoFallback: CodigoSanidad.ToString(),
 37857                nombreFallback: "FONASA",
 37858                rubroFallback: "5.010");
 859        }
 860
 37861        AgregarItemAporteLegal(
 37862            items, periodoId, cedula, version, aportes.Frl,
 37863            null,
 37864            codigoFallback: CodigoFrl,
 37865            nombreFallback: "FRL",
 37866            rubroFallback: "000.000");
 867
 37868        AgregarItemAporteLegal(
 37869            items, periodoId, cedula, version, aportes.TutelaSocial,
 37870            BuscarCatalogoItem(catalogoPorCodigo, CodigoTutelaSocial),
 37871            codigoFallback: CodigoTutelaSocial.ToString(),
 37872            nombreFallback: "SERV. ESP. TUT. SOC.",
 37873            rubroFallback: "5.08");
 874
 37875        return items;
 876    }
 877
 878    private static void AgregarItemAporteLegal(
 879        ICollection<LiquidacionItem> items,
 880        long periodoId,
 881        string cedula,
 882        int version,
 883        decimal importe,
 884        CatalogoItem? catalogoItem,
 885        string codigoFallback,
 886        string nombreFallback,
 887        string rubroFallback)
 888    {
 148889        if (importe <= 0m)
 46890            return;
 891
 102892        if (catalogoItem is not null)
 893        {
 7894            items.Add(CrearItemLiquidacion(periodoId, cedula, version, catalogoItem, -importe));
 7895            return;
 896        }
 897
 95898        items.Add(CrearItemLiquidacion(periodoId, cedula, version, codigoFallback, nombreFallback, -importe, rubroFallba
 95899    }
 900
 901    private static CatalogoItem? BuscarCatalogoItem(
 902        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo,
 903        params int[] codigos)
 904    {
 1121905        foreach (var codigo in codigos)
 906        {
 453907            if (catalogoPorCodigo.TryGetValue(codigo, out var catalogoItem))
 7908                return catalogoItem;
 909        }
 910
 104911        return null;
 912    }
 913
 914    private static LiquidacionItem CrearItemLiquidacion(
 915        long periodoId,
 916        string cedula,
 917        int version,
 918        CatalogoItem catalogoItem,
 919        decimal importe)
 920    {
 24921        return CrearItemLiquidacion(
 24922            periodoId, cedula, version,
 24923            catalogoItem.Codigo.ToString(),
 24924            catalogoItem.Nombre,
 24925            importe,
 24926            catalogoItem.ClasificacionSiif ?? "000.000");
 927    }
 928
 929    private static LiquidacionItem CrearItemLiquidacion(
 930        long periodoId,
 931        string cedula,
 932        int version,
 933        string codigoConcepto,
 934        string nombreConcepto,
 935        decimal importe,
 936        string rubroContable)
 937    {
 119938        return new LiquidacionItem
 119939        {
 119940            PeriodoId = periodoId,
 119941            Cedula = cedula,
 119942            Version = version,
 119943            CodigoConcepto = codigoConcepto,
 119944            NombreConcepto = nombreConcepto,
 119945            Importe = importe,
 119946            RubroContable = rubroContable,
 119947            Rubro083 = false,
 119948            FechaCreacion = DateTime.Now
 119949        };
 950    }
 951
 952    /// <summary>
 953    /// Diferencia entre el redondeo al entero más cercano y <paramref name="totalSinAjuste"/>.
 954    /// Centavos &lt;= 0.50 redondean hacia abajo; centavos &gt; 0.50 redondean hacia arriba.
 955    /// La parte fraccionaria (<c>frac</c>) siempre queda en [0, 1) vía <see cref="Math.Floor(decimal)"/>,
 956    /// por lo que la regla es válida también para totales negativos.
 957    /// </summary>
 958    internal static decimal CalcularAjusteRedondeo(decimal totalSinAjuste)
 959    {
 43960        var piso = Math.Floor(totalSinAjuste);
 43961        var frac = totalSinAjuste - piso;
 43962        var redondeado = frac <= 0.50m ? piso : piso + 1m;
 43963        return redondeado - totalSinAjuste;
 964    }
 965
 966    private static decimal CalcularSueldoBaseParaTraslado(InsumosPeriodo insumos, long gradoId)
 967    {
 968        const int CodigoSueldoBase     = 11000;
 969        const int CodigoCompOficial    = 42012;
 970        const int CodigoCompSubalterno = 42022;
 971
 38972        var sueldoBase     = insumos.ObtenerRemuneracion(gradoId, CodigoSueldoBase)?.Monto ?? 0m;
 38973        var compOficial    = insumos.ObtenerRemuneracion(gradoId, CodigoCompOficial)?.Monto ?? 0m;
 38974        var compSubalterno = insumos.ObtenerRemuneracion(gradoId, CodigoCompSubalterno)?.Monto ?? 0m;
 975
 38976        return sueldoBase + compOficial + compSubalterno;
 977    }
 978
 979    // Fonasa carries el monto del aporte de salud: FONASA por tasa única (civiles) o
 980    // Sanidad Militar por tramos (militares). SanidadCodigo indica el código del tramo
 981    // aplicado (5010/5011/5012) cuando corresponde a Sanidad Militar; null para FONASA.
 481982    private sealed record AportesLegales(decimal Montepio, decimal Fonasa, decimal Frl, decimal TutelaSocial = 0m, int? 
 983    {
 37984        public decimal Total => Montepio + Fonasa + Frl + TutelaSocial;
 985    }
 986
 222987    private sealed record ResultadoIrpfLiquidacion(decimal TotalIrpf, LiquidacionItem? Item);
 988}

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)
CalcularAjusteRedondeo(System.Decimal)
CalcularSueldoBaseParaTraslado(FAU.Logica.DTOs.InsumosPeriodo,System.Int64)
get_Montepio()
get_Total()
get_TotalIrpf()