< Summary

Information
Class: FAU.API.Controllers.CrearForm3100Request
Assembly: FAU.API
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/Form3100Controller.cs
Line coverage
100%
Covered lines: 13
Uncovered lines: 0
Coverable lines: 13
Total lines: 519
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_CedulaTitular()100%11100%
get_CappuCategoria()100%11100%
get_FondoSolidaridad()100%11100%
get_AdicionalFondoSolidaridad()100%11100%
get_AplicaMinimoNoImponible()100%11100%
get_ConyugeNombre()100%11100%
get_ConyugeApellido()100%11100%
get_ConyugeFechaNacimiento()100%11100%
get_ConyugeCedula()100%11100%
get_ConyugeSexo()100%11100%
get_ConyugeNacionalidad()100%11100%
get_VigenteDesdeMes()100%11100%
get_VigenteDesdeAnio()100%11100%

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.Exceptions;
 4using FAU.Logica.Services;
 5using Microsoft.AspNetCore.Authorization;
 6using Microsoft.AspNetCore.Http;
 7using Microsoft.AspNetCore.Mvc;
 8using System.ComponentModel.DataAnnotations;
 9
 10namespace FAU.API.Controllers;
 11
 12/// <summary>
 13/// Gestión del Formulario 3100 (deducciones IRPF) y personas a cargo
 14/// </summary>
 15[ApiController]
 16[Route("api/form3100")]
 17[Authorize]
 18[Produces("application/json")]
 19public class Form3100Controller : ControllerBase
 20{
 21    private readonly IForm3100Service _form3100Service;
 22    private readonly ICurrentUserContext _currentUser;
 23    private readonly ILogger<Form3100Controller> _logger;
 24
 25    public Form3100Controller(
 26        IForm3100Service form3100Service,
 27        ICurrentUserContext currentUser,
 28        ILogger<Form3100Controller> logger)
 29    {
 30        _form3100Service = form3100Service;
 31        _currentUser = currentUser;
 32        _logger = logger;
 33    }
 34
 35    /// <summary>
 36    /// Crear una nueva versión del Form 3100
 37    /// POST /api/form3100
 38    /// </summary>
 39    [HttpPost]
 40    [Authorize(Roles = "Administrador,Usuario")]
 41    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status201Created)]
 42    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 43    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 44    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 45    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 46    public async Task<IActionResult> Crear([FromBody] CrearForm3100Request request)
 47    {
 48        try
 49        {
 50            if (string.IsNullOrWhiteSpace(request.CedulaTitular))
 51                return BadRequest(ApiResponse<string>.ErrorResult("cedulaTitular es requerida"));
 52
 53            if (request.VigenteDesdeMes < 1 || request.VigenteDesdeMes > 12)
 54                return BadRequest(ApiResponse<string>.ErrorResult("vigenteDesdeMes debe estar entre 1 y 12"));
 55
 56            if (request.VigenteDesdeAnio < 2000 || request.VigenteDesdeAnio > 2100)
 57                return BadRequest(ApiResponse<string>.ErrorResult("vigenteDesdeAnio inválido"));
 58
 59            if (request.CappuCategoria.HasValue && (request.CappuCategoria < 1 || request.CappuCategoria > 10))
 60                return BadRequest(ApiResponse<string>.ErrorResult("cappuCategoria debe estar entre 1 y 10"));
 61
 62            if (!string.IsNullOrEmpty(request.FondoSolidaridad) &&
 63                !new[] { "0.5_BPC", "1_BPC", "2_BPC" }.Contains(request.FondoSolidaridad))
 64                return BadRequest(ApiResponse<string>.ErrorResult("fondoSolidaridad debe ser '0.5_BPC', '1_BPC' o '2_BPC
 65
 66            var form = new Form3100
 67            {
 68                CappuCategoria = request.CappuCategoria,
 69                FondoSolidaridad = request.FondoSolidaridad,
 70                AdicionalFondoSolidaridad = request.AdicionalFondoSolidaridad,
 71                AplicaMinimoNoImponible = request.AplicaMinimoNoImponible,
 72                ConyugeNombre = request.ConyugeNombre,
 73                ConyugeApellido = request.ConyugeApellido,
 74                ConyugeFechaNacimiento = request.ConyugeFechaNacimiento,
 75                ConyugeCedula = request.ConyugeCedula,
 76                ConyugeSexo = request.ConyugeSexo,
 77                ConyugeNacionalidad = request.ConyugeNacionalidad,
 78                VigenteDesde_Mes = (short)request.VigenteDesdeMes,
 79                VigenteDesde_Anio = (short)request.VigenteDesdeAnio
 80            };
 81
 82            var usuarioId = _currentUser.UserId ?? 0;
 83            var creado = await _form3100Service.CrearAsync(request.CedulaTitular, form, usuarioId);
 84
 85            return CreatedAtAction(
 86                nameof(GetById),
 87                new { id = creado.Id },
 88                ApiResponse<object>.SuccessResult(new { id = creado.Id, creado = true }, "Form 3100 creado exitosamente"
 89        }
 90        catch (ArgumentException ex)
 91        {
 92            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 93        }
 94        catch (PeriodoBloqueadoException ex)
 95        {
 96            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 97        }
 98        catch (InvalidOperationException ex)
 99        {
 100            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 101        }
 102        catch (Exception ex)
 103        {
 104            _logger.LogError(ex, "Error al crear Form 3100");
 105            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 106        }
 107    }
 108
 109    /// <summary>
 110    /// Obtener historial de versiones del Form 3100 por cédula
 111    /// GET /api/form3100?cedulaTitular=...&vigenteDesdeAnio=...&vigenteDesdeMes=...
 112    /// </summary>
 113    [HttpGet]
 114    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 115    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 116    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 117    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 118    public async Task<IActionResult> GetHistorial(
 119        [FromQuery] string cedulaTitular,
 120        [FromQuery] int? vigenteDesdeAnio = null,
 121        [FromQuery] int? vigenteDesdeMes = null)
 122    {
 123        try
 124        {
 125            if (string.IsNullOrWhiteSpace(cedulaTitular))
 126                return BadRequest(ApiResponse<string>.ErrorResult("cedulaTitular es requerida"));
 127
 128            var historial = await _form3100Service.GetHistorialAsync(cedulaTitular, vigenteDesdeAnio, vigenteDesdeMes);
 129
 130            var response = historial.Select(MapToDto);
 131            return Ok(ApiResponse<object>.SuccessResult(response));
 132        }
 133        catch (Exception ex)
 134        {
 135            _logger.LogError(ex, "Error al obtener historial Form 3100");
 136            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 137        }
 138    }
 139
 140    /// <summary>
 141    /// Obtener versión específica del Form 3100 por id
 142    /// GET /api/form3100/{id}
 143    /// </summary>
 144    [HttpGet("{id}")]
 145    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 146    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 147    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 148    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 149    public async Task<IActionResult> GetById(long id)
 150    {
 151        try
 152        {
 153            var form = await _form3100Service.GetByIdAsync(id);
 154            if (form == null)
 155                return NotFound(ApiResponse<string>.ErrorResult("Form 3100 no encontrado"));
 156
 157            return Ok(ApiResponse<object>.SuccessResult(MapToDto(form)));
 158        }
 159        catch (Exception ex)
 160        {
 161            _logger.LogError(ex, "Error al obtener Form 3100 {Id}", id);
 162            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 163        }
 164    }
 165
 166    /// <summary>
 167    /// Obtener versión vigente del Form 3100 para un período
 168    /// GET /api/form3100/vigente?cedulaTitular=...&anio=...&mes=...
 169    /// </summary>
 170    [HttpGet("vigente")]
 171    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 172    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 173    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 174    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 175    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 176    public async Task<IActionResult> GetVigente(
 177        [FromQuery] string cedulaTitular,
 178        [FromQuery] int anio,
 179        [FromQuery] int mes)
 180    {
 181        try
 182        {
 183            if (string.IsNullOrWhiteSpace(cedulaTitular))
 184                return BadRequest(ApiResponse<string>.ErrorResult("cedulaTitular es requerida"));
 185            if (mes < 1 || mes > 12)
 186                return BadRequest(ApiResponse<string>.ErrorResult("mes debe estar entre 1 y 12"));
 187
 188            var form = await _form3100Service.GetVigenteParaPeriodoAsync(cedulaTitular, anio, mes);
 189            if (form == null)
 190                return NotFound(ApiResponse<string>.ErrorResult("No se encontró Form 3100 vigente para el período indica
 191
 192            return Ok(ApiResponse<object>.SuccessResult(MapToDto(form)));
 193        }
 194        catch (Exception ex)
 195        {
 196            _logger.LogError(ex, "Error al obtener Form 3100 vigente");
 197            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 198        }
 199    }
 200
 201    /// <summary>
 202    /// Listar personas a cargo de una versión del Form 3100
 203    /// GET /api/form3100/{id}/personas-cargo
 204    /// </summary>
 205    [HttpGet("{id}/personas-cargo")]
 206    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 207    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 208    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 209    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 210    public async Task<IActionResult> GetPersonasACargo(long id)
 211    {
 212        try
 213        {
 214            var form = await _form3100Service.GetByIdAsync(id);
 215            if (form == null)
 216                return NotFound(ApiResponse<string>.ErrorResult("Form 3100 no encontrado"));
 217
 218            var personas = form.PersonasACargo?.Select(p => new
 219            {
 220                id = p.Id, nombre = p.Nombre, apellido = p.Apellido,
 221                fechaNacimiento = p.FechaNacimiento, cedula = p.Cedula,
 222                sexo = p.Sexo, porcentajeAtribucion = p.PorcentajeAtribucion,
 223                relacion = p.Relacion, discapacidad = p.Discapacidad
 224            });
 225            return Ok(ApiResponse<object>.SuccessResult(personas ?? Enumerable.Empty<object>()));
 226        }
 227        catch (Exception ex)
 228        {
 229            _logger.LogError(ex, "Error al listar personas a cargo de Form 3100 {Id}", id);
 230            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 231        }
 232    }
 233
 234    /// <summary>
 235    /// Agregar persona a cargo a una versión del Form 3100
 236    /// POST /api/form3100/{id}/personas-cargo
 237    /// </summary>
 238    [HttpPost("{id}/personas-cargo")]
 239    [Authorize(Roles = "Administrador,Usuario")]
 240    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status201Created)]
 241    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 242    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 243    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 244    public async Task<IActionResult> AgregarPersonaACargo(long id, [FromBody] AgregarPersonaACargoRequest request)
 245    {
 246        try
 247        {
 248            var persona = new Form3100PersonaACargo
 249            {
 250                Nombre = request.Nombre,
 251                Apellido = request.Apellido,
 252                FechaNacimiento = request.FechaNacimiento.Kind == DateTimeKind.Unspecified
 253                    ? DateTime.SpecifyKind(request.FechaNacimiento.Date, DateTimeKind.Utc)
 254                    : request.FechaNacimiento.Date.ToUniversalTime(),
 255                Cedula = request.Cedula,
 256                Sexo = request.Sexo,
 257                PorcentajeAtribucion = (short)request.PorcentajeAtribucion,
 258                Relacion = request.Relacion,
 259                Discapacidad = request.Discapacidad
 260            };
 261
 262            var creada = await _form3100Service.AgregarPersonaACargoAsync(id, persona);
 263
 264            return StatusCode(201, ApiResponse<object>.SuccessResult(
 265                new { id = creada.Id, creado = true },
 266                "Persona a cargo agregada exitosamente"));
 267        }
 268        catch (PeriodoBloqueadoException ex)
 269        {
 270            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 271        }
 272        catch (ArgumentException ex)
 273        {
 274            return BadRequest(ApiResponse<string>.ErrorResult(ex.Message));
 275        }
 276        catch (Exception ex)
 277        {
 278            _logger.LogError(ex, "Error al agregar persona a cargo a Form 3100 {Id}", id);
 279            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 280        }
 281    }
 282
 283    /// <summary>
 284    /// Eliminar persona a cargo de una versión del Form 3100
 285    /// DELETE /api/form3100/{id}/personas-cargo/{personaCargoId}
 286    /// </summary>
 287    [HttpDelete("{id}/personas-cargo/{personaCargoId}")]
 288    [Authorize(Roles = "Administrador,Usuario")]
 289    [ProducesResponseType(StatusCodes.Status204NoContent)]
 290    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 291    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 292    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 293    public async Task<IActionResult> EliminarPersonaACargo(long id, long personaCargoId)
 294    {
 295        try
 296        {
 297            await _form3100Service.EliminarPersonaACargoAsync(id, personaCargoId);
 298            return NoContent();
 299        }
 300        catch (PeriodoBloqueadoException ex)
 301        {
 302            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 303        }
 304        catch (ArgumentException ex)
 305        {
 306            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 307        }
 308        catch (Exception ex)
 309        {
 310            _logger.LogError(ex, "Error al eliminar persona a cargo {PersonaCargoId} de Form 3100 {Id}", personaCargoId,
 311            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 312        }
 313    }
 314
 315    /// <summary>
 316    /// Subir archivo escaneado del Form 3100
 317    /// POST /api/form3100/{id}/archivo (multipart/form-data, campo "file")
 318    /// </summary>
 319    [HttpPost("{id}/archivo")]
 320    [Authorize(Roles = "Administrador,Usuario")]
 321    [RequestSizeLimit(20_000_000)] // 20 MB
 322    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 323    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 324    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 325    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 326    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 327    public async Task<IActionResult> SubirArchivo(long id, IFormFile file)
 328    {
 329        try
 330        {
 331            if (file == null || file.Length == 0)
 332                return BadRequest(ApiResponse<string>.ErrorResult("Archivo requerido"));
 333
 334            var usuarioId = _currentUser.UserId ?? 0;
 335            await using var stream = file.OpenReadStream();
 336            var rutaRelativa = await _form3100Service.SubirArchivoAsync(
 337                id,
 338                stream,
 339                file.FileName,
 340                file.ContentType,
 341                usuarioId);
 342
 343            return Ok(ApiResponse<object>.SuccessResult(
 344                new { rutaRelativa, subido = true },
 345                "Archivo subido exitosamente"));
 346        }
 347        catch (PeriodoBloqueadoException ex)
 348        {
 349            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 350        }
 351        catch (ArgumentException ex)
 352        {
 353            if (ex.Message.Contains("no encontrado", StringComparison.OrdinalIgnoreCase))
 354                return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 355            return BadRequest(ApiResponse<string>.ErrorResult(ex.Message));
 356        }
 357        catch (Exception ex)
 358        {
 359            _logger.LogError(ex, "Error al subir archivo de Form 3100 {Id}", id);
 360            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 361        }
 362    }
 363
 364    /// <summary>
 365    /// Descargar archivo escaneado del Form 3100
 366    /// GET /api/form3100/{id}/archivo
 367    /// </summary>
 368    /// <summary>
 369    /// GET /api/form3100/{id}/archivo?inline=true
 370    /// inline=true  → Content-Disposition: inline  (para mostrar en visor del navegador)
 371    /// inline=false → Content-Disposition: attachment (para forzar descarga, default)
 372    /// </summary>
 373    [HttpGet("{id}/archivo")]
 374    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 375    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 376    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 377    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 378    public async Task<IActionResult> DescargarArchivo(long id, [FromQuery] bool inline = true)
 379    {
 380        try
 381        {
 382            var (contenido, nombreOriginal, contentType) = await _form3100Service.ObtenerArchivoAsync(id);
 383
 384            if (inline)
 385            {
 386                Response.Headers.ContentDisposition =
 387                    $"inline; filename=\"{nombreOriginal}\"; filename*=UTF-8''{Uri.EscapeDataString(nombreOriginal)}";
 388                return File(contenido, contentType);
 389            }
 390
 391            return File(contenido, contentType, nombreOriginal);
 392        }
 393        catch (ArgumentException ex)
 394        {
 395            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 396        }
 397        catch (KeyNotFoundException ex)
 398        {
 399            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 400        }
 401        catch (StorageFileNotFoundException ex)
 402        {
 403            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 404        }
 405        catch (Exception ex)
 406        {
 407            _logger.LogError(ex, "Error al descargar archivo de Form 3100 {Id}", id);
 408            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 409        }
 410    }
 411
 412    private static object MapToDto(Form3100 f)
 413    {
 414        return new
 415        {
 416            id = f.Id,
 417            personaId = f.PersonaId,
 418            cappuCategoria = f.CappuCategoria,
 419            fondoSolidaridad = f.FondoSolidaridad,
 420            adicionalFondoSolidaridad = f.AdicionalFondoSolidaridad,
 421            aplicaMinimoNoImponible = f.AplicaMinimoNoImponible,
 422            conyugeNombre = f.ConyugeNombre,
 423            conyugeApellido = f.ConyugeApellido,
 424            conyugeFechaNacimiento = f.ConyugeFechaNacimiento,
 425            conyugeCedula = f.ConyugeCedula,
 426            conyugeSexo = f.ConyugeSexo,
 427            conyugeNacionalidad = f.ConyugeNacionalidad,
 428            vigenteDesdeMes = f.VigenteDesde_Mes,
 429            vigenteDesdeAnio = f.VigenteDesde_Anio,
 430            documentoId = f.DocumentoId,
 431            fechaCreacion = f.FechaCreacion,
 432            usuarioCreacionId = f.UsuarioCreacionId,
 433            personasACargo = f.PersonasACargo?.Select(p => new
 434            {
 435                id = p.Id,
 436                nombre = p.Nombre,
 437                apellido = p.Apellido,
 438                fechaNacimiento = p.FechaNacimiento,
 439                cedula = p.Cedula,
 440                sexo = p.Sexo,
 441                porcentajeAtribucion = p.PorcentajeAtribucion,
 442                relacion = p.Relacion,
 443                discapacidad = p.Discapacidad
 444            })
 445        };
 446    }
 447}
 448
 449public class CrearForm3100Request
 450{
 451    [Required(ErrorMessage = "cedulaTitular es requerida")]
 452    [MaxLength(12)]
 19453    public string CedulaTitular { get; set; } = string.Empty;
 454
 10455    public short? CappuCategoria { get; set; }
 456
 457    [MaxLength(10)]
 6458    public string? FondoSolidaridad { get; set; }
 459
 3460    public bool AdicionalFondoSolidaridad { get; set; }
 461
 3462    public bool AplicaMinimoNoImponible { get; set; }
 463
 464    [MaxLength(100)]
 3465    public string? ConyugeNombre { get; set; }
 466
 467    [MaxLength(100)]
 3468    public string? ConyugeApellido { get; set; }
 469
 3470    public DateTime? ConyugeFechaNacimiento { get; set; }
 471
 472    [MaxLength(20)]
 3473    public string? ConyugeCedula { get; set; }
 474
 475    [MaxLength(1)]
 3476    public string? ConyugeSexo { get; set; }
 477
 478    [MaxLength(100)]
 3479    public string? ConyugeNacionalidad { get; set; }
 480
 481    [Required]
 482    [Range(1, 12)]
 19483    public int VigenteDesdeMes { get; set; }
 484
 485    [Required]
 486    [Range(2000, 2100)]
 16487    public int VigenteDesdeAnio { get; set; }
 488}
 489
 490public class AgregarPersonaACargoRequest
 491{
 492    [Required]
 493    [MaxLength(100)]
 494    public string Nombre { get; set; } = string.Empty;
 495
 496    [Required]
 497    [MaxLength(100)]
 498    public string Apellido { get; set; } = string.Empty;
 499
 500    [Required]
 501    public DateTime FechaNacimiento { get; set; }
 502
 503    [MaxLength(20)]
 504    public string? Cedula { get; set; }
 505
 506    [Required]
 507    [MaxLength(1)]
 508    public string Sexo { get; set; } = string.Empty;
 509
 510    [Required]
 511    [Range(0, 100)]
 512    public int PorcentajeAtribucion { get; set; }
 513
 514    [Required]
 515    [MaxLength(100)]
 516    public string Relacion { get; set; } = string.Empty;
 517
 518    public bool Discapacidad { get; set; }
 519}