< Summary

Information
Class: FAU.API.Controllers.PeriodosController
Assembly: FAU.API
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/PeriodosController.cs
Line coverage
59%
Covered lines: 93
Uncovered lines: 63
Coverable lines: 156
Total lines: 365
Line coverage: 59.6%
Branch coverage
63%
Covered branches: 47
Total branches: 74
Branch coverage: 63.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
AbrirPeriodo()80%131069.23%
GetUltimos()0%620%
GetPeriodoActivo()100%2262.5%
GetPeriodo()100%2262.5%
GetPrecondiciones()0%620%
ValidarInsumos()0%620%
CerrarInsumos()75%201675%
ReabrirInsumos()75%11863.63%
ReabrirDesdeCalculada()0%7280%
Calcular()87.5%88100%
<Calcular()100%1130%
Cerrar()87.5%9872.72%
MapearPeriodo(...)50%66100%

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/PeriodosController.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.Mvc;
 7
 8namespace FAU.API.Controllers;
 9
 10[ApiController]
 11[Route("api/liquidacion/[controller]")]
 12[Authorize]
 13[Produces("application/json")]
 14public class PeriodosController : ControllerBase
 15{
 16    private readonly IPeriodoService _periodoService;
 17    private readonly ILiquidacionService _liquidacionService;
 18    private readonly ILogger<PeriodosController> _logger;
 19    private readonly IServiceScopeFactory _scopeFactory;
 20    private readonly ICurrentUserContext _currentUser;
 21
 1922    public PeriodosController(
 1923        IPeriodoService periodoService,
 1924        ILiquidacionService liquidacionService,
 1925        ILogger<PeriodosController> logger,
 1926        IServiceScopeFactory scopeFactory,
 1927        ICurrentUserContext currentUser)
 28    {
 1929        _periodoService       = periodoService;
 1930        _liquidacionService   = liquidacionService;
 1931        _logger               = logger;
 1932        _scopeFactory         = scopeFactory;
 1933        _currentUser          = currentUser;
 1934    }
 35
 36    [HttpPost]
 37    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 38    [ProducesResponseType(typeof(ApiResponse<PeriodoDto>), StatusCodes.Status201Created)]
 39    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status400BadRequest)]
 40    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 41    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 42    public async Task<IActionResult> AbrirPeriodo([FromBody] AbrirPeriodoRequest request)
 43    {
 44        try
 45        {
 346            if (!ModelState.IsValid)
 47            {
 048                return BadRequest(ApiResponse<string>.ErrorResult("Solicitud de apertura de período inválida"));
 49            }
 50
 351            var (success, periodo, error) = await _periodoService.AbrirPeriodoAsync(request.Anio, request.Mes);
 52
 353            if (!success)
 54            {
 255                if (error?.Contains("Ya existe un período activo", StringComparison.OrdinalIgnoreCase) == true)
 56                {
 157                    return Conflict(ApiResponse<string>.ErrorResult(error));
 58                }
 59
 160                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al abrir el período"));
 61            }
 62
 163            var periodoFinal = await _periodoService.ObtenerPorIdAsync(periodo!.Id);
 164            return CreatedAtAction(nameof(GetPeriodo), new { id = periodo.Id }, ApiResponse<PeriodoDto>.SuccessResult(Ma
 65        }
 066        catch (Exception ex)
 67        {
 068            _logger.LogError(ex, "Error al abrir período");
 069            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 70        }
 371    }
 72
 73    [HttpGet]
 74    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 75    [ProducesResponseType(typeof(ApiResponse<List<PeriodoDto>>), StatusCodes.Status200OK)]
 76    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 77    public async Task<IActionResult> GetUltimos([FromQuery] int cantidad = 6)
 78    {
 79        try
 80        {
 081            var periodos = await _periodoService.ObtenerUltimosAsync(Math.Min(cantidad, 12));
 082            return Ok(ApiResponse<List<PeriodoDto>>.SuccessResult(periodos.Select(MapearPeriodo).ToList()));
 83        }
 084        catch (Exception ex)
 85        {
 086            _logger.LogError(ex, "Error al obtener últimos períodos");
 087            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 88        }
 089    }
 90
 91    [HttpGet("activo")]
 92    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 93    [ProducesResponseType(typeof(ApiResponse<PeriodoDto>), StatusCodes.Status200OK)]
 94    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 95    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 96    public async Task<IActionResult> GetPeriodoActivo()
 97    {
 98        try
 99        {
 2100            var periodo = await _periodoService.ObtenerEnCursoAsync();
 2101            if (periodo == null)
 102            {
 1103                return NotFound(ApiResponse<string>.ErrorResult("No hay un período activo"));
 104            }
 105
 1106            return Ok(ApiResponse<PeriodoDto>.SuccessResult(MapearPeriodo(periodo)));
 107        }
 0108        catch (Exception ex)
 109        {
 0110            _logger.LogError(ex, "Error al obtener período activo");
 0111            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 112        }
 2113    }
 114
 115    [HttpGet("{id}")]
 116    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 117    [ProducesResponseType(typeof(ApiResponse<PeriodoDto>), StatusCodes.Status200OK)]
 118    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 119    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 120    public async Task<IActionResult> GetPeriodo(long id)
 121    {
 122        try
 123        {
 2124            var periodo = await _periodoService.ObtenerPorIdAsync(id);
 2125            if (periodo == null)
 126            {
 1127                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 128            }
 129
 1130            return Ok(ApiResponse<PeriodoDto>.SuccessResult(MapearPeriodo(periodo)));
 131        }
 0132        catch (Exception ex)
 133        {
 0134            _logger.LogError(ex, "Error al obtener período {Id}", id);
 0135            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 136        }
 2137    }
 138
 139    [HttpGet("{id}/precondiciones")]
 140    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 141    [ProducesResponseType(typeof(ApiResponse<PrecondicionesDto>), StatusCodes.Status200OK)]
 142    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 143    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 144    public async Task<IActionResult> GetPrecondiciones(long id)
 145    {
 146        try
 147        {
 0148            var precondiciones = await _periodoService.ObtenerPrecondicionesAsync(id);
 0149            if (precondiciones == null)
 0150                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 0151            return Ok(ApiResponse<PrecondicionesDto>.SuccessResult(precondiciones));
 152        }
 0153        catch (Exception ex)
 154        {
 0155            _logger.LogError(ex, "Error al obtener precondiciones del período {Id}", id);
 0156            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 157        }
 0158    }
 159
 160    [HttpGet("{id}/validar-insumos")]
 161    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 162    [ProducesResponseType(typeof(ApiResponse<ValidacionInsumosDto>), StatusCodes.Status200OK)]
 163    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 164    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 165    public async Task<IActionResult> ValidarInsumos(long id)
 166    {
 167        try
 168        {
 0169            var validacion = await _periodoService.ValidarInsumosAsync(id);
 0170            if (validacion == null)
 0171                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 0172            return Ok(ApiResponse<ValidacionInsumosDto>.SuccessResult(validacion));
 173        }
 0174        catch (Exception ex)
 175        {
 0176            _logger.LogError(ex, "Error al validar insumos del período {Id}", id);
 0177            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 178        }
 0179    }
 180
 181    [HttpPost("{id}/cerrar-insumos")]
 182    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 183    [ProducesResponseType(typeof(ApiResponse<PeriodoDto>), StatusCodes.Status200OK)]
 184    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 185    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 186    [ProducesResponseType(typeof(ValidacionInsumosDto), StatusCodes.Status422UnprocessableEntity)]
 187    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 188    public async Task<IActionResult> CerrarInsumos(long id, [FromBody] CerrarInsumosRequest? request = null)
 189    {
 190        try
 191        {
 4192            var (success, periodo, validacion, error) =
 4193                await _periodoService.CerrarInsumosAsync(id, request?.Confirmaciones);
 194
 4195            if (!success)
 196            {
 3197                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 1198                    return NotFound(ApiResponse<string>.ErrorResult(error));
 2199                if (error?.Contains("Solo se puede", StringComparison.OrdinalIgnoreCase) == true)
 1200                    return Conflict(ApiResponse<string>.ErrorResult(error));
 1201                if (validacion != null)
 1202                    return UnprocessableEntity(validacion);
 0203                return UnprocessableEntity(ApiResponse<string>.ErrorResult(error ?? "Error al cerrar insumos"));
 204            }
 205
 1206            var periodoFinal = await _periodoService.ObtenerPorIdAsync(id);
 1207            return Ok(ApiResponse<PeriodoDto>.SuccessResult(MapearPeriodo(periodoFinal!), "Insumos cerrados. Período lis
 208        }
 0209        catch (Exception ex)
 210        {
 0211            _logger.LogError(ex, "Error al cerrar insumos del período {Id}", id);
 0212            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 213        }
 4214    }
 215
 216    [HttpPost("{id}/reabrir-insumos")]
 217    [Authorize(Roles = "Administrador,Supervisor")]
 218    [ProducesResponseType(typeof(ApiResponse<PeriodoDto>), StatusCodes.Status200OK)]
 219    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 220    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 221    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 222    public async Task<IActionResult> ReabrirInsumos(long id)
 223    {
 224        try
 225        {
 2226            var (success, periodo, error) = await _periodoService.ReabrirInsumosAsync(id);
 227
 2228            if (!success)
 229            {
 1230                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0231                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1232                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al reabrir insumos"));
 233            }
 234
 1235            var periodoFinal = await _periodoService.ObtenerPorIdAsync(id);
 1236            return Ok(ApiResponse<PeriodoDto>.SuccessResult(MapearPeriodo(periodoFinal!), "Insumos reabiertos. Período e
 237        }
 0238        catch (Exception ex)
 239        {
 0240            _logger.LogError(ex, "Error al reabrir insumos del período {Id}", id);
 0241            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 242        }
 2243    }
 244
 245    [HttpPost("{id}/reabrir")]
 246    [Authorize(Roles = "Administrador,Supervisor")]
 247    [ProducesResponseType(typeof(ApiResponse<PeriodoDto>), StatusCodes.Status200OK)]
 248    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 249    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 250    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 251    public async Task<IActionResult> ReabrirDesdeCalculada(long id)
 252    {
 253        try
 254        {
 0255            var (success, periodo, error) = await _periodoService.ReabrirDesdeCalculadaAsync(id);
 256
 0257            if (!success)
 258            {
 0259                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0260                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0261                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al reabrir período"));
 262            }
 263
 0264            var periodoFinal = await _periodoService.ObtenerPorIdAsync(id);
 0265            return Ok(ApiResponse<PeriodoDto>.SuccessResult(
 0266                MapearPeriodo(periodoFinal!),
 0267                "Período reabierto. Realizá las correcciones y volvé a calcular. El próximo cálculo generará una nueva v
 268        }
 0269        catch (Exception ex)
 270        {
 0271            _logger.LogError(ex, "Error al reabrir período {Id}", id);
 0272            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 273        }
 0274    }
 275
 276    [HttpPost("{id}/calcular")]
 277    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 278    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status202Accepted)]
 279    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 280    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 281    public async Task<IActionResult> Calcular(long id)
 282    {
 3283        var (success, _, error) = await _periodoService.IniciarCalculoAsync(id);
 3284        if (!success)
 285        {
 2286            if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 1287                return NotFound(ApiResponse<string>.ErrorResult(error));
 1288            return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al iniciar cálculo"));
 289        }
 290
 291        // Capturar antes de que el HttpContext desaparezca (el Task.Run corre sin request)
 1292        var usuarioId = _currentUser.UserId;
 1293        var host      = _currentUser.Host;
 294
 295        // ponytail: fire-and-forget con scope propio; los servicios scoped se disponen al terminar el request
 1296        _ = Task.Run(async () =>
 1297        {
 1298            using var scope = _scopeFactory.CreateScope();
 1299            var liquidacionService = scope.ServiceProvider.GetRequiredService<ILiquidacionService>();
 0300            var periodoService     = scope.ServiceProvider.GetRequiredService<IPeriodoService>();
 1301            try
 1302            {
 0303                var (exito, _, __) = await liquidacionService.GenerarLiquidacionAsync(id, usuarioId, host);
 0304                await periodoService.FinalizarCalculoAsync(id, exito);
 0305            }
 0306            catch (Exception ex)
 1307            {
 0308                _logger.LogError(ex, "Error en cálculo background del período {Id}", id);
 0309                await periodoService.FinalizarCalculoAsync(id, exitoso: false);
 1310            }
 1311        });
 312
 1313        return Accepted(ApiResponse<string>.SuccessResult("en_proceso", "Cálculo iniciado. Consultá el estado del períod
 3314    }
 315
 316    [HttpPost("{id}/cerrar")]
 317    [Authorize(Roles = "Administrador,Supervisor")]
 318    [ProducesResponseType(typeof(ApiResponse<PeriodoDto>), StatusCodes.Status200OK)]
 319    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 320    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 321    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 322    public async Task<IActionResult> Cerrar(long id)
 323    {
 324        try
 325        {
 3326            var (success, periodo, error) = await _periodoService.CerrarAsync(id);
 327
 3328            if (!success)
 329            {
 2330                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 1331                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1332                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al cerrar período"));
 333            }
 334
 1335            var periodoFinal = await _periodoService.ObtenerPorIdAsync(id);
 1336            return Ok(ApiResponse<PeriodoDto>.SuccessResult(MapearPeriodo(periodoFinal!), "Período cerrado definitivamen
 337        }
 0338        catch (Exception ex)
 339        {
 0340            _logger.LogError(ex, "Error al cerrar período {Id}", id);
 0341            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 342        }
 3343    }
 344
 345    private static PeriodoDto MapearPeriodo(Periodo periodo)
 346    {
 6347        return new PeriodoDto
 6348        {
 6349            Id = periodo.Id,
 6350            Anio = periodo.Anio,
 6351            Mes = periodo.Mes,
 6352            Estado = periodo.Estado,
 6353            SnapshotId = periodo.SnapshotId,
 6354            FechaApertura = periodo.FechaApertura,
 6355            UsuarioApertura = periodo.UsuarioAperturaUsername,
 6356            HashSnapshot = periodo.HashSnapshot,
 6357            FechaCierreInsumos = periodo.FechaCierreInsumos,
 6358            UsuarioCierreInsumos = periodo.UsuarioCierreInsumos?.Username,
 6359            FechaCalculo = periodo.FechaCalculo,
 6360            UsuarioCalculo = periodo.UsuarioCalculo?.Username,
 6361            FechaCierre = periodo.FechaCierre,
 6362            UsuarioCierre = periodo.UsuarioCierre?.Username
 6363        };
 364    }
 365}