< Summary

Information
Class: FAU.API.Controllers.CompensacionesController
Assembly: FAU.API
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.API/Controllers/CompensacionesController.cs
Line coverage
44%
Covered lines: 111
Uncovered lines: 140
Coverable lines: 251
Total lines: 686
Line coverage: 44.2%
Branch coverage
43%
Covered branches: 80
Total branches: 184
Branch coverage: 43.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetTipos()100%11100%
GetTipo()100%2262.5%
CreateTipo()87.5%9872.72%
UpdateTipo()50%12860%
DeleteTipo()87.5%10870%
GetConfigs()100%1150%
GetConfig()0%620%
CreateConfig()75%12860%
UpdateConfig()37.5%16850%
DeleteConfig()87.5%10870%
GetLotes()50%8662.5%
GetLote()50%3250%
CreateLote()62.5%10866.66%
UpdateLote()50%301250%
AprobarLote()66.66%201261.53%
RechazarLote()0%7280%
RevertirLote()0%7280%
CancelarLote()0%7280%
GetItems()0%4260%
CreateItem()87.5%221671.42%
CreateItemsBulk()0%110100%
CreateItemsBulkCsv()0%156120%
UpdateItem()50%301250%
DeleteItem()8.33%221258.33%

File(s)

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

#LineLine coverage
 1using System.Security.Claims;
 2using FAU.Logica.DTOs;
 3using FAU.Logica.Services;
 4using FAU.Entidades;
 5using Microsoft.AspNetCore.Authorization;
 6using Microsoft.AspNetCore.Mvc;
 7
 8namespace FAU.API.Controllers;
 9
 10[ApiController]
 11public class CompensacionesController : ControllerBase
 12{
 13    private readonly ICompensacionService _service;
 14    private readonly ILogger<CompensacionesController> _logger;
 15
 3416    public CompensacionesController(ICompensacionService service, ILogger<CompensacionesController> logger)
 17    {
 3418        _service = service;
 3419        _logger = logger;
 3420    }
 21
 22    // ── Tipos de Compensación ────────────────────────────────────────────────
 23
 24    /// <summary>
 25    /// Listar tipos de compensación
 26    /// GET /api/tipos-compensacion?activo=
 27    /// </summary>
 28    [HttpGet("api/tipos-compensacion")]
 29    [FAU.API.Security.RequirePermission(PermisoEnum.VerCompensaciones)]
 30    public async Task<IActionResult> GetTipos([FromQuery] bool? activo = null)
 31    {
 32        try
 33        {
 234            var tipos = await _service.GetTiposAsync(activo);
 135            return Ok(ApiResponse<object>.SuccessResult(tipos));
 36        }
 137        catch (Exception ex)
 38        {
 139            _logger.LogError(ex, "Error al obtener tipos de compensación");
 140            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 41        }
 242    }
 43
 44    /// <summary>
 45    /// Obtener tipo de compensación por ID
 46    /// GET /api/tipos-compensacion/{id}
 47    /// </summary>
 48    [HttpGet("api/tipos-compensacion/{id}")]
 49    [FAU.API.Security.RequirePermission(PermisoEnum.VerCompensaciones)]
 50    public async Task<IActionResult> GetTipo(long id)
 51    {
 52        try
 53        {
 254            var tipo = await _service.GetTipoByIdAsync(id);
 255            if (tipo == null)
 156                return NotFound(ApiResponse<string>.ErrorResult("Tipo de compensación no encontrado"));
 57
 158            return Ok(ApiResponse<object>.SuccessResult(tipo));
 59        }
 060        catch (Exception ex)
 61        {
 062            _logger.LogError(ex, "Error al obtener tipo de compensación {Id}", id);
 063            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 64        }
 265    }
 66
 67    /// <summary>
 68    /// Crear tipo de compensación
 69    /// POST /api/tipos-compensacion
 70    /// </summary>
 71    [HttpPost("api/tipos-compensacion")]
 72    [FAU.API.Security.RequirePermission(PermisoEnum.CrearCompensacion)]
 73    public async Task<IActionResult> CreateTipo([FromBody] CreateTipoCompensacionRequest request)
 74    {
 75        try
 76        {
 377            var (success, tipo, error) = await _service.CreateTipoAsync(request);
 378            if (!success)
 79            {
 280                if (error?.Contains("Ya existe", StringComparison.OrdinalIgnoreCase) == true)
 181                    return Conflict(ApiResponse<string>.ErrorResult(error));
 182                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al crear tipo"));
 83            }
 84
 185            return CreatedAtAction(nameof(GetTipo), new { id = tipo!.Id },
 186                ApiResponse<object>.SuccessResult(new { id = tipo.Id, creado = true }, "Tipo creado exitosamente"));
 87        }
 088        catch (Exception ex)
 89        {
 090            _logger.LogError(ex, "Error al crear tipo de compensación");
 091            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 92        }
 393    }
 94
 95    /// <summary>
 96    /// Actualizar tipo de compensación
 97    /// PUT /api/tipos-compensacion/{id}
 98    /// </summary>
 99    [HttpPut("api/tipos-compensacion/{id}")]
 100    [FAU.API.Security.RequirePermission(PermisoEnum.ActualizarCompensacion)]
 101    public async Task<IActionResult> UpdateTipo(long id, [FromBody] UpdateTipoCompensacionRequest request)
 102    {
 103        try
 104        {
 2105            var (success, _, error) = await _service.UpdateTipoAsync(id, request);
 2106            if (!success)
 107            {
 1108                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 1109                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0110                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al actualizar tipo"));
 111            }
 112
 1113            return Ok(ApiResponse<object>.SuccessResult(new { actualizado = true }, "Tipo actualizado exitosamente"));
 114        }
 0115        catch (Exception ex)
 116        {
 0117            _logger.LogError(ex, "Error al actualizar tipo de compensación {Id}", id);
 0118            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 119        }
 2120    }
 121
 122    /// <summary>
 123    /// Eliminar tipo de compensación
 124    /// DELETE /api/tipos-compensacion/{id}
 125    /// </summary>
 126    [HttpDelete("api/tipos-compensacion/{id}")]
 127    public async Task<IActionResult> DeleteTipo(long id)
 128    {
 129        try
 130        {
 4131            var (success, error) = await _service.DeleteTipoAsync(id);
 4132            if (!success)
 133            {
 3134                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 1135                    return NotFound(ApiResponse<string>.ErrorResult(error));
 2136                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al eliminar tipo"));
 137            }
 138
 1139            return NoContent();
 140        }
 0141        catch (Exception ex)
 142        {
 0143            _logger.LogError(ex, "Error al eliminar tipo de compensación {Id}", id);
 0144            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 145        }
 4146    }
 147
 148    // ── Configuraciones de Compensación ─────────────────────────────────────
 149
 150    /// <summary>
 151    /// Listar configuraciones de compensación
 152    /// GET /api/config-compensaciones?tipoId=&regimenId=&gradoId=&activo=
 153    /// </summary>
 154    [HttpGet("api/config-compensaciones")]
 155    [FAU.API.Security.RequirePermission(PermisoEnum.VerCompensaciones)]
 156    public async Task<IActionResult> GetConfigs(
 157        [FromQuery] long? tipoId = null,
 158        [FromQuery] long? regimenId = null,
 159        [FromQuery] long? gradoId = null,
 160        [FromQuery] bool? activo = null)
 161    {
 162        try
 163        {
 1164            var configs = await _service.GetConfigsAsync(tipoId, regimenId, gradoId, activo);
 1165            return Ok(ApiResponse<object>.SuccessResult(configs));
 166        }
 0167        catch (Exception ex)
 168        {
 0169            _logger.LogError(ex, "Error al obtener configuraciones de compensación");
 0170            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 171        }
 1172    }
 173
 174    /// <summary>
 175    /// Obtener configuración por ID (incluye tope, fórmula, unidad)
 176    /// GET /api/config-compensaciones/{id}
 177    /// </summary>
 178    [HttpGet("api/config-compensaciones/{id}")]
 179    [FAU.API.Security.RequirePermission(PermisoEnum.VerCompensaciones)]
 180    public async Task<IActionResult> GetConfig(long id)
 181    {
 182        try
 183        {
 0184            var config = await _service.GetConfigByIdAsync(id);
 0185            if (config == null)
 0186                return NotFound(ApiResponse<string>.ErrorResult("Configuración no encontrada"));
 0187            return Ok(ApiResponse<object>.SuccessResult(config));
 188        }
 0189        catch (Exception ex)
 190        {
 0191            _logger.LogError(ex, "Error al obtener configuración {Id}", id);
 0192            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 193        }
 0194    }
 195
 196    /// <summary>
 197    /// Crear configuración de compensación
 198    /// POST /api/config-compensaciones
 199    /// </summary>
 200    [HttpPost("api/config-compensaciones")]
 201    [FAU.API.Security.RequirePermission(PermisoEnum.CrearConfigCompensacion)]
 202    public async Task<IActionResult> CreateConfig([FromBody] CreateConfigCompensacionRequest request)
 203    {
 204        try
 205        {
 2206            var (success, config, error) = await _service.CreateConfigAsync(request);
 2207            if (!success)
 208            {
 1209                if (error?.Contains("No se encontró", StringComparison.OrdinalIgnoreCase) == true)
 0210                    return BadRequest(ApiResponse<string>.ErrorResult(error));
 1211                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al crear configuración"));
 212            }
 213
 1214            return StatusCode(201, ApiResponse<object>.SuccessResult(new { id = config!.Id, creado = true }, "Configurac
 215        }
 0216        catch (Exception ex)
 217        {
 0218            _logger.LogError(ex, "Error al crear configuración de compensación");
 0219            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 220        }
 2221    }
 222
 223    /// <summary>
 224    /// Actualizar configuración de compensación
 225    /// PUT /api/config-compensaciones/{id}
 226    /// </summary>
 227    [HttpPut("api/config-compensaciones/{id}")]
 228    [FAU.API.Security.RequirePermission(PermisoEnum.ActualizarConfigComprensacion)]
 229    public async Task<IActionResult> UpdateConfig(long id, [FromBody] UpdateConfigCompensacionRequest request)
 230    {
 231        try
 232        {
 1233            var (success, _, error) = await _service.UpdateConfigAsync(id, request);
 1234            if (!success)
 235            {
 1236                if (error?.Contains("no encontrada", StringComparison.OrdinalIgnoreCase) == true)
 1237                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0238                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al actualizar configuración"));
 239            }
 240
 0241            return Ok(ApiResponse<object>.SuccessResult(new { actualizado = true }, "Configuración actualizada exitosame
 242        }
 0243        catch (Exception ex)
 244        {
 0245            _logger.LogError(ex, "Error al actualizar configuración {Id}", id);
 0246            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 247        }
 1248    }
 249
 250    /// <summary>
 251    /// Eliminar configuración de compensación
 252    /// DELETE /api/config-compensaciones/{id}
 253    /// </summary>
 254    [HttpDelete("api/config-compensaciones/{id}")]
 255    public async Task<IActionResult> DeleteConfig(long id)
 256    {
 257        try
 258        {
 3259            var (success, error) = await _service.DeleteConfigAsync(id);
 3260            if (!success)
 261            {
 2262                if (error?.Contains("no encontrada", StringComparison.OrdinalIgnoreCase) == true)
 1263                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1264                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al eliminar configuración"));
 265            }
 266
 1267            return NoContent();
 268        }
 0269        catch (Exception ex)
 270        {
 0271            _logger.LogError(ex, "Error al eliminar configuración {Id}", id);
 0272            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 273        }
 3274    }
 275
 276    // ── Lotes de Compensación ────────────────────────────────────────────────
 277
 278    /// <summary>
 279    /// Listar lotes de compensación con paginación
 280    /// GET /api/lotes-compensacion?tipoId=&anio=&mes=&estado=&page=&pageSize=
 281    /// </summary>
 282    [HttpGet("api/lotes-compensacion")]
 283    [FAU.API.Security.RequirePermission(PermisoEnum.VerCompensaciones)]
 284    public async Task<IActionResult> GetLotes(
 285        [FromQuery] long? tipoId = null,
 286        [FromQuery] int? anio = null,
 287        [FromQuery] int? mes = null,
 288        [FromQuery] string? estado = null,
 289        [FromQuery] int page = 1,
 290        [FromQuery] int pageSize = 20)
 291    {
 292        try
 293        {
 1294            if (page < 1) page = 1;
 1295            if (pageSize < 1 || pageSize > 100) pageSize = 20;
 296
 1297            var resultado = await _service.GetLotesPagedAsync(page, pageSize, tipoId, anio, mes, estado);
 1298            return Ok(ApiResponse<object>.SuccessResult(resultado));
 299        }
 0300        catch (Exception ex)
 301        {
 0302            _logger.LogError(ex, "Error al obtener lotes de compensación");
 0303            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 304        }
 1305    }
 306
 307    /// <summary>
 308    /// Obtener lote con sus ítems
 309    /// GET /api/lotes-compensacion/{id}
 310    /// </summary>
 311    [HttpGet("api/lotes-compensacion/{id}")]
 312    [FAU.API.Security.RequirePermission(PermisoEnum.VerCompensaciones)]
 313    public async Task<IActionResult> GetLote(long id)
 314    {
 315        try
 316        {
 1317            var lote = await _service.GetLoteByIdAsync(id);
 1318            if (lote == null)
 1319                return NotFound(ApiResponse<string>.ErrorResult("Lote no encontrado"));
 320
 0321            return Ok(ApiResponse<object>.SuccessResult(lote));
 322        }
 0323        catch (Exception ex)
 324        {
 0325            _logger.LogError(ex, "Error al obtener lote {Id}", id);
 0326            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 327        }
 1328    }
 329
 330    /// <summary>
 331    /// Crear lote de compensación
 332    /// POST /api/lotes-compensacion
 333    /// </summary>
 334    [HttpPost("api/lotes-compensacion")]
 335    [FAU.API.Security.RequirePermission(PermisoEnum.CrearLoteCompensacion)]
 336    public async Task<IActionResult> CreateLote([FromBody] CreateLoteCompensacionRequest request)
 337    {
 338        try
 339        {
 2340            var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
 2341            if (!long.TryParse(userIdStr, out var usuarioId))
 0342                return Unauthorized(ApiResponse<string>.ErrorResult("No se pudo identificar al usuario"));
 343
 2344            var (success, lote, error) = await _service.CreateLoteAsync(usuarioId, request);
 2345            if (!success)
 1346                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al crear lote"));
 347
 1348            return CreatedAtAction(nameof(GetLote), new { id = lote!.Id },
 1349                ApiResponse<object>.SuccessResult(new { id = lote.Id, creado = true }, "Lote creado exitosamente"));
 350        }
 0351        catch (Exception ex)
 352        {
 0353            _logger.LogError(ex, "Error al crear lote de compensación");
 0354            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 355        }
 2356    }
 357
 358    /// <summary>
 359    /// Actualizar lote (solo en estado borrador)
 360    /// PUT /api/lotes-compensacion/{id}
 361    /// </summary>
 362    [HttpPut("api/lotes-compensacion/{id}")]
 363    [FAU.API.Security.RequirePermission(PermisoEnum.ActualizarLoteCompensacion)]
 364    public async Task<IActionResult> UpdateLote(long id, [FromBody] UpdateLoteCompensacionRequest request)
 365    {
 366        try
 367        {
 1368            var (success, _, error) = await _service.UpdateLoteAsync(id, request);
 1369            if (!success)
 370            {
 1371                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0372                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1373                if (error?.Contains("estado", StringComparison.OrdinalIgnoreCase) == true)
 1374                    return Conflict(ApiResponse<string>.ErrorResult(error));
 0375                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al actualizar lote"));
 376            }
 377
 0378            return Ok(ApiResponse<object>.SuccessResult(new { actualizado = true }, "Lote actualizado exitosamente"));
 379        }
 0380        catch (Exception ex)
 381        {
 0382            _logger.LogError(ex, "Error al actualizar lote {Id}", id);
 0383            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 384        }
 1385    }
 386
 387    /// <summary>
 388    /// Aprobar lote (debe estar en estado pendiente)
 389    /// PUT /api/lotes-compensacion/{id}/aprobar
 390    /// </summary>
 391    [HttpPut("api/lotes-compensacion/{id}/aprobar")]
 392    [FAU.API.Security.RequirePermission(PermisoEnum.AprobarLoteCompensacion)]
 393    public async Task<IActionResult> AprobarLote(long id)
 394    {
 395        try
 396        {
 2397            var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
 2398            if (!long.TryParse(userIdStr, out var aprobadoPorId))
 0399                return Unauthorized(ApiResponse<string>.ErrorResult("No se pudo identificar al usuario"));
 400
 2401            var (success, _, error) = await _service.AprobarLoteAsync(id, aprobadoPorId);
 2402            if (!success)
 403            {
 1404                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0405                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1406                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al aprobar lote"));
 407            }
 408
 1409            return Ok(ApiResponse<object>.SuccessResult(new { aprobado = true }, "Lote aprobado exitosamente"));
 410        }
 0411        catch (Exception ex)
 412        {
 0413            _logger.LogError(ex, "Error al aprobar lote {Id}", id);
 0414            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 415        }
 2416    }
 417
 418    /// <summary>
 419    /// Rechazar lote (desde cualquier estado editable)
 420    /// PUT /api/lotes-compensacion/{id}/rechazar
 421    /// </summary>
 422    [HttpPut("api/lotes-compensacion/{id}/rechazar")]
 423    [FAU.API.Security.RequirePermission(PermisoEnum.RechazarLoteCompensacion)]
 424    public async Task<IActionResult> RechazarLote(long id)
 425    {
 426        try
 427        {
 0428            var (success, _, error) = await _service.RechazarLoteAsync(id);
 0429            if (!success)
 430            {
 0431                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0432                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0433                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al rechazar lote"));
 434            }
 0435            return Ok(ApiResponse<object>.SuccessResult(new { rechazado = true }, "Lote rechazado"));
 436        }
 0437        catch (Exception ex)
 438        {
 0439            _logger.LogError(ex, "Error al rechazar lote {Id}", id);
 0440            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 441        }
 0442    }
 443
 444    /// <summary>
 445    /// Revertir lote aprobado a borrador (solo admin, antes de exportar)
 446    /// PUT /api/lotes-compensacion/{id}/revertir
 447    /// </summary>
 448    [HttpPut("api/lotes-compensacion/{id}/revertir")]
 449    [FAU.API.Security.RequirePermission(PermisoEnum.RevertirLoteCompensacion)]
 450    public async Task<IActionResult> RevertirLote(long id)
 451    {
 452        try
 453        {
 0454            var (success, _, error) = await _service.RevertirABorradorAsync(id);
 0455            if (!success)
 456            {
 0457                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0458                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0459                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al revertir lote"));
 460            }
 0461            return Ok(ApiResponse<object>.SuccessResult(new { revertido = true }, "Lote revertido a borrador"));
 462        }
 0463        catch (Exception ex)
 464        {
 0465            _logger.LogError(ex, "Error al revertir lote {Id}", id);
 0466            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 467        }
 0468    }
 469
 470    /// <summary>
 471    /// Cancelar y eliminar un lote en borrador
 472    /// DELETE /api/lotes-compensacion/{id}
 473    /// </summary>
 474    [HttpDelete("api/lotes-compensacion/{id}")]
 475    [FAU.API.Security.RequirePermission(PermisoEnum.EliminarLoteCompensacion)]
 476    public async Task<IActionResult> CancelarLote(long id)
 477    {
 478        try
 479        {
 0480            var (success, error) = await _service.CancelarLoteAsync(id);
 0481            if (!success)
 482            {
 0483                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0484                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0485                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al cancelar lote"));
 486            }
 0487            return Ok(ApiResponse<object>.SuccessResult(new { cancelado = true }, "Lote cancelado y eliminado"));
 488        }
 0489        catch (Exception ex)
 490        {
 0491            _logger.LogError(ex, "Error al cancelar lote {Id}", id);
 0492            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 493        }
 0494    }
 495
 496    // ── Ítems de Lote ────────────────────────────────────────────────────────
 497
 498    /// <summary>
 499    /// Listar ítems de un lote con paginación
 500    /// GET /api/lotes-compensacion/{id}/items?page=1&pageSize=50
 501    /// </summary>
 502    [HttpGet("api/lotes-compensacion/{id}/items")]
 503    [FAU.API.Security.RequirePermission(PermisoEnum.VerCompensaciones)]
 504    public async Task<IActionResult> GetItems(long id,
 505        [FromQuery] int page = 1,
 506        [FromQuery] int pageSize = 50)
 507    {
 508        try
 509        {
 0510            if (page < 1) page = 1;
 0511            if (pageSize < 1 || pageSize > 200) pageSize = 50;
 0512            var resultado = await _service.GetItemsPagedAsync(id, page, pageSize);
 0513            return Ok(ApiResponse<object>.SuccessResult(resultado));
 514        }
 0515        catch (Exception ex)
 516        {
 0517            _logger.LogError(ex, "Error al obtener ítems del lote {Id}", id);
 0518            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 519        }
 0520    }
 521
 522    /// <summary>
 523    /// Agregar ítem a lote (debe estar en estado borrador)
 524    /// POST /api/lotes-compensacion/{id}/items
 525    /// </summary>
 526    [HttpPost("api/lotes-compensacion/{id}/items")]
 527    [FAU.API.Security.RequirePermission(PermisoEnum.CrearItemLoteCompensacion)]
 528    public async Task<IActionResult> CreateItem(long id, [FromBody] CreateItemLoteRequest request)
 529    {
 530        try
 531        {
 4532            var (success, item, error) = await _service.CreateItemAsync(id, request);
 4533            if (!success)
 534            {
 3535                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0536                    return NotFound(ApiResponse<string>.ErrorResult(error));
 3537                if (error?.Contains("Ya existe", StringComparison.OrdinalIgnoreCase) == true)
 1538                    return Conflict(ApiResponse<string>.ErrorResult(error));
 2539                if (error?.Contains("estado", StringComparison.OrdinalIgnoreCase) == true)
 1540                    return Conflict(ApiResponse<string>.ErrorResult(error));
 1541                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al agregar ítem"));
 542            }
 543
 1544            return StatusCode(201, ApiResponse<object>.SuccessResult(new { id = item!.Id, creado = true }, "Ítem agregad
 545        }
 0546        catch (Exception ex)
 547        {
 0548            _logger.LogError(ex, "Error al agregar ítem al lote {LoteId}", id);
 0549            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 550        }
 4551    }
 552
 553    /// <summary>
 554    /// Carga masiva de ítems vía JSON array
 555    /// POST /api/lotes-compensacion/{id}/items/bulk?modo=reemplazar|agregar&amp;dryRun=false
 556    /// modo=reemplazar (default): elimina todos los ítems existentes antes de insertar (comportamiento legacy)
 557    /// modo=agregar: agrega sin borrar los existentes (para correcciones parciales)
 558    /// dryRun=true: valida sin persistir — devuelve el mismo resultado pero sin guardar nada
 559    /// </summary>
 560    [HttpPost("api/lotes-compensacion/{id}/items/bulk")]
 561    [FAU.API.Security.RequirePermission(PermisoEnum.CrearItemLoteCompensacion)]
 562    public async Task<IActionResult> CreateItemsBulk(
 563        long id,
 564        [FromBody] List<CreateItemLoteRequest> items,
 565        [FromQuery] string modo = "reemplazar",
 566        [FromQuery] bool dryRun = false)
 567    {
 568        try
 569        {
 0570            if (items == null || items.Count == 0)
 0571                return BadRequest(ApiResponse<string>.ErrorResult("La lista de ítems no puede estar vacía"));
 572
 0573            if (modo != "reemplazar" && modo != "agregar")
 0574                return BadRequest(ApiResponse<string>.ErrorResult("modo debe ser 'reemplazar' o 'agregar'"));
 575
 0576            var resultado = await _service.CreateItemsBulkAsync(id, items, modo, dryRun);
 0577            var msg = dryRun
 0578                ? $"Dry run: {resultado.Exitosos} válidos, {resultado.Fallidos} con errores (sin cambios)"
 0579                : $"Bulk {modo}: {resultado.Exitosos} exitosos, {resultado.Fallidos} fallidos";
 0580            return Ok(ApiResponse<object>.SuccessResult(resultado, msg));
 581        }
 0582        catch (Exception ex)
 583        {
 0584            _logger.LogError(ex, "Error en bulk import lote {LoteId}", id);
 0585            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 586        }
 0587    }
 588
 589    /// <summary>
 590    /// Carga masiva de ítems vía archivo CSV (multipart/form-data)
 591    /// POST /api/lotes-compensacion/{id}/items/bulk-csv?modo=reemplazar|agregar&amp;dryRun=false
 592    /// Formato CSV: cedula,monto,unidad_cantidad,funcion,incidencia,observaciones
 593    /// SAR/PAN requieren columna 'funcion' con valor CA o SA
 594    /// </summary>
 595    [HttpPost("api/lotes-compensacion/{id}/items/bulk-csv")]
 596    [FAU.API.Security.RequirePermission(PermisoEnum.CrearItemLoteCompensacion)]
 597    public async Task<IActionResult> CreateItemsBulkCsv(
 598        long id,
 599        IFormFile archivo,
 600        [FromQuery] string modo = "reemplazar",
 601        [FromQuery] bool dryRun = false)
 602    {
 603        try
 604        {
 0605            if (archivo == null || archivo.Length == 0)
 0606                return BadRequest(ApiResponse<string>.ErrorResult("Debe adjuntar un archivo CSV"));
 607
 0608            if (!archivo.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
 0609                return BadRequest(ApiResponse<string>.ErrorResult("El archivo debe tener extensión .csv"));
 610
 0611            if (modo != "reemplazar" && modo != "agregar")
 0612                return BadRequest(ApiResponse<string>.ErrorResult("modo debe ser 'reemplazar' o 'agregar'"));
 613
 0614            using var stream = archivo.OpenReadStream();
 0615            var resultado = await _service.CreateItemsFromCsvAsync(id, stream, modo, dryRun);
 616
 0617            var msg = dryRun
 0618                ? $"Dry run: {resultado.Exitosos} válidos, {resultado.Fallidos} con errores (sin cambios)"
 0619                : $"CSV {modo}: {resultado.Exitosos} exitosos, {resultado.Fallidos} fallidos";
 0620            return Ok(ApiResponse<object>.SuccessResult(resultado, msg));
 621        }
 0622        catch (Exception ex)
 623        {
 0624            _logger.LogError(ex, "Error en bulk CSV lote {LoteId}", id);
 0625            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 626        }
 0627    }
 628
 629    /// <summary>
 630    /// Actualizar ítem de lote (debe estar en estado borrador)
 631    /// PUT /api/lotes-compensacion/{id}/items/{itemId}
 632    /// </summary>
 633    [HttpPut("api/lotes-compensacion/{id}/items/{itemId}")]
 634    [FAU.API.Security.RequirePermission(PermisoEnum.ActualizarItemLoteCompensacion)]
 635    public async Task<IActionResult> UpdateItem(long id, long itemId, [FromBody] UpdateItemLoteRequest request)
 636    {
 637        try
 638        {
 1639            var (success, _, error) = await _service.UpdateItemAsync(id, itemId, request);
 1640            if (!success)
 641            {
 1642                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0643                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1644                if (error?.Contains("estado", StringComparison.OrdinalIgnoreCase) == true)
 1645                    return Conflict(ApiResponse<string>.ErrorResult(error));
 0646                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al actualizar ítem"));
 647            }
 648
 0649            return Ok(ApiResponse<object>.SuccessResult(new { actualizado = true }, "Ítem actualizado exitosamente"));
 650        }
 0651        catch (Exception ex)
 652        {
 0653            _logger.LogError(ex, "Error al actualizar ítem {ItemId} del lote {LoteId}", itemId, id);
 0654            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 655        }
 1656    }
 657
 658    /// <summary>
 659    /// Eliminar ítem de lote (debe estar en estado borrador)
 660    /// DELETE /api/lotes-compensacion/{id}/items/{itemId}
 661    /// </summary>
 662    [HttpDelete("api/lotes-compensacion/{id}/items/{itemId}")]
 663    [FAU.API.Security.RequirePermission(PermisoEnum.EliminarItemLoteCompensacion)]
 664    public async Task<IActionResult> DeleteItem(long id, long itemId)
 665    {
 666        try
 667        {
 2668            var (success, error) = await _service.DeleteItemAsync(id, itemId);
 1669            if (!success)
 670            {
 0671                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0672                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0673                if (error?.Contains("estado", StringComparison.OrdinalIgnoreCase) == true)
 0674                    return Conflict(ApiResponse<string>.ErrorResult(error));
 0675                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al eliminar ítem"));
 676            }
 677
 1678            return Ok(ApiResponse<object>.SuccessResult(new { eliminado = true }, "Ítem eliminado exitosamente"));
 679        }
 1680        catch (Exception ex)
 681        {
 1682            _logger.LogError(ex, "Error al eliminar ítem {ItemId} del lote {LoteId}", itemId, id);
 1683            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 684        }
 2685    }
 686}