< Summary

Information
Class: FAU.API.Controllers.CatalogoItemsController
Assembly: FAU.API
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/CatalogoItemsController.cs
Line coverage
18%
Covered lines: 39
Uncovered lines: 170
Coverable lines: 209
Total lines: 357
Line coverage: 18.6%
Branch coverage
9%
Covered branches: 9
Total branches: 92
Branch coverage: 9.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
GetCatalogoItems()50%511853.33%
GetCatalogoItem()0%620%
CrearCatalogoItem()0%110100%
ActualizarCatalogoItem()0%1806420%
GetUsos()0%620%
CambiarVigente()0%4260%
ParseCodigo(...)0%2040%
EsDuplicadoCodigo(...)0%7280%
MapDto(...)100%11100%

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/CatalogoItemsController.cs

#LineLine coverage
 1using FAU.DataAccess;
 2using FAU.Entidades;
 3using FAU.Logica.DTOs;
 4using Microsoft.AspNetCore.Authorization;
 5using Microsoft.AspNetCore.Mvc;
 6using Microsoft.EntityFrameworkCore;
 7
 8namespace FAU.API.Controllers;
 9
 10[ApiController]
 11[Route("api/catalogo-items")]
 12[Authorize]
 13public class CatalogoItemsController : ControllerBase
 14{
 15    private readonly ApplicationDbContext _context;
 16    private readonly ILogger<CatalogoItemsController> _logger;
 17
 018    private static readonly HashSet<string> TiposValidos =
 019    [
 020        "haber_normal", "haber_anual", "haber_no_gravado",
 021        "beneficio_social_grav", "beneficio_social_no_grav",
 022        "ficto_haber"
 023    ];
 24
 025    private static readonly HashSet<string> FormulasValidas =
 026    [
 027        "por_grado", "tabla_externa", "prorrateo", "fijo"
 028    ];
 29
 330    public CatalogoItemsController(ApplicationDbContext context, ILogger<CatalogoItemsController> logger)
 31    {
 332        _context = context;
 333        _logger = logger;
 334    }
 35
 36    [HttpGet]
 37    public async Task<IActionResult> GetCatalogoItems(
 38        [FromQuery] bool? vigente = null,
 39        [FromQuery] string? busqueda = null,
 40        [FromQuery] string? tipo = null,
 41        [FromQuery] string? formulaCalculo = null,
 42        [FromQuery] string? excluirTipos = null,
 43        [FromQuery] int pagina = 1,
 44        [FromQuery] int tamano = 50)
 45    {
 46        try
 47        {
 348            if (pagina < 1) pagina = 1;
 349            if (tamano < 1 || tamano > 200) tamano = 50;
 50
 351            var query = _context.CatalogoItems.AsNoTracking();
 52
 353            if (vigente.HasValue)
 154                query = query.Where(i => i.Vigente == vigente.Value);
 55
 356            if (!string.IsNullOrWhiteSpace(tipo))
 57            {
 58                // Soporta valor único ("haber_normal") o múltiples separados por coma ("haber_normal,haber_no_gravado")
 059                var tipos = tipo.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 060                query = tipos.Length == 1
 061                    ? query.Where(i => i.Tipo == tipos[0])
 062                    : query.Where(i => tipos.Contains(i.Tipo));
 63            }
 64
 365            if (!string.IsNullOrWhiteSpace(excluirTipos))
 66            {
 067                var excluidos = excluirTipos.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimE
 068                query = query.Where(i => !excluidos.Contains(i.Tipo));
 69            }
 70
 371            if (!string.IsNullOrWhiteSpace(formulaCalculo))
 072                query = query.Where(i => i.FormulaCalculo == formulaCalculo);
 73
 374            if (!string.IsNullOrWhiteSpace(busqueda))
 75            {
 076                var term = busqueda.Trim().ToLower();
 077                query = query.Where(i =>
 078                    i.Nombre.ToLower().Contains(term) ||
 079                    (i.ClasificacionSiif != null && i.ClasificacionSiif.Contains(term)));
 80            }
 81
 382            var items = await query
 383                .OrderBy(i => i.OrdenCalculo)
 384                .ThenBy(i => i.Nombre)
 385                .Select(i => MapDto(i))
 386                .ToListAsync();
 87
 388            return Ok(ApiResponse<IEnumerable<CatalogoItemDto>>.SuccessResult(items));
 89        }
 090        catch (Exception ex)
 91        {
 092            _logger.LogError(ex, "Error al obtener catálogo de ítems");
 093            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 94        }
 395    }
 96
 97    [HttpGet("{id:long}")]
 98    public async Task<IActionResult> GetCatalogoItem(long id)
 99    {
 100        try
 101        {
 0102            var item = await _context.CatalogoItems.AsNoTracking()
 0103                .FirstOrDefaultAsync(i => i.Id == id);
 104
 0105            if (item is null)
 0106                return NotFound(ApiResponse<string>.ErrorResult("Ítem no encontrado"));
 107
 0108            return Ok(ApiResponse<CatalogoItemDto>.SuccessResult(MapDto(item)));
 109        }
 0110        catch (Exception ex)
 111        {
 0112            _logger.LogError(ex, "Error al obtener catálogo ítem {Id}", id);
 0113            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 114        }
 0115    }
 116
 117    [HttpPost]
 118    [Authorize(Roles = "Administrador")]
 119    public async Task<IActionResult> CrearCatalogoItem([FromBody] CrearCatalogoItemRequest request)
 120    {
 121        try
 122        {
 0123            if (!TiposValidos.Contains(request.Tipo))
 0124                return BadRequest(ApiResponse<string>.ErrorResult(
 0125                    $"Tipo inválido '{request.Tipo}'. Valores permitidos: {string.Join(", ", TiposValidos)}"));
 126
 0127            if (!FormulasValidas.Contains(request.FormulaCalculo))
 0128                return BadRequest(ApiResponse<string>.ErrorResult(
 0129                    $"Fórmula inválida '{request.FormulaCalculo}'. Valores permitidos: {string.Join(", ", FormulasValida
 130
 0131            var codigo = ParseCodigo(request.ClasificacionSiif);
 0132            if (codigo is null)
 0133                return BadRequest(ApiResponse<string>.ErrorResult(
 0134                    "Formato de clasificación SIIF inválido. Debe ser NNN.NNN (ej: 042.568)"));
 135
 0136            if (await _context.CatalogoItems.AnyAsync(i =>
 0137                    i.Codigo == codigo.Value ||
 0138                    i.ClasificacionSiif == request.ClasificacionSiif))
 0139                return Conflict(ApiResponse<string>.ErrorResult(
 0140                    $"Ya existe un ítem con la clasificación SIIF '{request.ClasificacionSiif}'"));
 141
 0142            var item = new CatalogoItem
 0143            {
 0144                Codigo = codigo.Value,
 0145                Nombre = request.Nombre.Trim(),
 0146                Tipo = request.Tipo,
 0147                ClasificacionSiif = request.ClasificacionSiif,
 0148                NormaLegal = request.NormaLegal?.Trim(),
 0149                EsGravado = request.EsGravado,
 0150                AcumulaAguinaldo = request.AcumulaAguinaldo,
 0151                LlevaAumento = request.LlevaAumento,
 0152                FormulaCalculo = request.FormulaCalculo,
 0153                OrdenCalculo = request.OrdenCalculo,
 0154                ContribuyeWPara53 = request.ContribuyeWPara53,
 0155                ContribuyeWPara10 = request.ContribuyeWPara10,
 0156                ContribuyeWPara20 = request.ContribuyeWPara20,
 0157                ContribuyeWNominal = request.ContribuyeWNominal,
 0158                Vigente = request.Vigente
 0159            };
 160
 0161            _context.CatalogoItems.Add(item);
 0162            await _context.SaveChangesAsync();
 163
 0164            _logger.LogInformation("CatalogoItem creado: {Siif} - {Nombre}", item.ClasificacionSiif, item.Nombre);
 165
 0166            return CreatedAtAction(nameof(GetCatalogoItem), new { id = item.Id },
 0167                ApiResponse<CatalogoItemDto>.SuccessResult(MapDto(item)));
 168        }
 0169        catch (DbUpdateException ex) when (EsDuplicadoCodigo(ex))
 170        {
 0171            return Conflict(ApiResponse<string>.ErrorResult(
 0172                $"Ya existe un ítem con la clasificación SIIF '{request.ClasificacionSiif}'"));
 173        }
 0174        catch (Exception ex)
 175        {
 0176            _logger.LogError(ex, "Error al crear catálogo ítem");
 0177            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 178        }
 0179    }
 180
 181    [HttpPut("{id:long}")]
 182    [Authorize(Roles = "Administrador")]
 183    public async Task<IActionResult> ActualizarCatalogoItem(long id, [FromBody] ActualizarCatalogoItemRequest request)
 184    {
 185        try
 186        {
 0187            var item = await _context.CatalogoItems.FindAsync(id);
 0188            if (item is null)
 0189                return NotFound(ApiResponse<string>.ErrorResult("Ítem no encontrado"));
 190
 0191            if (request.Tipo is not null && !TiposValidos.Contains(request.Tipo))
 0192                return BadRequest(ApiResponse<string>.ErrorResult(
 0193                    $"Tipo inválido '{request.Tipo}'. Valores permitidos: {string.Join(", ", TiposValidos)}"));
 194
 0195            if (request.FormulaCalculo is not null && !FormulasValidas.Contains(request.FormulaCalculo))
 0196                return BadRequest(ApiResponse<string>.ErrorResult(
 0197                    $"Fórmula inválida '{request.FormulaCalculo}'. Valores permitidos: {string.Join(", ", FormulasValida
 198
 0199            if (request.Nombre is not null) item.Nombre = request.Nombre.Trim();
 0200            if (request.Tipo is not null) item.Tipo = request.Tipo;
 0201            if (request.NormaLegal is not null) item.NormaLegal = request.NormaLegal.Trim();
 0202            if (request.EsGravado.HasValue) item.EsGravado = request.EsGravado.Value;
 0203            if (request.AcumulaAguinaldo.HasValue) item.AcumulaAguinaldo = request.AcumulaAguinaldo.Value;
 0204            if (request.LlevaAumento.HasValue) item.LlevaAumento = request.LlevaAumento.Value;
 0205            if (request.FormulaCalculo is not null) item.FormulaCalculo = request.FormulaCalculo;
 0206            if (request.OrdenCalculo.HasValue) item.OrdenCalculo = request.OrdenCalculo.Value;
 0207            if (request.ContribuyeWPara53.HasValue) item.ContribuyeWPara53 = request.ContribuyeWPara53.Value;
 0208            if (request.ContribuyeWPara10.HasValue) item.ContribuyeWPara10 = request.ContribuyeWPara10.Value;
 0209            if (request.ContribuyeWPara20.HasValue) item.ContribuyeWPara20 = request.ContribuyeWPara20.Value;
 0210            if (request.ContribuyeWNominal.HasValue) item.ContribuyeWNominal = request.ContribuyeWNominal.Value;
 211
 0212            if (request.ClasificacionSiif is not null &&
 0213                request.ClasificacionSiif != item.ClasificacionSiif)
 214            {
 0215                var nuevoCodigo = ParseCodigo(request.ClasificacionSiif);
 0216                if (nuevoCodigo is null)
 0217                    return BadRequest(ApiResponse<string>.ErrorResult(
 0218                        "Formato de clasificación SIIF inválido. Debe ser NNN.NNN (ej: 042.568)"));
 219
 0220                if (await _context.CatalogoItems.AnyAsync(i =>
 0221                        i.Id != id &&
 0222                        (i.Codigo == nuevoCodigo.Value || i.ClasificacionSiif == request.ClasificacionSiif)))
 0223                    return Conflict(ApiResponse<string>.ErrorResult(
 0224                        $"Ya existe un ítem con la clasificación SIIF '{request.ClasificacionSiif}'"));
 225
 0226                item.ClasificacionSiif = request.ClasificacionSiif;
 0227                item.Codigo = nuevoCodigo.Value;
 228            }
 229
 0230            await _context.SaveChangesAsync();
 231
 0232            return Ok(ApiResponse<CatalogoItemDto>.SuccessResult(MapDto(item)));
 233        }
 0234        catch (DbUpdateException ex) when (EsDuplicadoCodigo(ex))
 235        {
 0236            return Conflict(ApiResponse<string>.ErrorResult(
 0237                $"Ya existe un ítem con la clasificación SIIF '{request.ClasificacionSiif}'"));
 238        }
 0239        catch (Exception ex)
 240        {
 0241            _logger.LogError(ex, "Error al actualizar catálogo ítem {Id}", id);
 0242            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 243        }
 0244    }
 245
 246    [HttpGet("{id:long}/usos")]
 247    [Authorize(Roles = "Administrador")]
 248    public async Task<IActionResult> GetUsos(long id)
 249    {
 250        try
 251        {
 0252            var item = await _context.CatalogoItems.AsNoTracking()
 0253                .FirstOrDefaultAsync(i => i.Id == id);
 0254            if (item is null)
 0255                return NotFound(ApiResponse<string>.ErrorResult("Ítem no encontrado"));
 256
 0257            var codigoStr = item.Codigo.ToString();
 258
 0259            var liquidacionesHistoricas = await _context.LiquidacionItems
 0260                .CountAsync(li => li.CodigoConcepto == codigoStr);
 261
 0262            var montosPorGrado = await _context.RemuneracionesGrado
 0263                .CountAsync(r => r.ItemId == id);
 264
 0265            var compensacionesActivas = await _context.Set<FAU.Entidades.ConfigCompensacion>()
 0266                .CountAsync(c => c.CatalogoItemId == id && c.Activo);
 267
 0268            var beneficiosActivos = await _context.Set<FAU.Entidades.TipoBeneficio>()
 0269                .CountAsync(tb => tb.CatalogoItemId == id && tb.Activo);
 270
 0271            return Ok(ApiResponse<object>.SuccessResult(new
 0272            {
 0273                liquidacionesHistoricas,
 0274                montosPorGrado,
 0275                compensacionesActivas,
 0276                beneficiosActivos,
 0277                puedeEditarSiif = liquidacionesHistoricas == 0,
 0278                puedeEditarFormula = montosPorGrado == 0
 0279            }));
 280        }
 0281        catch (Exception ex)
 282        {
 0283            _logger.LogError(ex, "Error al obtener usos de catálogo ítem {Id}", id);
 0284            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 285        }
 0286    }
 287
 288    [HttpPatch("{id:long}/vigente")]
 289    [Authorize(Roles = "Administrador")]
 290    public async Task<IActionResult> CambiarVigente(long id, [FromBody] CambiarVigenteRequest request)
 291    {
 292        try
 293        {
 0294            var item = await _context.CatalogoItems.FindAsync(id);
 0295            if (item is null)
 0296                return NotFound(ApiResponse<string>.ErrorResult("Ítem no encontrado"));
 297
 0298            if (!request.Vigente)
 299            {
 0300                var enUso = await _context.LiquidacionItems
 0301                    .AnyAsync(li => li.CodigoConcepto == item.Codigo.ToString());
 0302                if (enUso)
 0303                    return Conflict(ApiResponse<string>.ErrorResult(
 0304                        $"No se puede desactivar: el ítem '{item.ClasificacionSiif}' está en uso en liquidaciones"));
 305            }
 306
 0307            item.Vigente = request.Vigente;
 0308            await _context.SaveChangesAsync();
 309
 0310            return Ok(ApiResponse<CatalogoItemDto>.SuccessResult(MapDto(item)));
 311        }
 0312        catch (Exception ex)
 313        {
 0314            _logger.LogError(ex, "Error al cambiar vigente de catálogo ítem {Id}", id);
 0315            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 316        }
 0317    }
 318
 319    // ── Helpers ───────────────────────────────────────────────────────────────
 320
 321    private static int? ParseCodigo(string clasificacionSiif)
 322    {
 0323        if (string.IsNullOrWhiteSpace(clasificacionSiif)) return null;
 0324        var sinPunto = clasificacionSiif.Replace(".", "");
 0325        return int.TryParse(sinPunto, out var codigo) ? codigo : null;
 326    }
 327
 328    private static bool EsDuplicadoCodigo(DbUpdateException ex)
 329    {
 0330        var msg = ex.InnerException?.Message ?? ex.Message;
 0331        return msg.Contains("catalogo_items_codigo_key", StringComparison.OrdinalIgnoreCase)
 0332            || msg.Contains("ux_catalogo_items_clasificacion_siif", StringComparison.OrdinalIgnoreCase)
 0333            || msg.Contains("duplicate key", StringComparison.OrdinalIgnoreCase);
 334    }
 335
 4336    private static CatalogoItemDto MapDto(CatalogoItem i) => new()
 4337    {
 4338        Id = i.Id,
 4339        Codigo = i.Codigo,
 4340        Nombre = i.Nombre,
 4341        Tipo = i.Tipo,
 4342        ClasificacionSiif = i.ClasificacionSiif,
 4343        NormaLegal = i.NormaLegal,
 4344        EsGravado = i.EsGravado,
 4345        AcumulaAguinaldo = i.AcumulaAguinaldo,
 4346        LlevaAumento = i.LlevaAumento,
 4347        FormulaCalculo = i.FormulaCalculo,
 4348        OrdenCalculo = i.OrdenCalculo,
 4349        ContribuyeWPara53 = i.ContribuyeWPara53,
 4350        ContribuyeWPara10 = i.ContribuyeWPara10,
 4351        ContribuyeWPara20 = i.ContribuyeWPara20,
 4352        ContribuyeWNominal = i.ContribuyeWNominal,
 4353        Vigente = i.Vigente
 4354    };
 355}
 356
 357public record CambiarVigenteRequest(bool Vigente);