< Summary

Information
Class: FAU.API.Controllers.Form3100Controller
Assembly: FAU.API
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/Form3100Controller.cs
Line coverage
61%
Covered lines: 118
Uncovered lines: 75
Coverable lines: 193
Total lines: 466
Line coverage: 61.1%
Branch coverage
57%
Covered branches: 32
Total branches: 56
Branch coverage: 57.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Crear()70.83%262485%
GetHistorial()100%5466.66%
GetById()100%2262.5%
GetVigente()62.5%13858.33%
GetPersonasACargo()50%11650%
AgregarPersonaACargo()50%2286.95%
EliminarPersonaACargo()100%1137.5%
SubirArchivo()0%7280%
DescargarArchivo()0%4225%
MapToDto(...)50%2262.5%

File(s)

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

#LineLine coverage
 1using FAU.Entidades;
 2using FAU.Logica.DTOs;
 3using FAU.Logica.Services;
 4using Microsoft.AspNetCore.Authorization;
 5using Microsoft.AspNetCore.Mvc;
 6using System.ComponentModel.DataAnnotations;
 7
 8namespace FAU.API.Controllers;
 9
 10/// <summary>
 11/// Gestión del Formulario 3100 (deducciones IRPF) y personas a cargo
 12/// </summary>
 13[ApiController]
 14[Route("api/form3100")]
 15[Authorize]
 16public class Form3100Controller : ControllerBase
 17{
 18    private readonly IForm3100Service _form3100Service;
 19    private readonly ICurrentUserContext _currentUser;
 20    private readonly ILogger<Form3100Controller> _logger;
 21
 1622    public Form3100Controller(
 1623        IForm3100Service form3100Service,
 1624        ICurrentUserContext currentUser,
 1625        ILogger<Form3100Controller> logger)
 26    {
 1627        _form3100Service = form3100Service;
 1628        _currentUser = currentUser;
 1629        _logger = logger;
 1630    }
 31
 32    /// <summary>
 33    /// Crear una nueva versión del Form 3100
 34    /// POST /api/form3100
 35    /// </summary>
 36    [HttpPost]
 37    [FAU.API.Security.RequirePermission(PermisoEnum.CrearForm3100)]
 38    public async Task<IActionResult> Crear([FromBody] CrearForm3100Request request)
 39    {
 40        try
 41        {
 542            if (string.IsNullOrWhiteSpace(request.CedulaTitular))
 043                return BadRequest(ApiResponse<string>.ErrorResult("cedulaTitular es requerida"));
 44
 545            if (request.VigenteDesdeMes < 1 || request.VigenteDesdeMes > 12)
 146                return BadRequest(ApiResponse<string>.ErrorResult("vigenteDesdeMes debe estar entre 1 y 12"));
 47
 448            if (request.VigenteDesdeAnio < 2000 || request.VigenteDesdeAnio > 2100)
 049                return BadRequest(ApiResponse<string>.ErrorResult("vigenteDesdeAnio inválido"));
 50
 451            if (request.CappuCategoria.HasValue && (request.CappuCategoria < 1 || request.CappuCategoria > 10))
 152                return BadRequest(ApiResponse<string>.ErrorResult("cappuCategoria debe estar entre 1 y 10"));
 53
 354            if (!string.IsNullOrEmpty(request.FondoSolidaridad) &&
 355                !new[] { "0.5_BPC", "1_BPC", "2_BPC" }.Contains(request.FondoSolidaridad))
 056                return BadRequest(ApiResponse<string>.ErrorResult("fondoSolidaridad debe ser '0.5_BPC', '1_BPC' o '2_BPC
 57
 358            var form = new Form3100
 359            {
 360                CappuCategoria = request.CappuCategoria,
 361                FondoSolidaridad = request.FondoSolidaridad,
 362                AdicionalFondoSolidaridad = request.AdicionalFondoSolidaridad,
 363                AplicaMinimoNoImponible = request.AplicaMinimoNoImponible,
 364                ConyugeNombre = request.ConyugeNombre,
 365                ConyugeApellido = request.ConyugeApellido,
 366                ConyugeFechaNacimiento = request.ConyugeFechaNacimiento,
 367                ConyugeCedula = request.ConyugeCedula,
 368                ConyugeSexo = request.ConyugeSexo,
 369                ConyugeNacionalidad = request.ConyugeNacionalidad,
 370                VigenteDesde_Mes = (short)request.VigenteDesdeMes,
 371                VigenteDesde_Anio = (short)request.VigenteDesdeAnio
 372            };
 73
 374            var usuarioId = _currentUser.UserId ?? 0;
 375            var creado = await _form3100Service.CrearAsync(request.CedulaTitular, form, usuarioId);
 76
 177            return CreatedAtAction(
 178                nameof(GetById),
 179                new { id = creado.Id },
 180                ApiResponse<object>.SuccessResult(new { id = creado.Id, creado = true }, "Form 3100 creado exitosamente"
 81        }
 182        catch (ArgumentException ex)
 83        {
 184            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 85        }
 186        catch (InvalidOperationException ex)
 87        {
 188            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 89        }
 090        catch (Exception ex)
 91        {
 092            _logger.LogError(ex, "Error al crear Form 3100");
 093            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 94        }
 595    }
 96
 97    /// <summary>
 98    /// Obtener historial de versiones del Form 3100 por cédula
 99    /// GET /api/form3100?cedulaTitular=...&vigenteDesdeAnio=...&vigenteDesdeMes=...
 100    /// </summary>
 101    [HttpGet]
 102    [FAU.API.Security.RequirePermission(PermisoEnum.VerForm3100)]
 103    public async Task<IActionResult> GetHistorial(
 104        [FromQuery] string cedulaTitular,
 105        [FromQuery] int? vigenteDesdeAnio = null,
 106        [FromQuery] int? vigenteDesdeMes = null)
 107    {
 108        try
 109        {
 2110            if (string.IsNullOrWhiteSpace(cedulaTitular))
 1111                return BadRequest(ApiResponse<string>.ErrorResult("cedulaTitular es requerida"));
 112
 1113            var historial = await _form3100Service.GetHistorialAsync(cedulaTitular, vigenteDesdeAnio, vigenteDesdeMes);
 114
 1115            var response = historial.Select(MapToDto);
 1116            return Ok(ApiResponse<object>.SuccessResult(response));
 117        }
 0118        catch (Exception ex)
 119        {
 0120            _logger.LogError(ex, "Error al obtener historial Form 3100");
 0121            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 122        }
 2123    }
 124
 125    /// <summary>
 126    /// Obtener versión específica del Form 3100 por id
 127    /// GET /api/form3100/{id}
 128    /// </summary>
 129    [HttpGet("{id}")]
 130    [FAU.API.Security.RequirePermission(PermisoEnum.VerForm3100)]
 131    public async Task<IActionResult> GetById(long id)
 132    {
 133        try
 134        {
 2135            var form = await _form3100Service.GetByIdAsync(id);
 2136            if (form == null)
 1137                return NotFound(ApiResponse<string>.ErrorResult("Form 3100 no encontrado"));
 138
 1139            return Ok(ApiResponse<object>.SuccessResult(MapToDto(form)));
 140        }
 0141        catch (Exception ex)
 142        {
 0143            _logger.LogError(ex, "Error al obtener Form 3100 {Id}", id);
 0144            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 145        }
 2146    }
 147
 148    /// <summary>
 149    /// Obtener versión vigente del Form 3100 para un período
 150    /// GET /api/form3100/vigente?cedulaTitular=...&anio=...&mes=...
 151    /// </summary>
 152    [HttpGet("vigente")]
 153    [FAU.API.Security.RequirePermission(PermisoEnum.VerForm3100)]
 154    public async Task<IActionResult> GetVigente(
 155        [FromQuery] string cedulaTitular,
 156        [FromQuery] int anio,
 157        [FromQuery] int mes)
 158    {
 159        try
 160        {
 2161            if (string.IsNullOrWhiteSpace(cedulaTitular))
 0162                return BadRequest(ApiResponse<string>.ErrorResult("cedulaTitular es requerida"));
 2163            if (mes < 1 || mes > 12)
 0164                return BadRequest(ApiResponse<string>.ErrorResult("mes debe estar entre 1 y 12"));
 165
 2166            var form = await _form3100Service.GetVigenteParaPeriodoAsync(cedulaTitular, anio, mes);
 2167            if (form == null)
 1168                return NotFound(ApiResponse<string>.ErrorResult("No se encontró Form 3100 vigente para el período indica
 169
 1170            return Ok(ApiResponse<object>.SuccessResult(MapToDto(form)));
 171        }
 0172        catch (Exception ex)
 173        {
 0174            _logger.LogError(ex, "Error al obtener Form 3100 vigente");
 0175            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 176        }
 2177    }
 178
 179    /// <summary>
 180    /// Listar personas a cargo de una versión del Form 3100
 181    /// GET /api/form3100/{id}/personas-cargo
 182    /// </summary>
 183    [HttpGet("{id}/personas-cargo")]
 184    [FAU.API.Security.RequirePermission(PermisoEnum.VerForm3100)]
 185    public async Task<IActionResult> GetPersonasACargo(long id)
 186    {
 187        try
 188        {
 1189            var form = await _form3100Service.GetByIdAsync(id);
 1190            if (form == null)
 0191                return NotFound(ApiResponse<string>.ErrorResult("Form 3100 no encontrado"));
 192
 1193            var personas = form.PersonasACargo?.Select(p => new
 1194            {
 1195                id = p.Id, nombre = p.Nombre, apellido = p.Apellido,
 1196                fechaNacimiento = p.FechaNacimiento, cedula = p.Cedula,
 1197                sexo = p.Sexo, porcentajeAtribucion = p.PorcentajeAtribucion,
 1198                relacion = p.Relacion, discapacidad = p.Discapacidad
 1199            });
 1200            return Ok(ApiResponse<object>.SuccessResult(personas ?? Enumerable.Empty<object>()));
 201        }
 0202        catch (Exception ex)
 203        {
 0204            _logger.LogError(ex, "Error al listar personas a cargo de Form 3100 {Id}", id);
 0205            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 206        }
 1207    }
 208
 209    /// <summary>
 210    /// Agregar persona a cargo a una versión del Form 3100
 211    /// POST /api/form3100/{id}/personas-cargo
 212    /// </summary>
 213    [HttpPost("{id}/personas-cargo")]
 214    [FAU.API.Security.RequirePermission(PermisoEnum.CrearForm3100)]
 215    public async Task<IActionResult> AgregarPersonaACargo(long id, [FromBody] AgregarPersonaACargoRequest request)
 216    {
 217        try
 218        {
 2219            var persona = new Form3100PersonaACargo
 2220            {
 2221                Nombre = request.Nombre,
 2222                Apellido = request.Apellido,
 2223                FechaNacimiento = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 2224                    ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 2225                    : request.FechaNacimiento.Date.ToUniversalTime(),
 2226                Cedula = request.Cedula,
 2227                Sexo = request.Sexo,
 2228                PorcentajeAtribucion = (short)request.PorcentajeAtribucion,
 2229                Relacion = request.Relacion,
 2230                Discapacidad = request.Discapacidad
 2231            };
 232
 2233            var creada = await _form3100Service.AgregarPersonaACargoAsync(id, persona);
 234
 1235            return StatusCode(201, ApiResponse<object>.SuccessResult(
 1236                new { id = creada.Id, creado = true },
 1237                "Persona a cargo agregada exitosamente"));
 238        }
 1239        catch (ArgumentException ex)
 240        {
 1241            return BadRequest(ApiResponse<string>.ErrorResult(ex.Message));
 242        }
 0243        catch (Exception ex)
 244        {
 0245            _logger.LogError(ex, "Error al agregar persona a cargo a Form 3100 {Id}", id);
 0246            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 247        }
 2248    }
 249
 250    /// <summary>
 251    /// Eliminar persona a cargo de una versión del Form 3100
 252    /// DELETE /api/form3100/{id}/personas-cargo/{personaCargoId}
 253    /// </summary>
 254    [HttpDelete("{id}/personas-cargo/{personaCargoId}")]
 255    [FAU.API.Security.RequirePermission(PermisoEnum.CrearForm3100)]
 256    public async Task<IActionResult> EliminarPersonaACargo(long id, long personaCargoId)
 257    {
 258        try
 259        {
 1260            await _form3100Service.EliminarPersonaACargoAsync(id, personaCargoId);
 1261            return NoContent();
 262        }
 0263        catch (ArgumentException ex)
 264        {
 0265            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 266        }
 0267        catch (Exception ex)
 268        {
 0269            _logger.LogError(ex, "Error al eliminar persona a cargo {PersonaCargoId} de Form 3100 {Id}", personaCargoId,
 0270            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 271        }
 1272    }
 273
 274    /// <summary>
 275    /// Subir archivo escaneado del Form 3100
 276    /// POST /api/form3100/{id}/archivo (multipart/form-data, campo "file")
 277    /// </summary>
 278    [HttpPost("{id}/archivo")]
 279    [FAU.API.Security.RequirePermission(PermisoEnum.CrearForm3100)]
 280    [RequestSizeLimit(20_000_000)] // 20 MB
 281    public async Task<IActionResult> SubirArchivo(long id, IFormFile file)
 282    {
 283        try
 284        {
 0285            if (file == null || file.Length == 0)
 0286                return BadRequest(ApiResponse<string>.ErrorResult("Archivo requerido"));
 287
 0288            var usuarioId = _currentUser.UserId ?? 0;
 0289            await using var stream = file.OpenReadStream();
 0290            var rutaRelativa = await _form3100Service.SubirArchivoAsync(
 0291                id,
 0292                stream,
 0293                file.FileName,
 0294                file.ContentType,
 0295                usuarioId);
 296
 0297            return Ok(ApiResponse<object>.SuccessResult(
 0298                new { rutaRelativa, subido = true },
 0299                "Archivo subido exitosamente"));
 0300        }
 0301        catch (ArgumentException ex)
 302        {
 0303            if (ex.Message.Contains("no encontrado", StringComparison.OrdinalIgnoreCase))
 0304                return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 0305            return BadRequest(ApiResponse<string>.ErrorResult(ex.Message));
 306        }
 0307        catch (Exception ex)
 308        {
 0309            _logger.LogError(ex, "Error al subir archivo de Form 3100 {Id}", id);
 0310            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 311        }
 0312    }
 313
 314    /// <summary>
 315    /// Descargar archivo escaneado del Form 3100
 316    /// GET /api/form3100/{id}/archivo
 317    /// </summary>
 318    /// <summary>
 319    /// GET /api/form3100/{id}/archivo?inline=true
 320    /// inline=true  → Content-Disposition: inline  (para mostrar en visor del navegador)
 321    /// inline=false → Content-Disposition: attachment (para forzar descarga, default)
 322    /// </summary>
 323    [HttpGet("{id}/archivo")]
 324    [FAU.API.Security.RequirePermission(PermisoEnum.VerForm3100)]
 325    public async Task<IActionResult> DescargarArchivo(long id, [FromQuery] bool inline = true)
 326    {
 327        try
 328        {
 1329            var (contenido, nombreOriginal, contentType) = await _form3100Service.ObtenerArchivoAsync(id);
 330
 0331            if (inline)
 332            {
 0333                Response.Headers.ContentDisposition =
 0334                    $"inline; filename=\"{nombreOriginal}\"; filename*=UTF-8''{Uri.EscapeDataString(nombreOriginal)}";
 0335                return File(contenido, contentType);
 336            }
 337
 0338            return File(contenido, contentType, nombreOriginal);
 339        }
 0340        catch (ArgumentException ex)
 341        {
 0342            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 343        }
 1344        catch (KeyNotFoundException ex)
 345        {
 1346            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 347        }
 0348        catch (StorageFileNotFoundException ex)
 349        {
 0350            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 351        }
 0352        catch (Exception ex)
 353        {
 0354            _logger.LogError(ex, "Error al descargar archivo de Form 3100 {Id}", id);
 0355            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 356        }
 1357    }
 358
 359    private static object MapToDto(Form3100 f)
 360    {
 2361        return new
 2362        {
 2363            id = f.Id,
 2364            personaId = f.PersonaId,
 2365            cappuCategoria = f.CappuCategoria,
 2366            fondoSolidaridad = f.FondoSolidaridad,
 2367            adicionalFondoSolidaridad = f.AdicionalFondoSolidaridad,
 2368            aplicaMinimoNoImponible = f.AplicaMinimoNoImponible,
 2369            conyugeNombre = f.ConyugeNombre,
 2370            conyugeApellido = f.ConyugeApellido,
 2371            conyugeFechaNacimiento = f.ConyugeFechaNacimiento,
 2372            conyugeCedula = f.ConyugeCedula,
 2373            conyugeSexo = f.ConyugeSexo,
 2374            conyugeNacionalidad = f.ConyugeNacionalidad,
 2375            vigenteDesdeMes = f.VigenteDesde_Mes,
 2376            vigenteDesdeAnio = f.VigenteDesde_Anio,
 2377            documentoId = f.DocumentoId,
 2378            fechaCreacion = f.FechaCreacion,
 2379            usuarioCreacionId = f.UsuarioCreacionId,
 0380            personasACargo = f.PersonasACargo?.Select(p => new
 0381            {
 0382                id = p.Id,
 0383                nombre = p.Nombre,
 0384                apellido = p.Apellido,
 0385                fechaNacimiento = p.FechaNacimiento,
 0386                cedula = p.Cedula,
 0387                sexo = p.Sexo,
 0388                porcentajeAtribucion = p.PorcentajeAtribucion,
 0389                relacion = p.Relacion,
 0390                discapacidad = p.Discapacidad
 0391            })
 2392        };
 393    }
 394}
 395
 396public class CrearForm3100Request
 397{
 398    [Required(ErrorMessage = "cedulaTitular es requerida")]
 399    [MaxLength(12)]
 400    public string CedulaTitular { get; set; } = string.Empty;
 401
 402    public short? CappuCategoria { get; set; }
 403
 404    [MaxLength(10)]
 405    public string? FondoSolidaridad { get; set; }
 406
 407    public bool AdicionalFondoSolidaridad { get; set; }
 408
 409    public bool AplicaMinimoNoImponible { get; set; }
 410
 411    [MaxLength(100)]
 412    public string? ConyugeNombre { get; set; }
 413
 414    [MaxLength(100)]
 415    public string? ConyugeApellido { get; set; }
 416
 417    public DateTime? ConyugeFechaNacimiento { get; set; }
 418
 419    [MaxLength(20)]
 420    public string? ConyugeCedula { get; set; }
 421
 422    [MaxLength(1)]
 423    public string? ConyugeSexo { get; set; }
 424
 425    [MaxLength(100)]
 426    public string? ConyugeNacionalidad { get; set; }
 427
 428    [Required]
 429    [Range(1, 12)]
 430    public int VigenteDesdeMes { get; set; }
 431
 432    [Required]
 433    [Range(2000, 2100)]
 434    public int VigenteDesdeAnio { get; set; }
 435}
 436
 437public class AgregarPersonaACargoRequest
 438{
 439    [Required]
 440    [MaxLength(100)]
 441    public string Nombre { get; set; } = string.Empty;
 442
 443    [Required]
 444    [MaxLength(100)]
 445    public string Apellido { get; set; } = string.Empty;
 446
 447    [Required]
 448    public DateTime FechaNacimiento { get; set; }
 449
 450    [MaxLength(20)]
 451    public string? Cedula { get; set; }
 452
 453    [Required]
 454    [MaxLength(1)]
 455    public string Sexo { get; set; } = string.Empty;
 456
 457    [Required]
 458    [Range(0, 100)]
 459    public int PorcentajeAtribucion { get; set; }
 460
 461    [Required]
 462    [MaxLength(100)]
 463    public string Relacion { get; set; } = string.Empty;
 464
 465    public bool Discapacidad { get; set; }
 466}