< 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: 433
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.API.Security;
 2using FAU.Entidades;
 3using FAU.Logica.DTOs;
 4using FAU.Logica.Services;
 5using Microsoft.AspNetCore.Authorization;
 6using Microsoft.AspNetCore.Http;
 7using Microsoft.AspNetCore.Mvc;
 8
 9namespace FAU.API.Controllers;
 10
 11[ApiController]
 12[Route("api/liquidacion/reportes")]
 13[Authorize]
 14[Produces("application/json")]
 15public class ReportePresupuestalController : ControllerBase
 16{
 17    private readonly IReportePresupuestalService _service;
 18    private readonly IPeriodoService _periodoService;
 19    private readonly IAuditoriaService _auditoriaService;
 20    private readonly ICurrentUserContext _currentUser;
 21    private readonly ILogger<ReportePresupuestalController> _logger;
 22
 4223    public ReportePresupuestalController(
 4224        IReportePresupuestalService service,
 4225        IPeriodoService periodoService,
 4226        IAuditoriaService auditoriaService,
 4227        ICurrentUserContext currentUser,
 4228        ILogger<ReportePresupuestalController> logger)
 29    {
 4230        _service          = service;
 4231        _periodoService   = periodoService;
 4232        _auditoriaService = auditoriaService;
 4233        _currentUser      = currentUser;
 4234        _logger           = logger;
 4235    }
 36
 37    // ─── Resumen por programa ────────────────────────────────────────────────
 38
 39    [HttpGet("{periodoId}/resumen-programa")]
 40    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 41    [ProducesResponseType(typeof(ApiResponse<ResumenProgramaDto>), StatusCodes.Status200OK)]
 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> GetResumenPrograma(long periodoId)
 46    {
 47        try
 48        {
 349            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 350            if (periodo == null)
 151                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 252            if (periodo.Estado != Periodo.EstadoCerrada)
 153                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 54
 155            var result = await _service.ObtenerResumenProgramaAsync(periodoId);
 156            return Ok(ApiResponse<ResumenProgramaDto>.SuccessResult(result));
 57        }
 058        catch (Exception ex)
 59        {
 060            _logger.LogError(ex, "Error al obtener resumen programa del período {PeriodoId}", periodoId);
 061            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 62        }
 363    }
 64
 65    [HttpGet("{periodoId}/resumen-programa/csv")]
 66    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 67    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 68    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 69    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 70    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 71    public async Task<IActionResult> GetResumenProgramaCsv(long periodoId)
 72    {
 73        try
 74        {
 375            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 376            if (periodo == null)
 177                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 278            if (periodo.Estado != Periodo.EstadoCerrada)
 179                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 80
 181            var csv = await _service.GenerarCsvResumenProgramaAsync(periodoId);
 182            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteResumenPrograma, periodoId);
 183            return File(csv, "text/csv", $"resumen_programa_{periodo.Anio}_{periodo.Mes:D2}.csv");
 84        }
 085        catch (Exception ex)
 86        {
 087            _logger.LogError(ex, "Error al generar CSV resumen programa del período {PeriodoId}", periodoId);
 088            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 89        }
 390    }
 91
 92    // ─── Rubro 083 ───────────────────────────────────────────────────────────
 93
 94    [HttpGet("{periodoId}/rubro-083")]
 95    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 96    [ProducesResponseType(typeof(ApiResponse<SiifResumenDto>), StatusCodes.Status200OK)]
 97    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 98    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 99    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 100    public async Task<IActionResult> GetRubro083(long periodoId)
 101    {
 102        try
 103        {
 3104            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3105            if (periodo == null)
 1106                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2107            if (periodo.Estado != Periodo.EstadoCerrada)
 1108                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 109
 1110            var result = await _service.ObtenerResumenRubro083Async(periodoId);
 1111            return Ok(ApiResponse<SiifResumenDto>.SuccessResult(result));
 112        }
 0113        catch (Exception ex)
 114        {
 0115            _logger.LogError(ex, "Error al obtener resumen rubro 083 del período {PeriodoId}", periodoId);
 0116            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 117        }
 3118    }
 119
 120    [HttpGet("{periodoId}/rubro-083/csv")]
 121    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 122    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 123    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 124    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 125    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 126    public async Task<IActionResult> GetRubro083Csv(long periodoId)
 127    {
 128        try
 129        {
 3130            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3131            if (periodo == null)
 1132                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2133            if (periodo.Estado != Periodo.EstadoCerrada)
 1134                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 135
 1136            var csv = await _service.GenerarCsvRubro083Async(periodoId);
 1137            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteRubro083, periodoId);
 1138            return File(csv, "text/csv", $"rubro083_{periodo.Anio}_{periodo.Mes:D2}.csv");
 139        }
 0140        catch (Exception ex)
 141        {
 0142            _logger.LogError(ex, "Error al generar CSV rubro 083 del período {PeriodoId}", periodoId);
 0143            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 144        }
 3145    }
 146
 147    // ─── Padrón por rubro ────────────────────────────────────────────────────
 148
 149    [HttpGet("{periodoId}/padron-rubro")]
 150    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 151    [ProducesResponseType(typeof(ApiResponse<PadronRubroDto>), StatusCodes.Status200OK)]
 152    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 153    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 154    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 155    public async Task<IActionResult> GetPadronRubro(long periodoId)
 156    {
 157        try
 158        {
 3159            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3160            if (periodo == null)
 1161                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2162            if (periodo.Estado != Periodo.EstadoCerrada)
 1163                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 164
 1165            var result = await _service.ObtenerPadronRubroAsync(periodoId);
 1166            return Ok(ApiResponse<PadronRubroDto>.SuccessResult(result));
 167        }
 0168        catch (Exception ex)
 169        {
 0170            _logger.LogError(ex, "Error al obtener padrón rubro del período {PeriodoId}", periodoId);
 0171            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 172        }
 3173    }
 174
 175    [HttpGet("{periodoId}/padron-rubro/csv")]
 176    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 177    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 178    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 179    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 180    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 181    public async Task<IActionResult> GetPadronRubroCsv(long periodoId)
 182    {
 183        try
 184        {
 3185            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3186            if (periodo == null)
 1187                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2188            if (periodo.Estado != Periodo.EstadoCerrada)
 1189                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 190
 1191            var csv = await _service.GenerarCsvPadronRubroAsync(periodoId);
 1192            await RegistrarAuditoriaAsync(AccionEnum.GenerarReportePadronRubro, periodoId);
 1193            return File(csv, "text/csv", $"padron_rubro_{periodo.Anio}_{periodo.Mes:D2}.csv");
 194        }
 0195        catch (Exception ex)
 196        {
 0197            _logger.LogError(ex, "Error al generar CSV padrón rubro del período {PeriodoId}", periodoId);
 0198            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 199        }
 3200    }
 201
 202    // ─── Padrón mensual ──────────────────────────────────────────────────────
 203
 204    [HttpGet("{periodoId}/padron-mensual")]
 205    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 206    [ProducesResponseType(typeof(ApiResponse<List<PadronMensualFilaDto>>), StatusCodes.Status200OK)]
 207    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 208    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 209    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 210    public async Task<IActionResult> GetPadronMensual(long periodoId)
 211    {
 212        try
 213        {
 3214            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3215            if (periodo == null)
 1216                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2217            if (periodo.Estado != Periodo.EstadoCerrada)
 1218                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 219
 1220            var result = await _service.ObtenerPadronMensualAsync(periodoId);
 1221            return Ok(ApiResponse<List<PadronMensualFilaDto>>.SuccessResult(result));
 222        }
 0223        catch (Exception ex)
 224        {
 0225            _logger.LogError(ex, "Error al obtener padrón mensual del período {PeriodoId}", periodoId);
 0226            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 227        }
 3228    }
 229
 230    [HttpGet("{periodoId}/padron-mensual/csv")]
 231    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 232    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 233    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 234    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 235    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 236    public async Task<IActionResult> GetPadronMensualCsv(long periodoId)
 237    {
 238        try
 239        {
 3240            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3241            if (periodo == null)
 1242                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2243            if (periodo.Estado != Periodo.EstadoCerrada)
 1244                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 245
 1246            var csv = await _service.GenerarCsvPadronMensualAsync(periodoId);
 1247            await RegistrarAuditoriaAsync(AccionEnum.GenerarReportePadronMensual, periodoId);
 1248            return File(csv, "text/csv", $"padron_mensual_{periodo.Anio}_{periodo.Mes:D2}.csv");
 249        }
 0250        catch (Exception ex)
 251        {
 0252            _logger.LogError(ex, "Error al generar CSV padrón mensual del período {PeriodoId}", periodoId);
 0253            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 254        }
 3255    }
 256
 257    // ─── Movimientos ─────────────────────────────────────────────────────────
 258
 259    [HttpGet("{periodoId}/movimientos")]
 260    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 261    [ProducesResponseType(typeof(ApiResponse<List<MovimientoPersonalDto>>), StatusCodes.Status200OK)]
 262    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 263    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 264    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 265    public async Task<IActionResult> GetMovimientos(long periodoId)
 266    {
 267        try
 268        {
 3269            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3270            if (periodo == null)
 1271                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2272            if (periodo.Estado != Periodo.EstadoCerrada)
 1273                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 274
 1275            var result = await _service.ObtenerMovimientosAsync(periodoId);
 1276            return Ok(ApiResponse<List<MovimientoPersonalDto>>.SuccessResult(result));
 277        }
 0278        catch (Exception ex)
 279        {
 0280            _logger.LogError(ex, "Error al obtener movimientos del período {PeriodoId}", periodoId);
 0281            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 282        }
 3283    }
 284
 285    [HttpGet("{periodoId}/movimientos/csv")]
 286    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 287    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 288    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 289    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 290    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 291    public async Task<IActionResult> GetMovimientosCsv(long periodoId)
 292    {
 293        try
 294        {
 3295            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3296            if (periodo == null)
 1297                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2298            if (periodo.Estado != Periodo.EstadoCerrada)
 1299                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 300
 1301            var csv = await _service.GenerarCsvMovimientosAsync(periodoId);
 1302            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteMovimientos, periodoId);
 1303            return File(csv, "text/csv", $"movimientos_{periodo.Anio}_{periodo.Mes:D2}.csv");
 304        }
 0305        catch (Exception ex)
 306        {
 0307            _logger.LogError(ex, "Error al generar CSV movimientos del período {PeriodoId}", periodoId);
 0308            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 309        }
 3310    }
 311
 312    // ─── Diferencias ─────────────────────────────────────────────────────────
 313
 314    [HttpGet("{periodoId}/diferencias")]
 315    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 316    [ProducesResponseType(typeof(ApiResponse<PadronDiferenciasDto>), StatusCodes.Status200OK)]
 317    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 318    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 319    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 320    public async Task<IActionResult> GetDiferencias(long periodoId)
 321    {
 322        try
 323        {
 3324            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3325            if (periodo == null)
 1326                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2327            if (periodo.Estado != Periodo.EstadoCerrada)
 1328                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 329
 1330            var result = await _service.ObtenerDiferenciasAsync(periodoId);
 1331            return Ok(ApiResponse<PadronDiferenciasDto>.SuccessResult(result));
 332        }
 0333        catch (Exception ex)
 334        {
 0335            _logger.LogError(ex, "Error al obtener diferencias del período {PeriodoId}", periodoId);
 0336            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 337        }
 3338    }
 339
 340    [HttpGet("{periodoId}/diferencias/csv")]
 341    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 342    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 343    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 344    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 345    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 346    public async Task<IActionResult> GetDiferenciasCsv(long periodoId)
 347    {
 348        try
 349        {
 3350            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3351            if (periodo == null)
 1352                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2353            if (periodo.Estado != Periodo.EstadoCerrada)
 1354                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 355
 1356            var csv = await _service.GenerarCsvDiferenciasAsync(periodoId);
 1357            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteDiferencias, periodoId);
 1358            return File(csv, "text/csv", $"diferencias_{periodo.Anio}_{periodo.Mes:D2}.csv");
 359        }
 0360        catch (Exception ex)
 361        {
 0362            _logger.LogError(ex, "Error al generar CSV diferencias del período {PeriodoId}", periodoId);
 0363            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 364        }
 3365    }
 366
 367    // ─── No disponibles ──────────────────────────────────────────────────────
 368
 369    [HttpGet("{periodoId}/no-disponibles")]
 370    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 371    [ProducesResponseType(typeof(ApiResponse<List<NoDisponibleDto>>), StatusCodes.Status200OK)]
 372    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 373    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 374    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 375    public async Task<IActionResult> GetNoDisponibles(long periodoId)
 376    {
 377        try
 378        {
 3379            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3380            if (periodo == null)
 1381                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2382            if (periodo.Estado != Periodo.EstadoCerrada)
 1383                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 384
 1385            var result = await _service.ObtenerNoDisponiblesAsync(periodoId);
 1386            return Ok(ApiResponse<List<NoDisponibleDto>>.SuccessResult(result));
 387        }
 0388        catch (Exception ex)
 389        {
 0390            _logger.LogError(ex, "Error al obtener no disponibles del período {PeriodoId}", periodoId);
 0391            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 392        }
 3393    }
 394
 395    [HttpGet("{periodoId}/no-disponibles/csv")]
 396    [RequirePermission(PermisoEnum.VerReportesPresupuestales)]
 397    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
 398    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status404NotFound)]
 399    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status409Conflict)]
 400    [ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status500InternalServerError)]
 401    public async Task<IActionResult> GetNoDisponiblesCsv(long periodoId)
 402    {
 403        try
 404        {
 3405            var periodo = await _periodoService.ObtenerPorIdAsync(periodoId);
 3406            if (periodo == null)
 1407                return NotFound(ApiResponse<string>.ErrorResult("Período no encontrado"));
 2408            if (periodo.Estado != Periodo.EstadoCerrada)
 1409                return Conflict(ApiResponse<string>.ErrorResult("El período no está cerrado"));
 410
 1411            var csv = await _service.GenerarCsvNoDisponiblesAsync(periodoId);
 1412            await RegistrarAuditoriaAsync(AccionEnum.GenerarReporteNoDisponibles, periodoId);
 1413            return File(csv, "text/csv", $"no_disponibles_{periodo.Anio}_{periodo.Mes:D2}.csv");
 414        }
 0415        catch (Exception ex)
 416        {
 0417            _logger.LogError(ex, "Error al generar CSV no disponibles del período {PeriodoId}", periodoId);
 0418            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 419        }
 3420    }
 421
 422    // ─── Helper ──────────────────────────────────────────────────────────────
 423
 424    private Task RegistrarAuditoriaAsync(AccionEnum accion, long periodoId) =>
 7425        _auditoriaService.LogAuditoriaAsync(
 7426            _currentUser.UserId ?? 0,
 7427            accion,
 7428            ContextoEnum.Liquidaciones,
 7429            _currentUser.Host,
 7430            entidad: "ReportePresupuestal",
 7431            entidadId: periodoId,
 7432            detalle: new { periodoId });
 433}