< Summary

Information
Class: FAU.Logica.Services.IrpfCalculator
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/IrpfCalculator.cs
Line coverage
98%
Covered lines: 74
Uncovered lines: 1
Coverable lines: 75
Total lines: 179
Line coverage: 98.6%
Branch coverage
96%
Covered branches: 27
Total branches: 28
Branch coverage: 96.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
CalcularDeducciones(...)100%1212100%
AplicarFranjas(...)91.66%121296.55%
Calcular(...)100%44100%

File(s)

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

#LineLine coverage
 1namespace FAU.Logica.Services;
 2
 3using FAU.Logica.DTOs;
 4
 5/// <summary>
 6/// Calculadora IRPF stateless.
 7/// Implementa la lógica de cálculo de deducciones IRPF y aplicación de franjas progresivas.
 8/// No accede a la BD - recibe todos los datos por parámetro.
 9///
 10/// Flujo:
 11/// 1. CalcularDeducciones() - calcula aportes, hijos, cónyuge, profesional
 12/// 2. AplicarFranjas() - aplica 7 franjas progresivas a la base imponible neta
 13/// 3. Calcular() - orquesta el flujo completo y devuelve ResultadoIrpf
 14/// </summary>
 15public class IrpfCalculator
 16{
 17    /// <summary>
 18    /// Calcula todas las deducciones IRPF en el orden correcto.
 19    /// Orden: (1) aportes, (2) dependientes, (3) profesional.
 20    /// </summary>
 21    public DetalleDeduccionesResultado CalcularDeducciones(
 22        decimal ingresoGravado,
 23        decimal montepio,
 24        decimal fonasa,
 25        decimal frl,
 26        List<DeduccionDto> deducciones,
 27        ParametrosIrpfDto parametros)
 28    {
 1029        var resultado = new DetalleDeduccionesResultado();
 30
 31        // 1. Deducción por aportes jubilatorios
 1032        resultado.Aportes = montepio + fonasa + frl;
 33
 34        // 2. Deducción por dependientes (hijos y cónyuge)
 1035        var bpcAnual = parametros.BpcMensual * 12;
 36
 3037        foreach (var deduccion in deducciones)
 38        {
 539            if (deduccion.Tipo == "hijo_menor")
 40            {
 41                // 20 BPC anuales / 12 meses para hijo menor
 242                resultado.Hijos += deduccion.Cantidad * (parametros.BpcMensual * 20 / 12);
 43            }
 344            else if (deduccion.Tipo == "hijo_discapacidad")
 45            {
 46                // 40 BPC anuales / 12 meses para hijo con discapacidad
 147                resultado.Hijos += deduccion.Cantidad * (parametros.BpcMensual * 40 / 12);
 48            }
 249            else if (deduccion.Tipo == "conyuge_sin_ingresos")
 50            {
 51                // 20 BPC anuales / 12 meses para cónyuge sin ingresos
 252                if (deduccion.Cantidad > 0)
 53                {
 254                    resultado.Conyuge = parametros.BpcMensual * 20 / 12;
 55                }
 56            }
 57        }
 58
 59        // 3. Deducción profesional
 60        // Determinar tasa: 8% (0.08) si base >= umbral BPC, 14% (0.14) si < umbral BPC
 1061        var umbralEnPesos = parametros.UmbralDeduccionBpc * parametros.BpcMensual;
 1062        var tasaProfesional = ingresoGravado >= umbralEnPesos
 1063            ? parametros.TasaDeduccionAlta  // 0.08
 1064            : parametros.TasaDeduccionBaja;  // 0.14
 65
 1066        resultado.Profesional = ingresoGravado * tasaProfesional;
 67
 1068        return resultado;
 69    }
 70
 71    /// <summary>
 72    /// Aplica las 7 franjas progresivas IRPF a la base imponible neta.
 73    /// Retorna (TotalIrpf, DetallePorFranja[]).
 74    /// </summary>
 75    public (decimal TotalIrpf, List<DetalleFranjaResultado> Detalles) AplicarFranjas(
 76        decimal baseImponibleNeta,
 77        List<FranjaIrpfCalculoDto> franjas,
 78        decimal bpcMensual)
 79    {
 980        var totalIrpf = decimal.Zero;
 981        var detalles = new List<DetalleFranjaResultado>();
 82
 83        // Clampear a 0 si es negativa
 984        if (baseImponibleNeta < 0)
 85        {
 186            baseImponibleNeta = 0;
 87        }
 88
 989        var restante = baseImponibleNeta;
 90
 10791        foreach (var franja in franjas.OrderBy(f => f.NumeroFranja))
 92        {
 2393            if (restante <= 0)
 794                break;
 95
 96            // Calcular rango de la franja en pesos
 1697            var limiteInf = franja.DesdeBpc * bpcMensual;
 1698            var limiteSup = franja.HastaBpc.HasValue
 1699                ? franja.HastaBpc.Value * bpcMensual
 16100                : decimal.MaxValue;
 101
 16102            var ancho = limiteSup - limiteInf;
 103
 104            // Monto que cae en esta franja
 16105            var montoEnFranja = Math.Min(restante, ancho);
 16106            if (montoEnFranja < 0)
 0107                montoEnFranja = 0;
 108
 109            // IRPF en esta franja
 16110            var impuestoFranja = montoEnFranja * franja.Tasa;
 111
 16112            if (montoEnFranja > 0)
 113            {
 16114                detalles.Add(new DetalleFranjaResultado
 16115                {
 16116                    FranjaId = franja.FranjaId,
 16117                    NumeroFranja = franja.NumeroFranja,
 16118                    BaseFranja = montoEnFranja,
 16119                    Tasa = franja.Tasa,
 16120                    Impuesto = impuestoFranja
 16121                });
 122
 16123                totalIrpf += impuestoFranja;
 124            }
 125
 16126            restante -= ancho;
 127        }
 128
 9129        return (totalIrpf, detalles);
 130    }
 131
 132    /// <summary>
 133    /// Calcula IRPF completo: deducciones + aplicación de franjas.
 134    /// Orquesta el flujo completo y retorna ResultadoIrpf con todos los datos.
 135    /// </summary>
 136    public ResultadoIrpf Calcular(CalcularIrpfRequest request)
 137    {
 138        // 1. Calcular deducciones
 5139        var deducciones = CalcularDeducciones(
 5140            request.IngresoGravados,
 5141            request.Montepio,
 5142            request.Fonasa,
 5143            request.Frl,
 5144            request.Deducciones,
 5145            request.Parametros);
 146
 147        // 2. Calcular base imponible neta
 5148        var totalDeducciones = deducciones.Aportes + deducciones.Hijos + deducciones.Conyuge + deducciones.Profesional;
 5149        var baseImponibleNeta = request.IngresoGravados - totalDeducciones;
 150
 151        // Clampear a 0 si es negativa
 5152        if (baseImponibleNeta < 0)
 2153            baseImponibleNeta = 0;
 154
 155        // 3. Aplicar franjas
 5156        var (totalIrpf, detalleFranjas) = AplicarFranjas(
 5157            baseImponibleNeta,
 5158            request.Franjas,
 5159            request.Parametros.BpcMensual);
 160
 161        // 4. Determinar tasa de deducción profesional (para trazabilidad)
 5162        var umbralEnPesos = request.Parametros.UmbralDeduccionBpc * request.Parametros.BpcMensual;
 5163        var tasaProfesional = request.IngresoGravados >= umbralEnPesos
 5164            ? request.Parametros.TasaDeduccionAlta
 5165            : request.Parametros.TasaDeduccionBaja;
 166
 167        // 5. Retornar resultado completo
 5168        return new ResultadoIrpf
 5169        {
 5170            IngresoGravado = request.IngresoGravados,
 5171            Deducciones = deducciones,
 5172            TasaDeduccionProfesional = tasaProfesional,
 5173            TotalDeducciones = totalDeducciones,
 5174            BaseImponibleNeta = baseImponibleNeta,
 5175            TotalIrpf = totalIrpf,
 5176            DetallePorFranja = detalleFranjas
 5177        };
 178    }
 179}