< 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
83%
Covered lines: 436
Uncovered lines: 87
Coverable lines: 523
Total lines: 833
Line coverage: 83.3%
Branch coverage
81%
Covered branches: 146
Total branches: 180
Branch coverage: 81.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ObtenerOGCompensacion(...)50%88100%
.ctor(...)100%11100%
GenerarLiquidacionAsync()83.69%1149286.32%
ObtenerEstadosAsync()100%210%
ObtenerItemsAsync()100%210%
ObtenerResumenAsync()100%210%
.cctor()100%11100%
CalcularAportesLegalesAsync()75%8884.61%
CalcularIrpfAsync()87.5%8896.42%
CrearDeduccionesIrpfAsync()100%11100%
CrearItemsDescuentosPersonalesAsync()91.66%121296.42%
CrearEntradaDeficit(...)66.66%6684.61%
CrearItemsAportesLegales(...)50%2277.77%
AgregarItemAporteLegal(...)100%44100%
BuscarCatalogoItem(...)100%44100%
CrearItemLiquidacion(...)50%22100%
CrearItemLiquidacion(...)100%11100%
CalcularSueldoBaseParaTraslado(...)100%66100%
get_Montepio()100%11100%
get_Total()100%11100%
get_TotalIrpf()100%11100%

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
 3443    public LiquidacionService(
 3444        ApplicationDbContext context,
 3445        IAuditoriaService auditoriaService,
 3446        ICurrentUserContext currentUser,
 3447        IInsumosPeriodoLoader insumosLoader,
 3448        IProcesadorBeneficiosService procesadorBeneficios,
 3449        ICalculadorFactory calculadorFactory,
 3450        ICalcularDiferencia615Service diferencia615Service,
 3451        IDescuentoPersonalRepository descuentoPersonalRepository,
 3452        IDependienteRepository dependienteRepository,
 3453        IDeficitCalculator deficitCalculator,
 3454        IDescuentosLegalesCalculator descuentosLegalesCalculator,
 3455        IIrpfService irpfService)
 56    {
 3457        _context = context;
 3458        _auditoriaService = auditoriaService;
 3459        _currentUser = currentUser;
 3460        _insumosLoader = insumosLoader;
 3461        _procesadorBeneficios = procesadorBeneficios;
 3462        _calculadorFactory = calculadorFactory;
 3463        _diferencia615Service = diferencia615Service;
 3464        _descuentoPersonalRepository = descuentoPersonalRepository;
 3465        _dependienteRepository = dependienteRepository;
 3466        _deficitCalculator = deficitCalculator;
 3467        _descuentosLegalesCalculator = descuentosLegalesCalculator;
 3468        _irpfService = irpfService;
 3469    }
 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    public async Task<IEnumerable<CalculoEstadoDto>> ObtenerEstadosAsync(long periodoId, int version)
 464    {
 0465        return await _context.LiquidacionEstadosPersonal
 0466            .AsNoTracking()
 0467            .Where(e => e.PeriodoId == periodoId && e.Version == version)
 0468            .Select(e => new CalculoEstadoDto
 0469            {
 0470                Cedula = e.Cedula,
 0471                Version = e.Version,
 0472                Estado = e.Estado,
 0473                MensajeError = e.MensajeError
 0474            })
 0475            .ToListAsync();
 0476    }
 477
 478    public async Task<IEnumerable<CalculoItemDto>> ObtenerItemsAsync(long periodoId, int version)
 479    {
 0480        return await _context.LiquidacionItems
 0481            .AsNoTracking()
 0482            .Where(i => i.PeriodoId == periodoId && i.Version == version)
 0483            .Select(i => new CalculoItemDto
 0484            {
 0485                Cedula = i.Cedula,
 0486                Version = i.Version,
 0487                CodigoConcepto = i.CodigoConcepto,
 0488                NombreConcepto = i.NombreConcepto,
 0489                Importe = i.Importe,
 0490                RubroContable = i.RubroContable,
 0491                ObjetoGasto = i.ObjetoGasto
 0492            })
 0493            .ToListAsync();
 0494    }
 495
 496    public async Task<IEnumerable<LiquidacionResumenDto>> ObtenerResumenAsync(long periodoId, int version)
 497    {
 0498        return await _context.LiquidacionResumenes
 0499            .AsNoTracking()
 0500            .Where(r => r.PeriodoId == periodoId && r.Version == version)
 0501            .Select(r => new LiquidacionResumenDto
 0502            {
 0503                Version = r.Version,
 0504                ProgramaId = r.ProgramaId,
 0505                RegimenId = r.RegimenId,
 0506                Rubro083 = r.Rubro083,
 0507                TotalImporte = r.TotalImporte,
 0508                Descripcion = r.Descripcion
 0509            })
 0510            .ToListAsync();
 0511    }
 512
 513    // Civiles aportan FONASA por tasa única; los militares aportan Sanidad Militar por tramos.
 1514    private static readonly HashSet<string> CalculadoresCiviles =
 1515        new(StringComparer.OrdinalIgnoreCase) { "Sit70Calculator", "CivilCalculator" };
 516
 517    private async Task<AportesLegales> CalcularAportesLegalesAsync(
 518        long regimenId, decimal baseSdl, InsumosPeriodo insumos, bool esMilitar)
 519    {
 32520        var regimen = await _context.Regimenes.AsNoTracking().FirstOrDefaultAsync(r => r.Id == regimenId);
 32521        var tasaMontepio = regimen is not null && regimen.EsLeyVieja
 32522            ? insumos.ParametroPeriodo.TasaMontepioLeyVieja
 32523            : insumos.ParametroPeriodo.TasaMontepioLeyNueva;
 524
 32525        var montepio = _descuentosLegalesCalculator.CalcularMontepio(baseSdl, tasaMontepio);
 32526        var frl = _descuentosLegalesCalculator.CalcularFrl(baseSdl, insumos.ParametroPeriodo.TasaFrl);
 527
 528        // Sanidad Militar (Art. 94 Ley 18.834) reemplaza a FONASA para militares.
 529        // Base = misma que el montepío (baseSdl). Civiles mantienen FONASA por tasa única.
 32530        var tramo = esMilitar ? insumos.ObtenerTramoSanidad(baseSdl) : null;
 32531        if (tramo is not null)
 532        {
 0533            var sanidad = _descuentosLegalesCalculator.CalcularSanidad(baseSdl, tramo.Porcentaje);
 0534            return new AportesLegales(montepio, sanidad, frl, tramo.CodigoRetencion);
 535        }
 536
 32537        var fonasa = _descuentosLegalesCalculator.CalcularFonasa(baseSdl, insumos.ParametroPeriodo.TasaFonasa);
 32538        return new AportesLegales(montepio, fonasa, frl, SanidadCodigo: null);
 32539    }
 540
 541    private async Task<ResultadoIrpfLiquidacion> CalcularIrpfAsync(
 542        long periodoId,
 543        long personaId,
 544        string cedula,
 545        int version,
 546        decimal nominalBruto,
 547        AportesLegales aportes,
 548        InsumosPeriodo insumos,
 549        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo)
 550    {
 32551        var request = new CalcularIrpfRequest
 32552        {
 32553            IngresoGravados = nominalBruto,
 32554            Montepio = aportes.Montepio,
 32555            Fonasa = aportes.Fonasa,
 32556            Frl = aportes.Frl,
 32557            Deducciones = await CrearDeduccionesIrpfAsync(personaId),
 32558            Parametros = new ParametrosIrpfDto
 32559            {
 32560                BpcMensual = insumos.ParametroPeriodo.BpcAnual / 12m,
 32561                UmbralDeduccionBpc = insumos.ParametroPeriodo.UmbralDeduccionBpc,
 32562                TasaDeduccionAlta = insumos.ParametroPeriodo.TasaDeduccionAlta,
 32563                TasaDeduccionBaja = insumos.ParametroPeriodo.TasaDeduccionBaja
 32564            },
 32565            Franjas = insumos.FranjasIrpf
 75566                .Select(f => new FranjaIrpfCalculoDto
 75567                {
 75568                    FranjaId = f.Id,
 75569                    NumeroFranja = f.NumeroFranja,
 75570                    DesdeBpc = f.DesdeBpc,
 75571                    HastaBpc = f.HastaBpc,
 75572                    Tasa = f.Tasa
 75573                })
 32574                .ToList()
 32575        };
 576
 32577        var (exito, idIrpf, _) = await _irpfService.CalcularYPersistirAsync(periodoId, personaId, request);
 32578        if (!exito || !idIrpf.HasValue)
 28579            return new ResultadoIrpfLiquidacion(0m, null);
 580
 4581        var irpf = await _irpfService.ObtenerPorIdAsync(idIrpf.Value);
 4582        if (irpf is null)
 0583            return new ResultadoIrpfLiquidacion(0m, null);
 584
 4585        var item = catalogoPorCodigo.TryGetValue(CodigoIrpf, out var catalogoIrpf)
 4586            ? CrearItemLiquidacion(periodoId, cedula, version, catalogoIrpf, -irpf.TotalIrpf)
 4587            : null;
 588
 4589        return new ResultadoIrpfLiquidacion(irpf.TotalIrpf, item);
 32590    }
 591
 592    private async Task<List<DeduccionDto>> CrearDeduccionesIrpfAsync(long personaId)
 593    {
 32594        var dependientes = await _dependienteRepository.GetDependientesByPersonaAsync(personaId);
 595
 32596        return dependientes
 3597            .Where(d => d.Activo)
 32598            .SelectMany(d =>
 32599            {
 3600                if (string.Equals(d.Tipo, "conyuge_sin_ingresos", StringComparison.OrdinalIgnoreCase))
 1601                    return [new DeduccionDto { Tipo = "conyuge_sin_ingresos", Cantidad = 1 }];
 32602
 2603                if (string.Equals(d.Tipo, "hijo", StringComparison.OrdinalIgnoreCase))
 32604                {
 2605                    var tipoHijo = d.Discapacitado ? "hijo_discapacidad" : "hijo_menor";
 2606                    return [new DeduccionDto { Tipo = tipoHijo, Cantidad = 1 }];
 32607                }
 32608
 0609                return Array.Empty<DeduccionDto>();
 32610            })
 32611            .ToList();
 32612    }
 613
 614    private async Task<IReadOnlyList<LiquidacionItem>> CrearItemsDescuentosPersonalesAsync(
 615        long periodoId,
 616        long personaId,
 617        string cedula,
 618        int version,
 619        decimal nominalBruto,
 620        decimal descuentosLegales,
 621        DateTime fechaPeriodo)
 622    {
 32623        var descuentosConfirmados = (await _descuentoPersonalRepository
 32624                .GetConfirmadosPorPersonaAsync(periodoId, personaId))
 10625            .Where(d => d.CatalogoItem is not null)
 32626            // Vigencia (ej. CUOTA_ING): se aplica solo si no venció antes del período
 10627            .Where(d => d.FechaHasta is null || d.FechaHasta >= fechaPeriodo)
 32628            .ToList();
 629
 32630        if (descuentosConfirmados.Count == 0)
 23631            return Array.Empty<LiquidacionItem>();
 632
 633        // Filtrar descuentos sin acreedor asignado (datos previos a la migración de acreedor_id)
 9634        var descuentosConAcreedor = descuentosConfirmados
 10635            .Where(d => d.AcreedorId is not null && d.Acreedor is not null)
 9636            .ToList();
 637
 9638        if (descuentosConAcreedor.Count == 0)
 0639            return Array.Empty<LiquidacionItem>();
 640
 9641        var entradasDeficit = descuentosConAcreedor
 10642            .Select(d => CrearEntradaDeficit(d, personaId))
 9643            .ToList();
 644
 9645        var resultadosDeficit = await _deficitCalculator.AplicarAsync(
 9646            nominalBruto,
 9647            descuentosLegales,
 9648            entradasDeficit);
 649
 19650        var descuentosPorId = descuentosConAcreedor.ToDictionary(d => d.Id);
 9651        var items = new List<LiquidacionItem>();
 652
 38653        foreach (var resultadoDeficit in resultadosDeficit)
 654        {
 10655            if (resultadoDeficit.DescuentoPeriodoId is null || resultadoDeficit.MontoAplicado <= 0m)
 656                continue;
 657
 10658            if (!descuentosPorId.TryGetValue(resultadoDeficit.DescuentoPeriodoId.Value, out var descuento))
 659                continue;
 660
 10661            items.Add(CrearItemLiquidacion(
 10662                periodoId,
 10663                cedula,
 10664                version,
 10665                descuento.CatalogoItem,
 10666                -resultadoDeficit.MontoAplicado));
 667        }
 668
 9669        return items;
 32670    }
 671
 672    private static DescuentoPersonalInput CrearEntradaDeficit(DescuentoPersonalPeriodo descuento, long personaId)
 673    {
 10674        if (descuento.AcreedorId is null)
 0675            throw new InvalidOperationException($"El descuento personal {descuento.Id} no tiene acreedor asignado.");
 676
 10677        if (descuento.Acreedor is null)
 0678            throw new InvalidOperationException($"El descuento personal {descuento.Id} no tiene acreedor cargado.");
 679
 10680        return new DescuentoPersonalInput(
 10681            descuento.Id,
 10682            personaId,
 10683            descuento.CatalogoItem.Codigo,
 10684            descuento.Importe,
 10685            descuento.AcreedorId.Value,
 10686            descuento.Acreedor.CategoriaDeficit,
 10687            descuento.Acreedor.OrdenLegalDeficit,
 10688            descuento.FechaComunicacion ?? descuento.FechaCreacion);
 689    }
 690
 691    private static IReadOnlyList<LiquidacionItem> CrearItemsAportesLegales(
 692        long periodoId,
 693        string cedula,
 694        int version,
 695        AportesLegales aportes,
 696        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo)
 697    {
 32698        var items = new List<LiquidacionItem>();
 699
 32700        AgregarItemAporteLegal(
 32701            items, periodoId, cedula, version, aportes.Montepio,
 32702            BuscarCatalogoItem(catalogoPorCodigo, CodigoMontepioSubalterno, 545, 555),
 32703            codigoFallback: CodigoMontepioSubalterno.ToString(),
 32704            nombreFallback: "MONTEPIO",
 32705            rubroFallback: "5.04");
 706
 707        // Salud: Sanidad Militar por tramos (código 5010/5011/5012) o FONASA por tasa única.
 32708        if (aportes.SanidadCodigo is int sanidadCodigo)
 709        {
 0710            AgregarItemAporteLegal(
 0711                items, periodoId, cedula, version, aportes.Fonasa,
 0712                BuscarCatalogoItem(catalogoPorCodigo, sanidadCodigo),
 0713                codigoFallback: sanidadCodigo.ToString(),
 0714                nombreFallback: "SANIDAD",
 0715                rubroFallback: $"5.{sanidadCodigo % 100:D2}");
 716        }
 717        else
 718        {
 32719            AgregarItemAporteLegal(
 32720                items, periodoId, cedula, version, aportes.Fonasa,
 32721                BuscarCatalogoItem(catalogoPorCodigo, CodigoSanidad, 521, 522, 523, 524, 525, 526, 527, 528),
 32722                codigoFallback: CodigoSanidad.ToString(),
 32723                nombreFallback: "FONASA",
 32724                rubroFallback: "5.010");
 725        }
 726
 32727        AgregarItemAporteLegal(
 32728            items, periodoId, cedula, version, aportes.Frl,
 32729            null,
 32730            codigoFallback: CodigoFrl,
 32731            nombreFallback: "FRL",
 32732            rubroFallback: "000.000");
 733
 32734        return items;
 735    }
 736
 737    private static void AgregarItemAporteLegal(
 738        ICollection<LiquidacionItem> items,
 739        long periodoId,
 740        string cedula,
 741        int version,
 742        decimal importe,
 743        CatalogoItem? catalogoItem,
 744        string codigoFallback,
 745        string nombreFallback,
 746        string rubroFallback)
 747    {
 96748        if (importe <= 0m)
 18749            return;
 750
 78751        if (catalogoItem is not null)
 752        {
 7753            items.Add(CrearItemLiquidacion(periodoId, cedula, version, catalogoItem, -importe));
 7754            return;
 755        }
 756
 71757        items.Add(CrearItemLiquidacion(periodoId, cedula, version, codigoFallback, nombreFallback, -importe, rubroFallba
 71758    }
 759
 760    private static CatalogoItem? BuscarCatalogoItem(
 761        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo,
 762        params int[] codigos)
 763    {
 833764        foreach (var codigo in codigos)
 765        {
 356766            if (catalogoPorCodigo.TryGetValue(codigo, out var catalogoItem))
 7767                return catalogoItem;
 768        }
 769
 57770        return null;
 771    }
 772
 773    private static LiquidacionItem CrearItemLiquidacion(
 774        long periodoId,
 775        string cedula,
 776        int version,
 777        CatalogoItem catalogoItem,
 778        decimal importe)
 779    {
 20780        return CrearItemLiquidacion(
 20781            periodoId, cedula, version,
 20782            catalogoItem.Codigo.ToString(),
 20783            catalogoItem.Nombre,
 20784            importe,
 20785            catalogoItem.ClasificacionSiif ?? "000.000");
 786    }
 787
 788    private static LiquidacionItem CrearItemLiquidacion(
 789        long periodoId,
 790        string cedula,
 791        int version,
 792        string codigoConcepto,
 793        string nombreConcepto,
 794        decimal importe,
 795        string rubroContable)
 796    {
 91797        return new LiquidacionItem
 91798        {
 91799            PeriodoId = periodoId,
 91800            Cedula = cedula,
 91801            Version = version,
 91802            CodigoConcepto = codigoConcepto,
 91803            NombreConcepto = nombreConcepto,
 91804            Importe = importe,
 91805            RubroContable = rubroContable,
 91806            Rubro083 = false,
 91807            FechaCreacion = DateTime.UtcNow
 91808        };
 809    }
 810
 811    private static decimal CalcularSueldoBaseParaTraslado(InsumosPeriodo insumos, long gradoId)
 812    {
 813        const int CodigoSueldoBase     = 11000;
 814        const int CodigoCompOficial    = 42012;
 815        const int CodigoCompSubalterno = 42022;
 816
 32817        var sueldoBase     = insumos.ObtenerRemuneracion(gradoId, CodigoSueldoBase)?.Monto ?? 0m;
 32818        var compOficial    = insumos.ObtenerRemuneracion(gradoId, CodigoCompOficial)?.Monto ?? 0m;
 32819        var compSubalterno = insumos.ObtenerRemuneracion(gradoId, CodigoCompSubalterno)?.Monto ?? 0m;
 820
 32821        return sueldoBase + compOficial + compSubalterno;
 822    }
 823
 824    // Fonasa carries el monto del aporte de salud: FONASA por tasa única (civiles) o
 825    // Sanidad Militar por tramos (militares). SanidadCodigo indica el código del tramo
 826    // aplicado (5010/5011/5012) cuando corresponde a Sanidad Militar; null para FONASA.
 352827    private sealed record AportesLegales(decimal Montepio, decimal Fonasa, decimal Frl, int? SanidadCodigo = null)
 828    {
 32829        public decimal Total => Montepio + Fonasa + Frl;
 830    }
 831
 160832    private sealed record ResultadoIrpfLiquidacion(decimal TotalIrpf, LiquidacionItem? Item);
 833}

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()
ObtenerEstadosAsync()
ObtenerItemsAsync()
ObtenerResumenAsync()
.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()