< Summary

Information
Class: FAU.Logica.Services.IrpfService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/IrpfService.cs
Line coverage
64%
Covered lines: 90
Uncovered lines: 49
Coverable lines: 139
Total lines: 231
Line coverage: 64.7%
Branch coverage
60%
Covered branches: 45
Total branches: 74
Branch coverage: 60.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
GetFranjasVigentesAsync()100%22100%
GetFranjaByIdAsync()0%620%
CreateFranjaAsync()100%66100%
UpdateFranjaAsync()0%110100%
DeleteFranjaAsync()50%2260%
GetDeduccionesPagedAsync()0%620%
GetDeduccionByIdAsync()0%620%
CreateDeduccionAsync()100%1212100%
UpdateDeduccionAsync()75%211673.33%
ValidarFranja(...)100%1212100%
MapFranjaToDto(...)100%11100%
MapDeduccionToDto(...)0%7280%

File(s)

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

#LineLine coverage
 1using FAU.DataAccess.Repositories;
 2using FAU.Entidades;
 3using FAU.Logica.DTOs;
 4using Microsoft.Extensions.Logging;
 5
 6namespace FAU.Logica.Services;
 7
 8public class IrpfService : IIrpfService
 9{
 110    private static readonly HashSet<string> TiposValidos = new(StringComparer.Ordinal)
 111    {
 112        "hijo_menor", "hijo_discapacidad", "conyuge_sin_ingresos"
 113    };
 14
 15    private readonly IIrpfRepository _irpfRepository;
 16    private readonly IPersonalRepository _personalRepository;
 17    private readonly ILogger<IrpfService> _logger;
 18
 1519    public IrpfService(
 1520        IIrpfRepository irpfRepository,
 1521        IPersonalRepository personalRepository,
 1522        ILogger<IrpfService> logger)
 23    {
 1524        _irpfRepository = irpfRepository;
 1525        _personalRepository = personalRepository;
 1526        _logger = logger;
 1527    }
 28
 29    // ── Franjas ──────────────────────────────────────────────────────────────
 30
 31    public async Task<IEnumerable<FranjaIrpfDto>> GetFranjasVigentesAsync(DateTime? fecha = null)
 32    {
 133        var franjas = await _irpfRepository.GetFranjasVigentesAsync(fecha);
 134        return franjas.Select(MapFranjaToDto);
 135    }
 36
 37    public async Task<FranjaIrpfDto?> GetFranjaByIdAsync(long id)
 38    {
 039        var franja = await _irpfRepository.GetFranjaByIdAsync(id);
 040        return franja == null ? null : MapFranjaToDto(franja);
 041    }
 42
 43    public async Task<(bool Success, FranjaIrpf? Franja, string? Error)> CreateFranjaAsync(CreateFranjaIrpfRequest reque
 44    {
 645        var (valid, error) = ValidarFranja(request);
 946        if (!valid) return (false, null, error);
 47
 348        if (await _irpfRepository.ExisteFranjaAsync(request.NumeroFranja, request.VigenteDesde))
 149            return (false, null, $"Ya existe una franja {request.NumeroFranja} con vigente_desde {request.VigenteDesde:y
 50
 51        // Cerrar franja anterior del mismo número
 252        var franjaAnterior = await _irpfRepository.GetFranjaActivaByNumeroAsync(request.NumeroFranja);
 253        if (franjaAnterior != null)
 54        {
 155            franjaAnterior.VigenteHasta = request.VigenteDesde.AddDays(-1);
 156            await _irpfRepository.UpdateFranjaAsync(franjaAnterior);
 57        }
 58
 259        var franja = new FranjaIrpf
 260        {
 261            NumeroFranja = request.NumeroFranja,
 262            DesdeBpc = request.DesdeBpc,
 263            HastaBpc = request.HastaBpc,
 264            Tasa = request.Tasa,
 265            VigenteDesde = request.VigenteDesde,
 266            VigenteHasta = request.VigenteHasta
 267        };
 68
 269        var creada = await _irpfRepository.CreateFranjaAsync(franja);
 270        _logger.LogInformation("Franja IRPF {Numero} creada con ID {Id}", franja.NumeroFranja, franja.Id);
 271        return (true, creada, null);
 672    }
 73
 74    public async Task<(bool Success, FranjaIrpf? Franja, string? Error)> UpdateFranjaAsync(long id, CreateFranjaIrpfRequ
 75    {
 076        var franja = await _irpfRepository.GetFranjaByIdAsync(id);
 077        if (franja == null) return (false, null, "Franja no encontrada");
 78
 079        var (valid, error) = ValidarFranja(request);
 080        if (!valid) return (false, null, error);
 81
 82        // Verificar duplicado solo si cambió el número o la fecha
 083        if (franja.NumeroFranja != request.NumeroFranja || franja.VigenteDesde.Date != request.VigenteDesde.Date)
 84        {
 085            if (await _irpfRepository.ExisteFranjaAsync(request.NumeroFranja, request.VigenteDesde))
 086                return (false, null, $"Ya existe una franja {request.NumeroFranja} con vigente_desde {request.VigenteDes
 87        }
 88
 089        franja.NumeroFranja = request.NumeroFranja;
 090        franja.DesdeBpc = request.DesdeBpc;
 091        franja.HastaBpc = request.HastaBpc;
 092        franja.Tasa = request.Tasa;
 093        franja.VigenteDesde = request.VigenteDesde;
 094        franja.VigenteHasta = request.VigenteHasta;
 95
 096        var actualizada = await _irpfRepository.UpdateFranjaAsync(franja);
 097        return (true, actualizada, null);
 098    }
 99
 100    public async Task<(bool Success, string? Error)> DeleteFranjaAsync(long id)
 101    {
 1102        var franja = await _irpfRepository.GetFranjaByIdAsync(id);
 2103        if (franja == null) return (false, "Franja no encontrada");
 104
 0105        await _irpfRepository.DeleteFranjaAsync(id);
 0106        return (true, null);
 1107    }
 108
 109    // ── Deducciones ──────────────────────────────────────────────────────────
 110
 111    public async Task<PagedResult<DeduccionIrpfDto>> GetDeduccionesPagedAsync(
 112        int page, int pageSize, long? personaId = null, string? tipo = null, bool? activo = null)
 113    {
 0114        var (items, totalCount) = await _irpfRepository.GetDeduccionesPagedAsync(page, pageSize, personaId, tipo, activo
 0115        return new PagedResult<DeduccionIrpfDto>
 0116        {
 0117            Items = items.Select(MapDeduccionToDto),
 0118            Page = page,
 0119            PageSize = pageSize,
 0120            TotalCount = totalCount
 0121        };
 0122    }
 123
 124    public async Task<DeduccionIrpfDto?> GetDeduccionByIdAsync(long id)
 125    {
 0126        var deduccion = await _irpfRepository.GetDeduccionByIdAsync(id);
 0127        return deduccion == null ? null : MapDeduccionToDto(deduccion);
 0128    }
 129
 130    public async Task<(bool Success, DeduccionIrpf? Deduccion, string? Error)> CreateDeduccionAsync(CreateDeduccionIrpfR
 131    {
 5132        if (!TiposValidos.Contains(request.Tipo))
 1133            return (false, null, $"Tipo inválido. Valores aceptados: {string.Join(", ", TiposValidos)}");
 134
 4135        if (request.VigenteHasta.HasValue && request.VigenteHasta.Value < request.VigenteDesde)
 1136            return (false, null, "vigente_hasta debe ser mayor o igual a vigente_desde");
 137
 138        // Verificar que la persona existe y tiene relación laboral activa
 3139        var relacionActiva = await _personalRepository.GetActivoByPersonaIdAsync(request.PersonaId);
 3140        if (relacionActiva == null)
 1141            return (false, null, "No se encontró personal con ese ID o no tiene relación laboral activa");
 142
 143        // Regla: no duplicar cónyuge activo
 2144        if (request.Tipo == "conyuge_sin_ingresos")
 145        {
 1146            if (await _irpfRepository.ExisteDeduccionActivaAsync(request.PersonaId, "conyuge_sin_ingresos"))
 1147                return (false, null, "Ya existe una deducción activa de tipo conyuge_sin_ingresos para este funcionario"
 148        }
 149
 1150        var deduccion = new DeduccionIrpf
 1151        {
 1152            PersonaId = request.PersonaId,
 1153            Tipo = request.Tipo,
 1154            Descripcion = request.Descripcion,
 1155            VigenteDesde = request.VigenteDesde,
 1156            VigenteHasta = request.VigenteHasta,
 1157            Activo = true
 1158        };
 159
 1160        var creada = await _irpfRepository.CreateDeduccionAsync(deduccion);
 1161        _logger.LogInformation("Deducción IRPF tipo {Tipo} creada para persona {PersonaId}", deduccion.Tipo, deduccion.P
 1162        return (true, creada, null);
 5163    }
 164
 165    public async Task<(bool Success, DeduccionIrpf? Deduccion, string? Error)> UpdateDeduccionAsync(long id, UpdateDeduc
 166    {
 2167        var deduccion = await _irpfRepository.GetDeduccionByIdAsync(id);
 3168        if (deduccion == null) return (false, null, "Deducción no encontrada");
 169
 1170        if (request.Descripcion != null)
 0171            deduccion.Descripcion = request.Descripcion;
 172
 1173        if (request.VigenteHasta.HasValue)
 174        {
 0175            if (request.VigenteHasta.Value < deduccion.VigenteDesde)
 0176                return (false, null, "vigente_hasta debe ser mayor o igual a vigente_desde");
 0177            deduccion.VigenteHasta = request.VigenteHasta;
 178        }
 179
 1180        if (request.Activo.HasValue)
 181        {
 1182            deduccion.Activo = request.Activo.Value;
 183            // Al desactivar sin fecha de cierre, cerrar con hoy
 1184            if (!request.Activo.Value && !deduccion.VigenteHasta.HasValue && !request.VigenteHasta.HasValue)
 1185                deduccion.VigenteHasta = DateTime.UtcNow.Date;
 186        }
 187
 1188        var actualizada = await _irpfRepository.UpdateDeduccionAsync(deduccion);
 1189        return (true, actualizada, null);
 2190    }
 191
 192    // ── Helpers ──────────────────────────────────────────────────────────────
 193
 194    private static (bool Valid, string? Error) ValidarFranja(CreateFranjaIrpfRequest r)
 195    {
 6196        if (r.NumeroFranja == 1 && r.DesdeBpc != 0)
 1197            return (false, "La franja 1 debe tener desdeBpc = 0");
 198
 5199        if (r.NumeroFranja == 7 && r.HastaBpc.HasValue)
 1200            return (false, "La última franja (7) no puede tener límite superior (hastaBpc debe ser null)");
 201
 4202        if (r.HastaBpc.HasValue && r.DesdeBpc >= r.HastaBpc.Value)
 1203            return (false, "desdeBpc debe ser menor que hastaBpc");
 204
 3205        return (true, null);
 206    }
 207
 2208    private static FranjaIrpfDto MapFranjaToDto(FranjaIrpf f) => new()
 2209    {
 2210        Id = f.Id,
 2211        NumeroFranja = f.NumeroFranja,
 2212        DesdeBpc = f.DesdeBpc,
 2213        HastaBpc = f.HastaBpc,
 2214        Tasa = f.Tasa,
 2215        VigenteDesde = f.VigenteDesde,
 2216        VigenteHasta = f.VigenteHasta
 2217    };
 218
 0219    private static DeduccionIrpfDto MapDeduccionToDto(DeduccionIrpf d) => new()
 0220    {
 0221        Id = d.Id,
 0222        PersonaId = d.PersonaId,
 0223        NombrePersona = d.Persona?.NombreCompleto ?? string.Empty,
 0224        Cedula = d.Persona?.Cedula ?? string.Empty,
 0225        Tipo = d.Tipo,
 0226        Descripcion = d.Descripcion,
 0227        VigenteDesde = d.VigenteDesde,
 0228        VigenteHasta = d.VigenteHasta,
 0229        Activo = d.Activo
 0230    };
 231}