< Summary

Information
Class: FAU.API.Controllers.CrearDescuentoPersonalRequest
Assembly: FAU.API
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/DescuentosPersonalesController.cs
Line coverage
100%
Covered lines: 7
Uncovered lines: 0
Coverable lines: 7
Total lines: 397
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_PeriodoId()100%11100%
get_CedulaTitular()100%11100%
get_CodigoConcepto()100%11100%
get_AcreedorId()100%11100%
get_Importe()100%11100%
get_Observaciones()100%11100%
get_FechaComunicacion()100%11100%

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
 24    public DescuentosPersonalesController(
 25        IDescuentoPersonalService service,
 26        ICurrentUserContext currentUser,
 27        ILogger<DescuentosPersonalesController> logger)
 28    {
 29        _service = service;
 30        _currentUser = currentUser;
 31        _logger = logger;
 32    }
 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        {
 49            if (request.PeriodoId <= 0)
 50                return BadRequest(ApiResponse<string>.ErrorResult("periodoId es requerido"));
 51            if (string.IsNullOrWhiteSpace(request.CedulaTitular))
 52                return BadRequest(ApiResponse<string>.ErrorResult("cedulaTitular es requerida"));
 53            if (request.AcreedorId <= 0)
 54                return BadRequest(ApiResponse<string>.ErrorResult("acreedorId es requerido"));
 55            if (request.Importe <= 0)
 56                return BadRequest(ApiResponse<string>.ErrorResult("importe debe ser mayor a cero"));
 57
 58            var usuarioId = _currentUser.UserId ?? 0;
 59            var creado = await _service.CrearAsync(
 60                request.PeriodoId,
 61                request.CedulaTitular,
 62                request.CodigoConcepto,
 63                request.AcreedorId,
 64                request.Importe,
 65                request.Observaciones,
 66                usuarioId,
 67                request.FechaComunicacion);
 68
 69            return CreatedAtAction(
 70                nameof(Listar),
 71                new { periodoId = request.PeriodoId },
 72                ApiResponse<object>.SuccessResult(
 73                    new { id = creado.Id, creado = true },
 74                    "Descuento personal creado exitosamente"));
 75        }
 76        catch (ArgumentException ex)
 77        {
 78            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 79        }
 80        catch (InvalidOperationException ex)
 81        {
 82            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 83        }
 84        catch (Exception ex)
 85        {
 86            _logger.LogError(ex, "Error al crear descuento personal");
 87            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 88        }
 89    }
 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        {
 108            if (periodoId <= 0)
 109                return BadRequest(ApiResponse<string>.ErrorResult("periodoId es requerido"));
 110
 111            var items = await _service.ListarAsync(periodoId, cedulaTitular, codigoConcepto, estado);
 112
 113            var response = items.Select(d => new
 114            {
 115                id = d.Id,
 116                periodoId = d.PeriodoId,
 117                personaId = d.PersonaId,
 118                cedula = d.Persona?.Cedula,
 119                nombreCompleto = d.Persona?.NombreCompleto,
 120                acreedorId = d.AcreedorId,
 121                acreedorCodigo = d.Acreedor?.Codigo,
 122                acreedorNombre = d.Acreedor?.Nombre,
 123                categoriaDeficit = d.Acreedor?.CategoriaDeficit,
 124                ordenLegalDeficit = d.Acreedor?.OrdenLegalDeficit,
 125                catalogoItemId = d.CatalogoItemId,
 126                codigoConcepto = d.CatalogoItem?.Codigo,
 127                nombreConcepto = d.CatalogoItem?.Nombre,
 128                importe = d.Importe,
 129                estado = d.Estado,
 130                observaciones = d.Observaciones,
 131                fechaComunicacion = d.FechaComunicacion,
 132                fechaCreacion = d.FechaCreacion,
 133                usuarioCreacionId = d.UsuarioCreacionId
 134            });
 135
 136            return Ok(ApiResponse<object>.SuccessResult(response));
 137        }
 138        catch (Exception ex)
 139        {
 140            _logger.LogError(ex, "Error al listar descuentos personales");
 141            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 142        }
 143    }
 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        {
 160            if (request.Importe <= 0)
 161                return BadRequest(ApiResponse<string>.ErrorResult("importe debe ser mayor a cero"));
 162
 163            var actualizado = await _service.EditarAsync(id, request.Importe, request.Observaciones, request.FechaComuni
 164            return Ok(ApiResponse<object>.SuccessResult(
 165                new { id = actualizado.Id, actualizado = true },
 166                "Descuento personal actualizado exitosamente"));
 167        }
 168        catch (ArgumentException ex)
 169        {
 170            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 171        }
 172        catch (InvalidOperationException ex)
 173        {
 174            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 175        }
 176        catch (Exception ex)
 177        {
 178            _logger.LogError(ex, "Error al actualizar descuento personal {Id}", id);
 179            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 180        }
 181    }
 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        {
 197            await _service.EliminarAsync(id);
 198            return NoContent();
 199        }
 200        catch (ArgumentException ex)
 201        {
 202            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 203        }
 204        catch (InvalidOperationException ex)
 205        {
 206            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 207        }
 208        catch (Exception ex)
 209        {
 210            _logger.LogError(ex, "Error al eliminar descuento personal {Id}", id);
 211            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 212        }
 213    }
 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        {
 229            var confirmado = await _service.ConfirmarAsync(id);
 230            return Ok(ApiResponse<object>.SuccessResult(
 231                new { confirmado = true },
 232                $"Descuento personal {confirmado.Id} confirmado exitosamente"));
 233        }
 234        catch (ArgumentException ex)
 235        {
 236            return NotFound(ApiResponse<string>.ErrorResult(ex.Message));
 237        }
 238        catch (InvalidOperationException ex)
 239        {
 240            return Conflict(ApiResponse<string>.ErrorResult(ex.Message));
 241        }
 242        catch (Exception ex)
 243        {
 244            _logger.LogError(ex, "Error al confirmar descuento personal {Id}", id);
 245            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 246        }
 247    }
 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        {
 262            if (request.PeriodoId <= 0)
 263                return BadRequest(ApiResponse<string>.ErrorResult("periodoId es requerido"));
 264
 265            var cantidad = await _service.ConfirmarBorradoresAsync(request.PeriodoId);
 266            return Ok(ApiResponse<object>.SuccessResult(
 267                new { confirmados = cantidad },
 268                $"Se confirmaron {cantidad} descuento(s) personal(es)"));
 269        }
 270        catch (Exception ex)
 271        {
 272            _logger.LogError(ex, "Error al confirmar descuentos personales del período {PeriodoId}", request.PeriodoId);
 273            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 274        }
 275    }
 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        {
 291            if (archivo == null || archivo.Length == 0)
 292                return BadRequest(ApiResponse<string>.ErrorResult("Debe adjuntar un archivo .txt"));
 293            if (!archivo.FileName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
 294                return BadRequest(ApiResponse<string>.ErrorResult("El archivo debe tener extensión .txt"));
 295            if (acreedorId <= 0)
 296                return BadRequest(ApiResponse<string>.ErrorResult("acreedorId es requerido"));
 297
 298            using var stream = archivo.OpenReadStream();
 299            var resultado = await _service.ValidarImportacionAsync(acreedorId, stream);
 300            return Ok(ApiResponse<ResultadoValidacionImportacionDescuentosDto>.SuccessResult(resultado));
 301        }
 302        catch (Exception ex)
 303        {
 304            _logger.LogError(ex, "Error al validar una importación de descuentos personales");
 305            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 306        }
 307    }
 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        {
 325            if (archivo == null || archivo.Length == 0)
 326                return BadRequest(ApiResponse<string>.ErrorResult("Debe adjuntar un archivo .txt"));
 327            if (!archivo.FileName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
 328                return BadRequest(ApiResponse<string>.ErrorResult("El archivo debe tener extensión .txt"));
 329            if (acreedorId <= 0)
 330                return BadRequest(ApiResponse<string>.ErrorResult("acreedorId es requerido"));
 331
 332            var usuarioId = _currentUser.UserId ?? 0;
 333            using var stream = archivo.OpenReadStream();
 334            var resultado = await _service.AplicarImportacionAsync(acreedorId, stream, usuarioId, _currentUser.Host);
 335
 336            if (!resultado.Aplicado)
 337            {
 338                return Conflict(new ApiResponse<ResultadoImportacionDescuentosDto>
 339                {
 340                    Success = false,
 341                    Data = resultado,
 342                    Error = "El archivo contiene errores y no se aplicó."
 343                });
 344            }
 345
 346            return Ok(ApiResponse<ResultadoImportacionDescuentosDto>.SuccessResult(
 347                resultado,
 348                "Descuentos personales importados exitosamente"));
 349        }
 350        catch (Exception ex)
 351        {
 352            _logger.LogError(ex, "Error al aplicar una importación de descuentos personales");
 353            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 354        }
 355    }
 356}
 357
 358public class CrearDescuentoPersonalRequest
 359{
 360    [Required]
 12361    public long PeriodoId { get; set; }
 362
 363    [Required]
 364    [MaxLength(12)]
 15365    public string CedulaTitular { get; set; } = string.Empty;
 366
 367    [Required]
 7368    public int CodigoConcepto { get; set; }
 369
 370    [Required]
 11371    public long AcreedorId { get; set; }
 372
 373    [Required]
 11374    public decimal Importe { get; set; }
 375
 376    [MaxLength(500)]
 4377    public string? Observaciones { get; set; }
 378
 3379    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}