< Summary

Information
Class: FAU.API.Controllers.DescuentosPersonalesController
Assembly: FAU.API
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/DescuentosPersonalesController.cs
Line coverage
76%
Covered lines: 116
Uncovered lines: 36
Coverable lines: 152
Total lines: 397
Line coverage: 76.3%
Branch coverage
43%
Covered branches: 21
Total branches: 48
Branch coverage: 43.7%
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()62.5%8881.25%
Listar()50%3250%
Editar()50%2257.14%
Eliminar()100%1150%
ConfirmarUno()100%1158.33%
Confirmar()50%2290%
ValidarImportacion()75%10869.23%
AplicarImportacion()70%111078.26%

File(s)

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

#LineLine coverage
 1using FAU.Entidades;
 2using FAU.Logica.DTOs;
 3using FAU.Logica.Services;
 4using Microsoft.AspNetCore.Authorization;
 5using Microsoft.AspNetCore.Http;
 6using Microsoft.AspNetCore.Mvc;
 7using System.ComponentModel.DataAnnotations;
 8
 9namespace FAU.API.Controllers;
 10
 11/// <summary>
 12/// Gestión de descuentos personales del período
 13/// </summary>
 14[ApiController]
 15[Route("api/descuentos-personales")]
 16[Authorize]
 17[Produces("application/json")]
 18public class DescuentosPersonalesController : ControllerBase
 19{
 20    private readonly IDescuentoPersonalService _service;
 21    private readonly ICurrentUserContext _currentUser;
 22    private readonly ILogger<DescuentosPersonalesController> _logger;
 23
 1924    public DescuentosPersonalesController(
 1925        IDescuentoPersonalService service,
 1926        ICurrentUserContext currentUser,
 1927        ILogger<DescuentosPersonalesController> logger)
 28    {
 1929        _service = service;
 1930        _currentUser = currentUser;
 1931        _logger = logger;
 1932    }
 33
 34    /// <summary>
 35    /// Crear un descuento personal en borrador
 36    /// POST /api/descuentos-personales
 37    /// </summary>
 38    [HttpPost]
 39    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 40    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status201Created)]
 41    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 42    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 43    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 44    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 45    public async Task<IActionResult> Crear([FromBody] CrearDescuentoPersonalRequest request)
 46    {
 47        try
 48        {
 449            if (request.PeriodoId <= 0)
 050                return BadRequest(ApiResponse<string>.ErrorResult("periodoId es requerido"));
 451            if (string.IsNullOrWhiteSpace(request.CedulaTitular))
 052                return BadRequest(ApiResponse<string>.ErrorResult("cedulaTitular es requerida"));
 453            if (request.AcreedorId <= 0)
 054                return BadRequest(ApiResponse<string>.ErrorResult("acreedorId es requerido"));
 455            if (request.Importe <= 0)
 156                return BadRequest(ApiResponse<string>.ErrorResult("importe debe ser mayor a cero"));
 57
 358            var usuarioId = _currentUser.UserId ?? 0;
 359            var creado = await _service.CrearAsync(
 360                request.PeriodoId,
 361                request.CedulaTitular,
 362                request.CodigoConcepto,
 363                request.AcreedorId,
 364                request.Importe,
 365                request.Observaciones,
 366                usuarioId,
 367                request.FechaComunicacion);
 68
 169            return CreatedAtAction(
 170                nameof(Listar),
 171                new { periodoId = request.PeriodoId },
 172                ApiResponse<object>.SuccessResult(
 173                    new { id = creado.Id, creado = true },
 174                    "Descuento personal creado exitosamente"));
 75        }
 176        catch (ArgumentException ex)
 77        {
 178            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 79        }
 180        catch (InvalidOperationException ex)
 81        {
 182            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 83        }
 084        catch (Exception ex)
 85        {
 086            _logger.LogError(ex, "Error al crear descuento personal");
 087            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 88        }
 489    }
 90
 91    /// <summary>
 92    /// Listar descuentos personales del período
 93    /// GET /api/descuentos-personales?periodoId=...&amp;cedulaTitular=...&amp;codigoConcepto=...&amp;estado=...
 94    /// </summary>
 95    [HttpGet]
 96    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 97    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 98    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 99    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 100    public async Task<IActionResult> Listar(
 101        [FromQuery] long periodoId,
 102        [FromQuery] string? cedulaTitular = null,
 103        [FromQuery] int? codigoConcepto = null,
 104        [FromQuery] string? estado = null)
 105    {
 106        try
 107        {
 1108            if (periodoId <= 0)
 0109                return BadRequest(ApiResponse<string>.ErrorResult("periodoId es requerido"));
 110
 1111            var items = await _service.ListarAsync(periodoId, cedulaTitular, codigoConcepto, estado);
 112
 1113            var response = items.Select(d => new
 1114            {
 1115                id = d.Id,
 1116                periodoId = d.PeriodoId,
 1117                personaId = d.PersonaId,
 1118                cedula = d.Persona?.Cedula,
 1119                nombreCompleto = d.Persona?.NombreCompleto,
 1120                acreedorId = d.AcreedorId,
 1121                acreedorCodigo = d.Acreedor?.Codigo,
 1122                acreedorNombre = d.Acreedor?.Nombre,
 1123                categoriaDeficit = d.Acreedor?.CategoriaDeficit,
 1124                ordenLegalDeficit = d.Acreedor?.OrdenLegalDeficit,
 1125                catalogoItemId = d.CatalogoItemId,
 1126                codigoConcepto = d.CatalogoItem?.Codigo,
 1127                nombreConcepto = d.CatalogoItem?.Nombre,
 1128                importe = d.Importe,
 1129                estado = d.Estado,
 1130                observaciones = d.Observaciones,
 1131                fechaComunicacion = d.FechaComunicacion,
 1132                fechaCreacion = d.FechaCreacion,
 1133                usuarioCreacionId = d.UsuarioCreacionId
 1134            });
 135
 1136            return Ok(ApiResponse<object>.SuccessResult(response));
 137        }
 0138        catch (Exception ex)
 139        {
 0140            _logger.LogError(ex, "Error al listar descuentos personales");
 0141            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 142        }
 1143    }
 144
 145    /// <summary>
 146    /// Editar un descuento personal en borrador
 147    /// PUT /api/descuentos-personales/{id}
 148    /// </summary>
 149    [HttpPut("{id}")]
 150    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 151    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 152    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 153    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 154    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 155    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 156    public async Task<IActionResult> Editar(long id, [FromBody] EditarDescuentoPersonalRequest request)
 157    {
 158        try
 159        {
 2160            if (request.Importe <= 0)
 0161                return BadRequest(ApiResponse<string>.ErrorResult("importe debe ser mayor a cero"));
 162
 2163            var actualizado = await _service.EditarAsync(id, request.Importe, request.Observaciones, request.FechaComuni
 1164            return Ok(ApiResponse<object>.SuccessResult(
 1165                new { id = actualizado.Id, actualizado = true },
 1166                "Descuento personal actualizado exitosamente"));
 167        }
 0168        catch (ArgumentException ex)
 169        {
 0170            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 171        }
 1172        catch (InvalidOperationException ex)
 173        {
 1174            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 175        }
 0176        catch (Exception ex)
 177        {
 0178            _logger.LogError(ex, "Error al actualizar descuento personal {Id}", id);
 0179            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 180        }
 2181    }
 182
 183    /// <summary>
 184    /// Eliminar un descuento personal en borrador
 185    /// DELETE /api/descuentos-personales/{id}
 186    /// </summary>
 187    [HttpDelete("{id}")]
 188    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 189    [ProducesResponseType(StatusCodes.Status204NoContent)]
 190    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 191    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 192    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 193    public async Task<IActionResult> Eliminar(long id)
 194    {
 195        try
 196        {
 2197            await _service.EliminarAsync(id);
 1198            return NoContent();
 199        }
 0200        catch (ArgumentException ex)
 201        {
 0202            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 203        }
 1204        catch (InvalidOperationException ex)
 205        {
 1206            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 207        }
 0208        catch (Exception ex)
 209        {
 0210            _logger.LogError(ex, "Error al eliminar descuento personal {Id}", id);
 0211            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 212        }
 2213    }
 214
 215    /// <summary>
 216    /// Confirmar un descuento personal en borrador
 217    /// POST /api/descuentos-personales/{id}/confirmar
 218    /// </summary>
 219    [HttpPost("{id}/confirmar")]
 220    [Authorize(Roles = "Administrador,Supervisor")]
 221    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 222    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 223    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 224    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 225    public async Task<IActionResult> ConfirmarUno(long id)
 226    {
 227        try
 228        {
 2229            var confirmado = await _service.ConfirmarAsync(id);
 1230            return Ok(ApiResponse<object>.SuccessResult(
 1231                new { confirmado = true },
 1232                $"Descuento personal {confirmado.Id} confirmado exitosamente"));
 233        }
 0234        catch (ArgumentException ex)
 235        {
 0236            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 237        }
 1238        catch (InvalidOperationException ex)
 239        {
 1240            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 241        }
 0242        catch (Exception ex)
 243        {
 0244            _logger.LogError(ex, "Error al confirmar descuento personal {Id}", id);
 0245            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 246        }
 2247    }
 248
 249    /// <summary>
 250    /// Confirmar todos los borradores de un período
 251    /// POST /api/descuentos-personales/confirmar
 252    /// </summary>
 253    [HttpPost("confirmar")]
 254    [Authorize(Roles = "Administrador,Supervisor")]
 255    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
 256    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 257    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 258    public async Task<IActionResult> Confirmar([FromBody] ConfirmarDescuentosRequest request)
 259    {
 260        try
 261        {
 2262            if (request.PeriodoId <= 0)
 0263                return BadRequest(ApiResponse<string>.ErrorResult("periodoId es requerido"));
 264
 2265            var cantidad = await _service.ConfirmarBorradoresAsync(request.PeriodoId);
 1266            return Ok(ApiResponse<object>.SuccessResult(
 1267                new { confirmados = cantidad },
 1268                $"Se confirmaron {cantidad} descuento(s) personal(es)"));
 269        }
 1270        catch (Exception ex)
 271        {
 1272            _logger.LogError(ex, "Error al confirmar descuentos personales del período {PeriodoId}", request.PeriodoId);
 1273            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 274        }
 2275    }
 276
 277    /// <summary>
 278    /// Valida (dry-run) un archivo .txt de carga masiva de descuentos personales de un acreedor,
 279    /// sin persistir cambios.
 280    /// POST /api/descuentos-personales/importacion/validar?acreedorId=...
 281    /// </summary>
 282    [HttpPost("importacion/validar")]
 283    [Authorize(Roles = "Administrador,Supervisor")]
 284    [ProducesResponseType(typeof(ApiResponse<ResultadoValidacionImportacionDescuentosDto>), StatusCodes.Status200OK)]
 285    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 286    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 287    public async Task<IActionResult> ValidarImportacion(IFormFile archivo, [FromQuery] long acreedorId)
 288    {
 289        try
 290        {
 3291            if (archivo == null || archivo.Length == 0)
 0292                return BadRequest(ApiResponse<string>.ErrorResult("Debe adjuntar un archivo .txt"));
 3293            if (!archivo.FileName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
 1294                return BadRequest(ApiResponse<string>.ErrorResult("El archivo debe tener extensión .txt"));
 2295            if (acreedorId <= 0)
 1296                return BadRequest(ApiResponse<string>.ErrorResult("acreedorId es requerido"));
 297
 1298            using var stream = archivo.OpenReadStream();
 1299            var resultado = await _service.ValidarImportacionAsync(acreedorId, stream);
 1300            return Ok(ApiResponse<ResultadoValidacionImportacionDescuentosDto>.SuccessResult(resultado));
 301        }
 0302        catch (Exception ex)
 303        {
 0304            _logger.LogError(ex, "Error al validar una importación de descuentos personales");
 0305            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 306        }
 3307    }
 308
 309    /// <summary>
 310    /// Aplica una carga masiva de descuentos personales de un acreedor desde un archivo .txt.
 311    /// Reemplaza los borradores previos de ese acreedor en el período en curso.
 312    /// Si el archivo tiene errores, se rechaza por completo (no se persiste nada).
 313    /// POST /api/descuentos-personales/importacion/aplicar?acreedorId=...
 314    /// </summary>
 315    [HttpPost("importacion/aplicar")]
 316    [Authorize(Roles = "Administrador,Supervisor")]
 317    [ProducesResponseType(typeof(ApiResponse<ResultadoImportacionDescuentosDto>), StatusCodes.Status200OK)]
 318    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 319    [ProducesResponseType(typeof(ApiResponse<ResultadoImportacionDescuentosDto>), StatusCodes.Status409Conflict)]
 320    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 321    public async Task<IActionResult> AplicarImportacion(IFormFile archivo, [FromQuery] long acreedorId)
 322    {
 323        try
 324        {
 3325            if (archivo == null || archivo.Length == 0)
 0326                return BadRequest(ApiResponse<string>.ErrorResult("Debe adjuntar un archivo .txt"));
 3327            if (!archivo.FileName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
 1328                return BadRequest(ApiResponse<string>.ErrorResult("El archivo debe tener extensión .txt"));
 2329            if (acreedorId <= 0)
 0330                return BadRequest(ApiResponse<string>.ErrorResult("acreedorId es requerido"));
 331
 2332            var usuarioId = _currentUser.UserId ?? 0;
 2333            using var stream = archivo.OpenReadStream();
 2334            var resultado = await _service.AplicarImportacionAsync(acreedorId, stream, usuarioId, _currentUser.Host);
 335
 2336            if (!resultado.Aplicado)
 337            {
 1338                return Conflict(new ApiResponse<ResultadoImportacionDescuentosDto>
 1339                {
 1340                    Success = false,
 1341                    Data = resultado,
 1342                    Error = "El archivo contiene errores y no se aplicó."
 1343                });
 344            }
 345
 1346            return Ok(ApiResponse<ResultadoImportacionDescuentosDto>.SuccessResult(
 1347                resultado,
 1348                "Descuentos personales importados exitosamente"));
 349        }
 0350        catch (Exception ex)
 351        {
 0352            _logger.LogError(ex, "Error al aplicar una importación de descuentos personales");
 0353            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 354        }
 3355    }
 356}
 357
 358public class CrearDescuentoPersonalRequest
 359{
 360    [Required]
 361    public long PeriodoId { get; set; }
 362
 363    [Required]
 364    [MaxLength(12)]
 365    public string CedulaTitular { get; set; } = string.Empty;
 366
 367    [Required]
 368    public int CodigoConcepto { get; set; }
 369
 370    [Required]
 371    public long AcreedorId { get; set; }
 372
 373    [Required]
 374    public decimal Importe { get; set; }
 375
 376    [MaxLength(500)]
 377    public string? Observaciones { get; set; }
 378
 379    public DateTime? FechaComunicacion { get; set; }
 380}
 381
 382public class EditarDescuentoPersonalRequest
 383{
 384    [Required]
 385    public decimal Importe { get; set; }
 386
 387    [MaxLength(500)]
 388    public string? Observaciones { get; set; }
 389
 390    public DateTime? FechaComunicacion { get; set; }
 391}
 392
 393public class ConfirmarDescuentosRequest
 394{
 395    [Required]
 396    public long PeriodoId { get; set; }
 397}