< Summary

Information
Class: FAU.API.Controllers.ReportePresupuestalController
Assembly: FAU.API
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/ReportePresupuestalController.cs
Line coverage
76%
Covered lines: 139
Uncovered lines: 42
Coverable lines: 181
Total lines: 432
Line coverage: 76.7%
Branch coverage
100%
Covered branches: 56
Total branches: 56
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetResumenPrograma()100%4472.72%
GetResumenProgramaCsv()100%4475%
GetRubro083()100%4472.72%
GetRubro083Csv()100%4475%
GetPadronRubro()100%4472.72%
GetPadronRubroCsv()100%4475%
GetPadronMensual()100%4472.72%
GetPadronMensualCsv()100%4475%
GetMovimientos()100%4472.72%
GetMovimientosCsv()100%4475%
GetDiferencias()100%4472.72%
GetDiferenciasCsv()100%4475%
GetNoDisponibles()100%4472.72%
GetNoDisponiblesCsv()100%4475%
RegistrarAuditoriaAsync(...)100%11100%

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/ReportePresupuestalController.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;
 7
 8namespace FAU.API.Controllers;
 9
 10[ApiController]
 11[Route("api/liquidacion/reportes")]
 12[Authorize]
 13[Produces("application/json")]
 14public class ReportePresupuestalController : ControllerBase
 15{
 16    private readonly IReportePresupuestalService _service;
 17    private readonly IPeriodoService _periodoService;
 18    private readonly IAuditoriaService _auditoriaService;
 19    private readonly ICurrentUserContext _currentUser;
 20    private readonly ILogger<ReportePresupuestalController> _logger;
 21
 4222    public ReportePresupuestalController(
 4223        IReportePresupuestalService service,
 4224        IPeriodoService periodoService,
 4225        IAuditoriaService auditoriaService,
 4226        ICurrentUserContext currentUser,
 4227        ILogger<ReportePresupuestalController> logger)
 28    {
 4229        _service          = service;
 4230        _periodoService   = periodoService;
 4231        _auditoriaService = auditoriaService;
 4232        _currentUser      = currentUser;
 4233        _logger           = logger;
 4234    }
 35
 36    // ─── Resumen por programa ────────────────────────────────────────────────
 37
 38    [HttpGet("{periodoId}/resumen-programa")]
 39    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 40    [ProducesResponseType(typeof(ApiResponse<ResumenProgramaDto>), StatusCodes.Status200OK)]
 41    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 42    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 43    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 44    public async Task<IActionResult> GetResumenPrograma(long periodoId)
 45    {
 46        try
 47        {
 348            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 349            if (periodo == null)
 150                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 251            if (periodo.Estado != Periodo.EstadoCerrada)
 152                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 53
 154            var result = await _service.ObtenerResumenProgramaAsync(periodoId);
 155            return Ok(ApiResponse<ResumenProgramaDto>.SuccessResult(result));
 56        }
 057        catch (Exception ex)
 58        {
 059            _logger.LogError(ex, "Error al obtener resumen programa del período {PeriodoId}", periodoId);
 060            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 61        }
 362    }
 63
 64    [HttpGet("{periodoId}/resumen-programa/csv")]
 65    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 66    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 67    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 68    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 69    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 70    public async Task<IActionResult> GetResumenProgramaCsv(long periodoId)
 71    {
 72        try
 73        {
 374            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 375            if (periodo == null)
 176                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 277            if (periodo.Estado != Periodo.EstadoCerrada)
 178                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 79
 180            var csv = await _service.GenerarCsvResumenProgramaAsync(periodoId);
 181            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteResumenPrograma, periodoId);
 182            return File(csv, "text/csv", $"resumen_programa_{periodo.Anio}_{periodo.Mes:D2}.csv");
 83        }
 084        catch (Exception ex)
 85        {
 086            _logger.LogError(ex, "Error al generar CSV resumen programa del período {PeriodoId}", periodoId);
 087            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 88        }
 389    }
 90
 91    // ─── Rubro 083 ───────────────────────────────────────────────────────────
 92
 93    [HttpGet("{periodoId}/rubro-083")]
 94    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 95    [ProducesResponseType(typeof(ApiResponse<SiifResumenDto>), StatusCodes.Status200OK)]
 96    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 97    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 98    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 99    public async Task<IActionResult> GetRubro083(long periodoId)
 100    {
 101        try
 102        {
 3103            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3104            if (periodo == null)
 1105                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2106            if (periodo.Estado != Periodo.EstadoCerrada)
 1107                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 108
 1109            var result = await _service.ObtenerResumenRubro083Async(periodoId);
 1110            return Ok(ApiResponse<SiifResumenDto>.SuccessResult(result));
 111        }
 0112        catch (Exception ex)
 113        {
 0114            _logger.LogError(ex, "Error al obtener resumen rubro 083 del período {PeriodoId}", periodoId);
 0115            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 116        }
 3117    }
 118
 119    [HttpGet("{periodoId}/rubro-083/csv")]
 120    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 121    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 122    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 123    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 124    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 125    public async Task<IActionResult> GetRubro083Csv(long periodoId)
 126    {
 127        try
 128        {
 3129            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3130            if (periodo == null)
 1131                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2132            if (periodo.Estado != Periodo.EstadoCerrada)
 1133                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 134
 1135            var csv = await _service.GenerarCsvRubro083Async(periodoId);
 1136            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteRubro083, periodoId);
 1137            return File(csv, "text/csv", $"rubro083_{periodo.Anio}_{periodo.Mes:D2}.csv");
 138        }
 0139        catch (Exception ex)
 140        {
 0141            _logger.LogError(ex, "Error al generar CSV rubro 083 del período {PeriodoId}", periodoId);
 0142            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 143        }
 3144    }
 145
 146    // ─── Padrón por rubro ────────────────────────────────────────────────────
 147
 148    [HttpGet("{periodoId}/padron-rubro")]
 149    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 150    [ProducesResponseType(typeof(ApiResponse<PadronRubroDto>), StatusCodes.Status200OK)]
 151    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 152    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 153    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 154    public async Task<IActionResult> GetPadronRubro(long periodoId)
 155    {
 156        try
 157        {
 3158            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3159            if (periodo == null)
 1160                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2161            if (periodo.Estado != Periodo.EstadoCerrada)
 1162                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 163
 1164            var result = await _service.ObtenerPadronRubroAsync(periodoId);
 1165            return Ok(ApiResponse<PadronRubroDto>.SuccessResult(result));
 166        }
 0167        catch (Exception ex)
 168        {
 0169            _logger.LogError(ex, "Error al obtener padrón rubro del período {PeriodoId}", periodoId);
 0170            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 171        }
 3172    }
 173
 174    [HttpGet("{periodoId}/padron-rubro/csv")]
 175    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 176    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 177    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 178    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 179    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 180    public async Task<IActionResult> GetPadronRubroCsv(long periodoId)
 181    {
 182        try
 183        {
 3184            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3185            if (periodo == null)
 1186                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2187            if (periodo.Estado != Periodo.EstadoCerrada)
 1188                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 189
 1190            var csv = await _service.GenerarCsvPadronRubroAsync(periodoId);
 1191            await RegistrarAuditoriaAsync(AccionEnum.GenerarReportePadronRubro, periodoId);
 1192            return File(csv, "text/csv", $"padron_rubro_{periodo.Anio}_{periodo.Mes:D2}.csv");
 193        }
 0194        catch (Exception ex)
 195        {
 0196            _logger.LogError(ex, "Error al generar CSV padrón rubro del período {PeriodoId}", periodoId);
 0197            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 198        }
 3199    }
 200
 201    // ─── Padrón mensual ──────────────────────────────────────────────────────
 202
 203    [HttpGet("{periodoId}/padron-mensual")]
 204    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 205    [ProducesResponseType(typeof(ApiResponse<List<PadronMensualFilaDto>>), StatusCodes.Status200OK)]
 206    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 207    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 208    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 209    public async Task<IActionResult> GetPadronMensual(long periodoId)
 210    {
 211        try
 212        {
 3213            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3214            if (periodo == null)
 1215                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2216            if (periodo.Estado != Periodo.EstadoCerrada)
 1217                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 218
 1219            var result = await _service.ObtenerPadronMensualAsync(periodoId);
 1220            return Ok(ApiResponse<List<PadronMensualFilaDto>>.SuccessResult(result));
 221        }
 0222        catch (Exception ex)
 223        {
 0224            _logger.LogError(ex, "Error al obtener padrón mensual del período {PeriodoId}", periodoId);
 0225            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 226        }
 3227    }
 228
 229    [HttpGet("{periodoId}/padron-mensual/csv")]
 230    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 231    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 232    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 233    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 234    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 235    public async Task<IActionResult> GetPadronMensualCsv(long periodoId)
 236    {
 237        try
 238        {
 3239            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3240            if (periodo == null)
 1241                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2242            if (periodo.Estado != Periodo.EstadoCerrada)
 1243                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 244
 1245            var csv = await _service.GenerarCsvPadronMensualAsync(periodoId);
 1246            await RegistrarAuditoriaAsync(AccionEnum.GenerarReportePadronMensual, periodoId);
 1247            return File(csv, "text/csv", $"padron_mensual_{periodo.Anio}_{periodo.Mes:D2}.csv");
 248        }
 0249        catch (Exception ex)
 250        {
 0251            _logger.LogError(ex, "Error al generar CSV padrón mensual del período {PeriodoId}", periodoId);
 0252            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 253        }
 3254    }
 255
 256    // ─── Movimientos ─────────────────────────────────────────────────────────
 257
 258    [HttpGet("{periodoId}/movimientos")]
 259    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 260    [ProducesResponseType(typeof(ApiResponse<List<MovimientoPersonalDto>>), StatusCodes.Status200OK)]
 261    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 262    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 263    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 264    public async Task<IActionResult> GetMovimientos(long periodoId)
 265    {
 266        try
 267        {
 3268            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3269            if (periodo == null)
 1270                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2271            if (periodo.Estado != Periodo.EstadoCerrada)
 1272                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 273
 1274            var result = await _service.ObtenerMovimientosAsync(periodoId);
 1275            return Ok(ApiResponse<List<MovimientoPersonalDto>>.SuccessResult(result));
 276        }
 0277        catch (Exception ex)
 278        {
 0279            _logger.LogError(ex, "Error al obtener movimientos del período {PeriodoId}", periodoId);
 0280            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 281        }
 3282    }
 283
 284    [HttpGet("{periodoId}/movimientos/csv")]
 285    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 286    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 287    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 288    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 289    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 290    public async Task<IActionResult> GetMovimientosCsv(long periodoId)
 291    {
 292        try
 293        {
 3294            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3295            if (periodo == null)
 1296                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2297            if (periodo.Estado != Periodo.EstadoCerrada)
 1298                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 299
 1300            var csv = await _service.GenerarCsvMovimientosAsync(periodoId);
 1301            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteMovimientos, periodoId);
 1302            return File(csv, "text/csv", $"movimientos_{periodo.Anio}_{periodo.Mes:D2}.csv");
 303        }
 0304        catch (Exception ex)
 305        {
 0306            _logger.LogError(ex, "Error al generar CSV movimientos del período {PeriodoId}", periodoId);
 0307            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 308        }
 3309    }
 310
 311    // ─── Diferencias ─────────────────────────────────────────────────────────
 312
 313    [HttpGet("{periodoId}/diferencias")]
 314    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 315    [ProducesResponseType(typeof(ApiResponse<PadronDiferenciasDto>), StatusCodes.Status200OK)]
 316    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 317    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 318    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 319    public async Task<IActionResult> GetDiferencias(long periodoId)
 320    {
 321        try
 322        {
 3323            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3324            if (periodo == null)
 1325                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2326            if (periodo.Estado != Periodo.EstadoCerrada)
 1327                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 328
 1329            var result = await _service.ObtenerDiferenciasAsync(periodoId);
 1330            return Ok(ApiResponse<PadronDiferenciasDto>.SuccessResult(result));
 331        }
 0332        catch (Exception ex)
 333        {
 0334            _logger.LogError(ex, "Error al obtener diferencias del período {PeriodoId}", periodoId);
 0335            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 336        }
 3337    }
 338
 339    [HttpGet("{periodoId}/diferencias/csv")]
 340    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 341    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 342    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 343    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 344    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 345    public async Task<IActionResult> GetDiferenciasCsv(long periodoId)
 346    {
 347        try
 348        {
 3349            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3350            if (periodo == null)
 1351                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2352            if (periodo.Estado != Periodo.EstadoCerrada)
 1353                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 354
 1355            var csv = await _service.GenerarCsvDiferenciasAsync(periodoId);
 1356            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteDiferencias, periodoId);
 1357            return File(csv, "text/csv", $"diferencias_{periodo.Anio}_{periodo.Mes:D2}.csv");
 358        }
 0359        catch (Exception ex)
 360        {
 0361            _logger.LogError(ex, "Error al generar CSV diferencias del período {PeriodoId}", periodoId);
 0362            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 363        }
 3364    }
 365
 366    // ─── No disponibles ──────────────────────────────────────────────────────
 367
 368    [HttpGet("{periodoId}/no-disponibles")]
 369    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 370    [ProducesResponseType(typeof(ApiResponse<List<NoDisponibleDto>>), StatusCodes.Status200OK)]
 371    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 372    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 373    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 374    public async Task<IActionResult> GetNoDisponibles(long periodoId)
 375    {
 376        try
 377        {
 3378            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3379            if (periodo == null)
 1380                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2381            if (periodo.Estado != Periodo.EstadoCerrada)
 1382                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 383
 1384            var result = await _service.ObtenerNoDisponiblesAsync(periodoId);
 1385            return Ok(ApiResponse<List<NoDisponibleDto>>.SuccessResult(result));
 386        }
 0387        catch (Exception ex)
 388        {
 0389            _logger.LogError(ex, "Error al obtener no disponibles del período {PeriodoId}", periodoId);
 0390            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 391        }
 3392    }
 393
 394    [HttpGet("{periodoId}/no-disponibles/csv")]
 395    [Authorize(Roles = "Administrador,Supervisor,Usuario")]
 396    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 397    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 398    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 399    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 400    public async Task<IActionResult> GetNoDisponiblesCsv(long periodoId)
 401    {
 402        try
 403        {
 3404            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3405            if (periodo == null)
 1406                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2407            if (periodo.Estado != Periodo.EstadoCerrada)
 1408                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 409
 1410            var csv = await _service.GenerarCsvNoDisponiblesAsync(periodoId);
 1411            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteNoDisponibles, periodoId);
 1412            return File(csv, "text/csv", $"no_disponibles_{periodo.Anio}_{periodo.Mes:D2}.csv");
 413        }
 0414        catch (Exception ex)
 415        {
 0416            _logger.LogError(ex, "Error al generar CSV no disponibles del período {PeriodoId}", periodoId);
 0417            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 418        }
 3419    }
 420
 421    // ─── Helper ──────────────────────────────────────────────────────────────
 422
 423    private Task RegistrarAuditoriaAsync(AccionEnum accion, long periodoId) =>
 7424        _auditoriaService.LogAuditoriaAsync(
 7425            _currentUser.UserId ?? 0,
 7426            accion,
 7427            ContextoEnum.Liquidaciones,
 7428            _currentUser.Host,
 7429            entidad: "ReportePresupuestal",
 7430            entidadId: periodoId,
 7431            detalle: new { periodoId });
 432}