< 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
84%
Covered lines: 419
Uncovered lines: 79
Coverable lines: 498
Total lines: 775
Line coverage: 84.1%
Branch coverage
82%
Covered branches: 136
Total branches: 164
Branch coverage: 82.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1using FAU.DataAccess;
 2using FAU.DataAccess.Repositories;
 3using FAU.Entidades;
 4using FAU.Logica.Calculadores;
 5using FAU.Logica.Calculators;
 6using FAU.Logica.DTOs;
 7using Microsoft.EntityFrameworkCore;
 8
 9namespace FAU.Logica.Services;
 10
 11public class LiquidacionService : ILiquidacionService
 12{
 13    private const int CodigoIrpf = 517;
 14    private const int CodigoMontepioSubalterno = 550;
 15    private const int CodigoSanidad = 520;
 16    private const string CodigoFrl = "FRL";
 17
 18    // El OG de cada compensación viene de ConfigCompensacion.CatalogoItem
 19    // cargado via InsumosPeriodoLoader (TipoCompensacion → Configuraciones → CatalogoItem)
 20    private static int? ObtenerOGCompensacion(ItemLoteCompensacion comp)
 21    {
 222        var codigo = comp.LoteCompensacion?.TipoCompensacion?.Configuraciones
 223            .FirstOrDefault(c => c.CatalogoItem is not null)
 224            ?.CatalogoItem?.Codigo;
 225        return codigo;
 26    }
 27
 28    private readonly ApplicationDbContext _context;
 29    private readonly IAuditoriaService _auditoriaService;
 30    private readonly ICurrentUserContext _currentUser;
 31    private readonly IInsumosPeriodoLoader _insumosLoader;
 32    private readonly IProcesadorBeneficiosService _procesadorBeneficios;
 33    // Calculadoras de haberes base
 34    private readonly ICalculadorFactory _calculadorFactory;
 35    private readonly ICalcularDiferencia615Service _diferencia615Service;
 36    // Descuentos legales, IRPF y déficit
 37    private readonly IDescuentoPersonalRepository _descuentoPersonalRepository;
 38    private readonly IDependienteRepository _dependienteRepository;
 39    private readonly IDeficitCalculator _deficitCalculator;
 40    private readonly IDescuentosLegalesCalculator _descuentosLegalesCalculator;
 41    private readonly IIrpfService _irpfService;
 42
 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    public async Task<(bool Exito, string? Error)> GenerarLiquidacionAsync(long periodoId, int version)
 72    {
 3473        var periodo = await _context.PeriodosLiquidacion
 3474            .AsNoTracking()
 3475            .FirstOrDefaultAsync(p => p.Id == periodoId && p.Activo);
 76
 3477        if (periodo is null)
 178            return (false, "Período de liquidación no encontrado o no está activo");
 79
 3380        if (version <= 0)
 181            return (false, "La versión debe ser mayor a cero");
 82
 3283        var existItems = await _context.LiquidacionItems
 3284            .AnyAsync(li => li.PeriodoId == periodoId && li.Version == version);
 85
 3286        if (existItems)
 187            return (false, "Ya existe una liquidación generada para ese período y versión");
 88
 89        InsumosPeriodo insumos;
 90        try
 91        {
 3192            insumos = await _insumosLoader.CargarAsync(periodoId);
 3193        }
 094        catch (InvalidOperationException ex) when (ex.Message.Contains("arámetro"))
 95        {
 096            return (false, $"No se encontraron parámetros para el período {periodo.Anio}/{periodo.Mes}");
 97        }
 98
 99        // Usamos el mismo patrón de Join que funciona con el HasConversion de Estado enum→string
 31100        var rlRaw = await _context.RelacionesLaborales
 31101            .AsNoTracking()
 31102            .Where(r => r.Estado == EstadoRelacion.Activo)
 31103            .Join(
 31104                _context.Personas.AsNoTracking(),
 31105                r => r.PersonaId,
 31106                p => p.Id,
 31107                (r, p) => new { RL = r, Persona = p })
 31108            .ToListAsync();
 109
 31110        if (!rlRaw.Any())
 1111            return (false, "No se encontraron relaciones laborales activas para generar la liquidación");
 112
 113        // Cargar navigation properties necesarias para el pipeline de calculadoras
 61114        var situacionIds = rlRaw.Select(x => x.RL.SituacionId).Distinct().ToHashSet();
 61115        var regimenIds   = rlRaw.Select(x => x.RL.RegimenId).Distinct().ToHashSet();
 116
 30117        var situacionesDict = await _context.Situaciones.AsNoTracking()
 30118            .Where(s => situacionIds.Contains(s.Id))
 45119            .ToDictionaryAsync(s => s.Id);
 120
 30121        var regimenesDict = await _context.Regimenes.AsNoTracking()
 30122            .Where(r => regimenIds.Contains(r.Id))
 47123            .ToDictionaryAsync(r => r.Id);
 124
 125        // Materializar RelacionLaboral con navigation properties resueltas en memoria
 30126        var relacionesLaborales = rlRaw.Select(x =>
 30127        {
 31128            x.RL.Persona = x.Persona;
 31129            x.RL.Situacion = situacionesDict.GetValueOrDefault(x.RL.SituacionId)!;
 31130            x.RL.Regimen   = regimenesDict.GetValueOrDefault(x.RL.RegimenId)!;
 31131            return x.RL;
 30132        }).ToList();
 133
 30134        var primerDiaPeriodo = new DateOnly(periodo.Anio, periodo.Mes, 1);
 30135        var itemsToSave = new List<LiquidacionItem>();
 30136        var incidenciasToSave = new List<IncidenciaCalculo>();
 30137        var estadosPersonal = new Dictionary<string, string>();
 138
 139        // Pre-load historico de primas únicas en una sola consulta
 61140        var cedulasPersonas = relacionesLaborales.Select(r => r.Persona.Cedula).ToHashSet();
 30141        var itemsHistoricosGlobal = await _context.LiquidacionItems
 30142            .AsNoTracking()
 30143            .Where(li => cedulasPersonas.Contains(li.Cedula) &&
 30144                         (li.CodigoConcepto == "340" || li.CodigoConcepto == "350" || li.CodigoConcepto == "370"))
 30145            .ToListAsync();
 146
 147        // Índice de catálogo por código (usado por helpers de aportes legales)
 30148        var catalogoPorCodigo = insumos.CatalogoItems
 210149            .GroupBy(i => i.Codigo)
 400150            .ToDictionary(g => g.Key, g => g.First());
 151
 122152        foreach (var rl in relacionesLaborales)
 153        {
 31154            var cedula = rl.Persona.Cedula;
 155            try
 156            {
 31157                var acumuladores = new Acumuladores();
 31158                var itemsHaberes = new List<ItemCalculado>();
 31159                var itemsBeneficios = new List<ItemBeneficioCalculado>();
 160
 161                // --- Paso 1-3: Calculador de haberes base ---
 31162                var calculador = rl.Situacion?.Calculador is not null
 31163                    ? _calculadorFactory.Resolver(rl.Situacion.Calculador)
 31164                    : null;
 165
 31166                if (calculador is not null)
 167                {
 15168                    var novedadEximidos = insumos.ObtenerNovedadEximidos(rl.PersonaId);
 15169                    var factorProrrateo = (novedadEximidos is not null && novedadEximidos.DiasAfectados < 30)
 15170                        ? novedadEximidos.DiasAfectados / 30m
 15171                        : 1m;
 172
 15173                    var resultadoCalculo = await calculador.CalcularAsync(rl, insumos, acumuladores);
 174
 15175                    if (factorProrrateo < 1m)
 176                    {
 24177                        foreach (var item in resultadoCalculo.Items)
 10178                            item.Importe = Math.Round(item.Importe * factorProrrateo, 2);
 179
 2180                        acumuladores.Reiniciar();
 24181                        foreach (var item in resultadoCalculo.Items)
 182                        {
 58183                            var ci = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == item.CodigoConcepto);
 19184                            if (ci is not null) acumuladores.AgregarItem(item.Importe, ci);
 185                        }
 186                    }
 187
 15188                    itemsHaberes.AddRange(resultadoCalculo.Items);
 189
 30190                    foreach (var inc in resultadoCalculo.Incidencias)
 0191                        incidenciasToSave.Add(new IncidenciaCalculo
 0192                        {
 0193                            PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 0194                            Tipo = TipoIncidenciaCalculo.CalculoSimplificado,
 0195                            Descripcion = inc, CreadoEn = DateTime.UtcNow
 0196                        });
 197                }
 198                else
 199                {
 16200                    incidenciasToSave.Add(new IncidenciaCalculo
 16201                    {
 16202                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 16203                        Tipo = TipoIncidenciaCalculo.ErrorCalculo,
 16204                        Descripcion = $"Calculador no registrado: {rl.Situacion?.Calculador}",
 16205                        CreadoEn = DateTime.UtcNow
 16206                    });
 207                }
 208
 209                // --- Paso 4: Beneficios sociales ---
 31210                var beneficios = insumos.ObtenerBeneficios(rl.PersonaId);
 31211                var itemsHistoricoPersona = itemsHistoricosGlobal.Where(h => h.Cedula == cedula).ToList();
 31212                var sueldoBasePersona = CalcularSueldoBaseParaTraslado(insumos, rl.GradoId);
 31213                var resultadoBeneficios = _procesadorBeneficios.ProcesarBeneficios(
 31214                    rl.PersonaId, beneficios, insumos.ParametroPeriodo, insumos.CatalogoItems,
 31215                    itemsHistoricoPersona, primerDiaPeriodo,
 31216                    insumos.BeneficiariosPorBeneficio, insumos.InstructivoAsigFamiliar,
 31217                    insumos.InstructivoPsf, insumos.TablaPorHogar,
 31218                    sueldoBaseParaTraslado: sueldoBasePersona);
 31219                itemsBeneficios.AddRange(resultadoBeneficios.Items);
 220
 62221                foreach (var inc in resultadoBeneficios.Incidencias)
 0222                    incidenciasToSave.Add(new IncidenciaCalculo
 0223                    {
 0224                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 0225                        Tipo = inc.Tipo, Descripcion = inc.Descripcion, CreadoEn = DateTime.UtcNow
 0226                    });
 227
 228                // --- Paso 5: Compensaciones por lote ---
 229                // El OG viene de ConfigCompensacion.CatalogoItem (no hardcodeado)
 31230                var compensaciones = insumos.ObtenerCompensaciones(rl.PersonaId);
 66231                foreach (var comp in compensaciones)
 232                {
 2233                    var ogCode = ObtenerOGCompensacion(comp);
 2234                    if (ogCode is null) continue;   // sin config de OG → saltear
 235
 2236                    var monto = comp.Monto ?? 0m;
 2237                    if (monto <= 0) continue;
 238
 18239                    var ci = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == ogCode.Value);
 2240                    itemsHaberes.Add(new ItemCalculado
 2241                    {
 2242                        CodigoConcepto = ogCode.Value,
 2243                        NombreConcepto = ci?.Nombre ?? comp.LoteCompensacion?.TipoCompensacion?.Nombre ?? "",
 2244                        Importe = monto
 2245                    });
 4246                    if (ci is not null) acumuladores.AgregarItem(monto, ci);
 247                }
 248
 249                // --- Paso 6: Diferencia por ascenso 042.615 ---
 31250                var diffAscenso = insumos.ObtenerDiferenciaAscenso(rl.PersonaId);
 31251                if (diffAscenso is not null && diffAscenso.Monto > 0)
 252                {
 253                    const int Cod615 = 42615;
 9254                    var ci615 = insumos.CatalogoItems.FirstOrDefault(c => c.Codigo == Cod615);
 1255                    itemsHaberes.Add(new ItemCalculado
 1256                    {
 1257                        CodigoConcepto = Cod615,
 1258                        NombreConcepto = ci615?.Nombre ?? "Diferencia por Ascenso (042.615)",
 1259                        Importe = diffAscenso.Monto
 1260                    });
 2261                    if (ci615 is not null) acumuladores.AgregarItem(diffAscenso.Monto, ci615);
 262                }
 263
 264                // --- Paso 7: Aportes legales (Montepío, FONASA, FRL) ---
 265                // baseSdlAportes = todos haberes positivos + beneficios gravados
 266                // nominalBruto   = todos haberes positivos + todos beneficios positivos
 267                // Se suma directamente desde itemsHaberes porque contribuye_w_nominal
 268                // puede no estar configurado en el catálogo (en ese caso WNominal=0).
 193269                var totalHaberesPositivos = itemsHaberes.Where(i => i.Importe > 0m).Sum(i => i.Importe);
 31270                var totalBeneficiosGravados = resultadoBeneficios.Items
 23271                    .Where(i => i.Importe > 0m
 23272                                && catalogoPorCodigo.TryGetValue(i.CodigoConcepto, out var ciBen)
 23273                                && (ciBen.EsGravado || ciBen.ContribuyeWNominal))
 47274                    .Sum(i => i.Importe);
 31275                var baseSdlAportes = totalHaberesPositivos + totalBeneficiosGravados;
 31276                var nominalBruto = totalHaberesPositivos
 73277                    + resultadoBeneficios.Items.Where(i => i.Importe > 0m).Sum(i => i.Importe);
 278
 31279                var aportes = await CalcularAportesLegalesAsync(rl.RegimenId, baseSdlAportes, insumos);
 31280                var itemsAportesLegales = CrearItemsAportesLegales(periodoId, cedula, version, aportes, catalogoPorCodig
 31281                itemsToSave.AddRange(itemsAportesLegales);
 282
 283                // --- Paso 8: IRPF ---
 284                // Eliminar registro anterior para que CalcularYPersistirAsync siempre inserte limpio
 31285                var irpfAnterior = await _context.Set<IrpfMensual>()
 31286                    .FirstOrDefaultAsync(i => i.LiquidacionId == periodoId && i.PersonaId == rl.PersonaId);
 31287                if (irpfAnterior is not null)
 288                {
 0289                    var detallesAnteriores = await _context.Set<IrpfDetalleFranjas>()
 0290                        .Where(d => d.IrpfMensualId == irpfAnterior.Id)
 0291                        .ToListAsync();
 0292                    _context.Set<IrpfDetalleFranjas>().RemoveRange(detallesAnteriores);
 0293                    _context.Set<IrpfMensual>().Remove(irpfAnterior);
 0294                    await _context.SaveChangesAsync();
 295                }
 296
 31297                var resultadoIrpf = await CalcularIrpfAsync(
 31298                    periodoId, rl.PersonaId, cedula, version,
 31299                    nominalBruto, aportes, insumos, catalogoPorCodigo);
 300
 301                // Descartar entidades IrpfMensual trackeadas si la persistencia falló,
 302                // para que no se reintentan en el SaveChangesAsync final.
 62303                foreach (var entry in _context.ChangeTracker.Entries<IrpfMensual>()
 0304                             .Where(e => e.State == Microsoft.EntityFrameworkCore.EntityState.Added)
 31305                             .ToList())
 0306                    entry.State = Microsoft.EntityFrameworkCore.EntityState.Detached;
 307
 31308                if (resultadoIrpf.Item is not null && resultadoIrpf.TotalIrpf != 0)
 3309                    itemsToSave.Add(resultadoIrpf.Item);
 310                else
 311                {
 312                    // Diagnóstico: registrar siempre el resultado del cálculo IRPF para debugging
 28313                    var tieneCatalogo = catalogoPorCodigo.ContainsKey(CodigoIrpf);
 28314                    incidenciasToSave.Add(new IncidenciaCalculo
 28315                    {
 28316                        PeriodoId = periodoId, Version = version, PersonaId = rl.PersonaId,
 28317                        Tipo = TipoIncidenciaCalculo.CalculoSimplificado,
 28318                        Descripcion = $"IRPF diag: TotalIrpf={resultadoIrpf.TotalIrpf:F2} ItemNull={resultadoIrpf.Item i
 28319                        CreadoEn = DateTime.UtcNow
 28320                    });
 321                }
 322
 323                // --- Paso 9: Descuentos personales con déficit ---
 324                // Excluir códigos ya emitidos como aportes para no contarlos dos veces en el déficit
 31325                var codigosAportesEmitidos = itemsAportesLegales
 75326                    .Where(i => !string.IsNullOrWhiteSpace(i.CodigoConcepto))
 75327                    .Select(i => i.CodigoConcepto)
 31328                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
 31329                var descuentosLegalesEnBeneficios = resultadoBeneficios.Items
 23330                    .Where(i => i.Importe < 0m
 23331                                && catalogoPorCodigo.TryGetValue(i.CodigoConcepto, out var ci)
 23332                                && string.Equals(ci.Tipo, "descuento_legal", StringComparison.OrdinalIgnoreCase)
 23333                                && !codigosAportesEmitidos.Contains(i.CodigoConcepto.ToString()))
 34334                    .Sum(i => Math.Abs(i.Importe));
 31335                var totalDescuentosLegales = aportes.Total + resultadoIrpf.TotalIrpf + descuentosLegalesEnBeneficios;
 31336                var itemsDescuentosPersonales = await CrearItemsDescuentosPersonalesAsync(
 31337                    periodoId, rl.PersonaId, cedula, version,
 31338                    nominalBruto, totalDescuentosLegales);
 31339                itemsToSave.AddRange(itemsDescuentosPersonales);
 340
 341                // --- Paso 10: Estado ---
 31342                var tieneError = incidenciasToSave.Any(i =>
 63343                    i.PersonaId == rl.PersonaId && i.Tipo == TipoIncidenciaCalculo.ErrorCalculo);
 31344                var liquido = nominalBruto - totalDescuentosLegales
 41345                    - itemsDescuentosPersonales.Sum(i => Math.Abs(i.Importe));
 31346                var estado = tieneError ? "ERROR" : (liquido < 0 ? "REVISAR" : "CALCULADO");
 31347                estadosPersonal[cedula] = estado;
 348
 349                // --- Paso 11: Persistir ítems de haberes y beneficios ---
 224350                foreach (var item in itemsHaberes)
 351                {
 81352                    itemsToSave.Add(new LiquidacionItem
 81353                    {
 81354                        PeriodoId = periodoId,
 81355                        Cedula = cedula,
 81356                        Version = version,
 81357                        CodigoConcepto = item.CodigoConcepto.ToString(),
 81358                        NombreConcepto = item.NombreConcepto,
 81359                        Importe = item.Importe,
 81360                        RubroContable = string.Empty,
 81361                        Rubro083 = false,
 81362                        FechaCreacion = DateTime.UtcNow
 81363                    });
 364                }
 108365                foreach (var item in itemsBeneficios)
 366                {
 23367                    itemsToSave.Add(new LiquidacionItem
 23368                    {
 23369                        PeriodoId = periodoId,
 23370                        Cedula = cedula,
 23371                        Version = version,
 23372                        CodigoConcepto = item.CodigoConcepto.ToString(),
 23373                        NombreConcepto = item.NombreConcepto,
 23374                        Importe = item.Importe,
 23375                        RubroContable = item.RubroContable,
 23376                        Rubro083 = false,
 23377                        ClaveEvento = item.ClaveEvento,
 23378                        FechaCreacion = DateTime.UtcNow
 23379                    });
 380                }
 31381            }
 0382            catch (Exception ex)
 383            {
 0384                estadosPersonal[cedula] = "ERROR";
 0385                incidenciasToSave.Add(new IncidenciaCalculo
 0386                {
 0387                    PeriodoId = periodoId,
 0388                    Version = version,
 0389                    PersonaId = rl.PersonaId,
 0390                    Tipo = TipoIncidenciaCalculo.ErrorCalculo,
 0391                    Descripcion = $"Error en pipeline de liquidación: {ex.Message}",
 0392                    CreadoEn = DateTime.UtcNow
 0393                });
 0394            }
 31395        }
 396
 61397        var estados = relacionesLaborales.Select(rl => new LiquidacionEstadoPersonal
 61398        {
 61399            PeriodoId = periodoId,
 61400            Cedula = rl.Persona.Cedula,
 61401            Version = version,
 61402            Estado = estadosPersonal.TryGetValue(rl.Persona.Cedula, out var e) ? e : "PENDIENTE",
 61403            FechaActualizacion = DateTime.UtcNow
 61404        }).ToList();
 405
 30406        await _context.LiquidacionEstadosPersonal.AddRangeAsync(estados);
 407
 30408        if (itemsToSave.Count > 0)
 30409            await _context.LiquidacionItems.AddRangeAsync(itemsToSave);
 410
 30411        if (incidenciasToSave.Count > 0)
 30412            await _context.IncidenciasCalculo.AddRangeAsync(incidenciasToSave);
 413
 222414        var totalImporte = itemsToSave.Sum(i => i.Importe);
 30415        var resumen = new LiquidacionResumen
 30416        {
 30417            PeriodoId = periodoId,
 30418            Version = version,
 30419            TotalImporte = totalImporte,
 30420            Rubro083 = false,
 30421            Descripcion = "Liquidación generada"
 30422        };
 30423        await _context.LiquidacionResumenes.AddAsync(resumen);
 424
 30425        await _context.SaveChangesAsync();
 426
 30427        await _auditoriaService.LogAuditoriaAsync(
 30428            _currentUser.UserId ?? 0,
 30429            AccionEnum.GenerarLiquidacion,
 30430            ContextoEnum.Liquidaciones,
 30431            _currentUser.Host,
 30432            entidad: nameof(LiquidacionEstadoPersonal),
 30433            entidadId: periodoId,
 30434            detalle: new { periodoId, version, registros = estados.Count, items = itemsToSave.Count, incidencias = incid
 435
 30436        return (true, null);
 34437    }
 438
 439    public async Task<IEnumerable<CalculoEstadoDto>> ObtenerEstadosAsync(long periodoId, int version)
 440    {
 0441        return await _context.LiquidacionEstadosPersonal
 0442            .AsNoTracking()
 0443            .Where(e => e.PeriodoId == periodoId && e.Version == version)
 0444            .Select(e => new CalculoEstadoDto
 0445            {
 0446                Cedula = e.Cedula,
 0447                Version = e.Version,
 0448                Estado = e.Estado,
 0449                MensajeError = e.MensajeError
 0450            })
 0451            .ToListAsync();
 0452    }
 453
 454    public async Task<IEnumerable<CalculoItemDto>> ObtenerItemsAsync(long periodoId, int version)
 455    {
 0456        return await _context.LiquidacionItems
 0457            .AsNoTracking()
 0458            .Where(i => i.PeriodoId == periodoId && i.Version == version)
 0459            .Select(i => new CalculoItemDto
 0460            {
 0461                Cedula = i.Cedula,
 0462                Version = i.Version,
 0463                CodigoConcepto = i.CodigoConcepto,
 0464                NombreConcepto = i.NombreConcepto,
 0465                Importe = i.Importe,
 0466                RubroContable = i.RubroContable,
 0467                ObjetoGasto = i.ObjetoGasto
 0468            })
 0469            .ToListAsync();
 0470    }
 471
 472    public async Task<IEnumerable<LiquidacionResumenDto>> ObtenerResumenAsync(long periodoId, int version)
 473    {
 0474        return await _context.LiquidacionResumenes
 0475            .AsNoTracking()
 0476            .Where(r => r.PeriodoId == periodoId && r.Version == version)
 0477            .Select(r => new LiquidacionResumenDto
 0478            {
 0479                Version = r.Version,
 0480                ProgramaId = r.ProgramaId,
 0481                RegimenId = r.RegimenId,
 0482                Rubro083 = r.Rubro083,
 0483                TotalImporte = r.TotalImporte,
 0484                Descripcion = r.Descripcion
 0485            })
 0486            .ToListAsync();
 0487    }
 488
 489    private async Task<AportesLegales> CalcularAportesLegalesAsync(long regimenId, decimal baseSdl, InsumosPeriodo insum
 490    {
 31491        var regimen = await _context.Regimenes.AsNoTracking().FirstOrDefaultAsync(r => r.Id == regimenId);
 31492        var tasaMontepio = regimen is not null && regimen.EsLeyVieja
 31493            ? insumos.ParametroPeriodo.TasaMontepioLeyVieja
 31494            : insumos.ParametroPeriodo.TasaMontepioLeyNueva;
 495
 31496        return new AportesLegales(
 31497            _descuentosLegalesCalculator.CalcularMontepio(baseSdl, tasaMontepio),
 31498            _descuentosLegalesCalculator.CalcularFonasa(baseSdl, insumos.ParametroPeriodo.TasaFonasa),
 31499            _descuentosLegalesCalculator.CalcularFrl(baseSdl, insumos.ParametroPeriodo.TasaFrl));
 31500    }
 501
 502    private async Task<ResultadoIrpfLiquidacion> CalcularIrpfAsync(
 503        long periodoId,
 504        long personaId,
 505        string cedula,
 506        int version,
 507        decimal nominalBruto,
 508        AportesLegales aportes,
 509        InsumosPeriodo insumos,
 510        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo)
 511    {
 31512        var request = new CalcularIrpfRequest
 31513        {
 31514            IngresoGravados = nominalBruto,
 31515            Montepio = aportes.Montepio,
 31516            Fonasa = aportes.Fonasa,
 31517            Frl = aportes.Frl,
 31518            Deducciones = await CrearDeduccionesIrpfAsync(personaId),
 31519            Parametros = new ParametrosIrpfDto
 31520            {
 31521                BpcMensual = insumos.ParametroPeriodo.BpcAnual / 12m,
 31522                UmbralDeduccionBpc = insumos.ParametroPeriodo.UmbralDeduccionBpc,
 31523                TasaDeduccionAlta = insumos.ParametroPeriodo.TasaDeduccionAlta,
 31524                TasaDeduccionBaja = insumos.ParametroPeriodo.TasaDeduccionBaja
 31525            },
 31526            Franjas = insumos.FranjasIrpf
 72527                .Select(f => new FranjaIrpfCalculoDto
 72528                {
 72529                    FranjaId = f.Id,
 72530                    NumeroFranja = f.NumeroFranja,
 72531                    DesdeBpc = f.DesdeBpc,
 72532                    HastaBpc = f.HastaBpc,
 72533                    Tasa = f.Tasa
 72534                })
 31535                .ToList()
 31536        };
 537
 31538        var (exito, idIrpf, _) = await _irpfService.CalcularYPersistirAsync(periodoId, personaId, request);
 31539        if (!exito || !idIrpf.HasValue)
 27540            return new ResultadoIrpfLiquidacion(0m, null);
 541
 4542        var irpf = await _irpfService.ObtenerPorIdAsync(idIrpf.Value);
 4543        if (irpf is null)
 0544            return new ResultadoIrpfLiquidacion(0m, null);
 545
 4546        var item = catalogoPorCodigo.TryGetValue(CodigoIrpf, out var catalogoIrpf)
 4547            ? CrearItemLiquidacion(periodoId, cedula, version, catalogoIrpf, -irpf.TotalIrpf)
 4548            : null;
 549
 4550        return new ResultadoIrpfLiquidacion(irpf.TotalIrpf, item);
 31551    }
 552
 553    private async Task<List<DeduccionDto>> CrearDeduccionesIrpfAsync(long personaId)
 554    {
 31555        var dependientes = await _dependienteRepository.GetDependientesByPersonaAsync(personaId);
 556
 31557        return dependientes
 3558            .Where(d => d.Activo)
 31559            .SelectMany(d =>
 31560            {
 3561                if (string.Equals(d.Tipo, "conyuge_sin_ingresos", StringComparison.OrdinalIgnoreCase))
 1562                    return [new DeduccionDto { Tipo = "conyuge_sin_ingresos", Cantidad = 1 }];
 31563
 2564                if (string.Equals(d.Tipo, "hijo", StringComparison.OrdinalIgnoreCase))
 31565                {
 2566                    var tipoHijo = d.Discapacitado ? "hijo_discapacidad" : "hijo_menor";
 2567                    return [new DeduccionDto { Tipo = tipoHijo, Cantidad = 1 }];
 31568                }
 31569
 0570                return Array.Empty<DeduccionDto>();
 31571            })
 31572            .ToList();
 31573    }
 574
 575    private async Task<IReadOnlyList<LiquidacionItem>> CrearItemsDescuentosPersonalesAsync(
 576        long periodoId,
 577        long personaId,
 578        string cedula,
 579        int version,
 580        decimal nominalBruto,
 581        decimal descuentosLegales)
 582    {
 31583        var descuentosConfirmados = (await _descuentoPersonalRepository
 31584                .GetConfirmadosPorPersonaAsync(periodoId, personaId))
 10585            .Where(d => d.CatalogoItem is not null)
 31586            .ToList();
 587
 31588        if (descuentosConfirmados.Count == 0)
 22589            return Array.Empty<LiquidacionItem>();
 590
 591        // Filtrar descuentos sin acreedor asignado (datos previos a la migración de acreedor_id)
 9592        var descuentosConAcreedor = descuentosConfirmados
 10593            .Where(d => d.AcreedorId is not null && d.Acreedor is not null)
 9594            .ToList();
 595
 9596        if (descuentosConAcreedor.Count == 0)
 0597            return Array.Empty<LiquidacionItem>();
 598
 9599        var entradasDeficit = descuentosConAcreedor
 10600            .Select(d => CrearEntradaDeficit(d, personaId))
 9601            .ToList();
 602
 9603        var resultadosDeficit = await _deficitCalculator.AplicarAsync(
 9604            nominalBruto,
 9605            descuentosLegales,
 9606            entradasDeficit);
 607
 19608        var descuentosPorId = descuentosConAcreedor.ToDictionary(d => d.Id);
 9609        var items = new List<LiquidacionItem>();
 610
 38611        foreach (var resultadoDeficit in resultadosDeficit)
 612        {
 10613            if (resultadoDeficit.DescuentoPeriodoId is null || resultadoDeficit.MontoAplicado <= 0m)
 614                continue;
 615
 10616            if (!descuentosPorId.TryGetValue(resultadoDeficit.DescuentoPeriodoId.Value, out var descuento))
 617                continue;
 618
 10619            items.Add(CrearItemLiquidacion(
 10620                periodoId,
 10621                cedula,
 10622                version,
 10623                descuento.CatalogoItem,
 10624                -resultadoDeficit.MontoAplicado));
 625        }
 626
 9627        return items;
 31628    }
 629
 630    private static DescuentoPersonalInput CrearEntradaDeficit(DescuentoPersonalPeriodo descuento, long personaId)
 631    {
 10632        if (descuento.AcreedorId is null)
 0633            throw new InvalidOperationException($"El descuento personal {descuento.Id} no tiene acreedor asignado.");
 634
 10635        if (descuento.Acreedor is null)
 0636            throw new InvalidOperationException($"El descuento personal {descuento.Id} no tiene acreedor cargado.");
 637
 10638        return new DescuentoPersonalInput(
 10639            descuento.Id,
 10640            personaId,
 10641            descuento.CatalogoItem.Codigo,
 10642            descuento.Importe,
 10643            descuento.AcreedorId.Value,
 10644            descuento.Acreedor.CategoriaDeficit,
 10645            descuento.Acreedor.OrdenLegalDeficit,
 10646            descuento.FechaComunicacion ?? descuento.FechaCreacion);
 647    }
 648
 649    private static IReadOnlyList<LiquidacionItem> CrearItemsAportesLegales(
 650        long periodoId,
 651        string cedula,
 652        int version,
 653        AportesLegales aportes,
 654        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo)
 655    {
 31656        var items = new List<LiquidacionItem>();
 657
 31658        AgregarItemAporteLegal(
 31659            items, periodoId, cedula, version, aportes.Montepio,
 31660            BuscarCatalogoItem(catalogoPorCodigo, CodigoMontepioSubalterno, 545, 555),
 31661            codigoFallback: CodigoMontepioSubalterno.ToString(),
 31662            nombreFallback: "MONTEPIO",
 31663            rubroFallback: "5.04");
 664
 31665        AgregarItemAporteLegal(
 31666            items, periodoId, cedula, version, aportes.Fonasa,
 31667            BuscarCatalogoItem(catalogoPorCodigo, CodigoSanidad, 521, 522, 523, 524, 525, 526, 527, 528),
 31668            codigoFallback: CodigoSanidad.ToString(),
 31669            nombreFallback: "FONASA",
 31670            rubroFallback: "5.010");
 671
 31672        AgregarItemAporteLegal(
 31673            items, periodoId, cedula, version, aportes.Frl,
 31674            null,
 31675            codigoFallback: CodigoFrl,
 31676            nombreFallback: "FRL",
 31677            rubroFallback: "000.000");
 678
 31679        return items;
 680    }
 681
 682    private static void AgregarItemAporteLegal(
 683        ICollection<LiquidacionItem> items,
 684        long periodoId,
 685        string cedula,
 686        int version,
 687        decimal importe,
 688        CatalogoItem? catalogoItem,
 689        string codigoFallback,
 690        string nombreFallback,
 691        string rubroFallback)
 692    {
 93693        if (importe <= 0m)
 18694            return;
 695
 75696        if (catalogoItem is not null)
 697        {
 7698            items.Add(CrearItemLiquidacion(periodoId, cedula, version, catalogoItem, -importe));
 7699            return;
 700        }
 701
 68702        items.Add(CrearItemLiquidacion(periodoId, cedula, version, codigoFallback, nombreFallback, -importe, rubroFallba
 68703    }
 704
 705    private static CatalogoItem? BuscarCatalogoItem(
 706        IReadOnlyDictionary<int, CatalogoItem> catalogoPorCodigo,
 707        params int[] codigos)
 708    {
 805709        foreach (var codigo in codigos)
 710        {
 344711            if (catalogoPorCodigo.TryGetValue(codigo, out var catalogoItem))
 7712                return catalogoItem;
 713        }
 714
 55715        return null;
 716    }
 717
 718    private static LiquidacionItem CrearItemLiquidacion(
 719        long periodoId,
 720        string cedula,
 721        int version,
 722        CatalogoItem catalogoItem,
 723        decimal importe)
 724    {
 20725        return CrearItemLiquidacion(
 20726            periodoId, cedula, version,
 20727            catalogoItem.Codigo.ToString(),
 20728            catalogoItem.Nombre,
 20729            importe,
 20730            catalogoItem.ClasificacionSiif ?? "000.000");
 731    }
 732
 733    private static LiquidacionItem CrearItemLiquidacion(
 734        long periodoId,
 735        string cedula,
 736        int version,
 737        string codigoConcepto,
 738        string nombreConcepto,
 739        decimal importe,
 740        string rubroContable)
 741    {
 88742        return new LiquidacionItem
 88743        {
 88744            PeriodoId = periodoId,
 88745            Cedula = cedula,
 88746            Version = version,
 88747            CodigoConcepto = codigoConcepto,
 88748            NombreConcepto = nombreConcepto,
 88749            Importe = importe,
 88750            RubroContable = rubroContable,
 88751            Rubro083 = false,
 88752            FechaCreacion = DateTime.UtcNow
 88753        };
 754    }
 755
 756    private static decimal CalcularSueldoBaseParaTraslado(InsumosPeriodo insumos, long gradoId)
 757    {
 758        const int CodigoSueldoBase     = 11000;
 759        const int CodigoCompOficial    = 42012;
 760        const int CodigoCompSubalterno = 42022;
 761
 31762        var sueldoBase     = insumos.ObtenerRemuneracion(gradoId, CodigoSueldoBase)?.Monto ?? 0m;
 31763        var compOficial    = insumos.ObtenerRemuneracion(gradoId, CodigoCompOficial)?.Monto ?? 0m;
 31764        var compSubalterno = insumos.ObtenerRemuneracion(gradoId, CodigoCompSubalterno)?.Monto ?? 0m;
 765
 31766        return sueldoBase + compOficial + compSubalterno;
 767    }
 768
 310769    private sealed record AportesLegales(decimal Montepio, decimal Fonasa, decimal Frl)
 770    {
 31771        public decimal Total => Montepio + Fonasa + Frl;
 772    }
 773
 155774    private sealed record ResultadoIrpfLiquidacion(decimal TotalIrpf, LiquidacionItem? Item);
 775}

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()
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()