< 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: 664
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 Microsoft.AspNetCore.Authorization;
 5using Microsoft.AspNetCore.Mvc;
 6
 7namespace FAU.API.Controllers;
 8
 9[ApiController]
 10[Authorize(Roles = "Administrador")]
 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    public async Task<IActionResult> GetTipos([FromQuery] bool? activo = null)
 30    {
 31        try
 32        {
 233            var tipos = await _service.GetTiposAsync(activo);
 134            return Ok(ApiResponse<object>.SuccessResult(tipos));
 35        }
 136        catch (Exception ex)
 37        {
 138            _logger.LogError(ex, "Error al obtener tipos de compensación");
 139            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 40        }
 241    }
 42
 43    /// <summary>
 44    /// Obtener tipo de compensación por ID
 45    /// GET /api/tipos-compensacion/{id}
 46    /// </summary>
 47    [HttpGet("api/tipos-compensacion/{id}")]
 48    public async Task<IActionResult> GetTipo(long id)
 49    {
 50        try
 51        {
 252            var tipo = await _service.GetTipoByIdAsync(id);
 253            if (tipo == null)
 154                return NotFound(ApiResponse<string>.ErrorResult("Tipo de compensación no encontrado"));
 55
 156            return Ok(ApiResponse<object>.SuccessResult(tipo));
 57        }
 058        catch (Exception ex)
 59        {
 060            _logger.LogError(ex, "Error al obtener tipo de compensación {Id}", id);
 061            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 62        }
 263    }
 64
 65    /// <summary>
 66    /// Crear tipo de compensación
 67    /// POST /api/tipos-compensacion
 68    /// </summary>
 69    [HttpPost("api/tipos-compensacion")]
 70    public async Task<IActionResult> CreateTipo([FromBody] CreateTipoCompensacionRequest request)
 71    {
 72        try
 73        {
 374            var (success, tipo, error) = await _service.CreateTipoAsync(request);
 375            if (!success)
 76            {
 277                if (error?.Contains("Ya existe", StringComparison.OrdinalIgnoreCase) == true)
 178                    return Conflict(ApiResponse<string>.ErrorResult(error));
 179                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al crear tipo"));
 80            }
 81
 182            return CreatedAtAction(nameof(GetTipo), new { id = tipo!.Id },
 183                ApiResponse<object>.SuccessResult(new { id = tipo.Id, creado = true }, "Tipo creado exitosamente"));
 84        }
 085        catch (Exception ex)
 86        {
 087            _logger.LogError(ex, "Error al crear tipo de compensación");
 088            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 89        }
 390    }
 91
 92    /// <summary>
 93    /// Actualizar tipo de compensación
 94    /// PUT /api/tipos-compensacion/{id}
 95    /// </summary>
 96    [HttpPut("api/tipos-compensacion/{id}")]
 97    public async Task<IActionResult> UpdateTipo(long id, [FromBody] UpdateTipoCompensacionRequest request)
 98    {
 99        try
 100        {
 2101            var (success, _, error) = await _service.UpdateTipoAsync(id, request);
 2102            if (!success)
 103            {
 1104                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 1105                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0106                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al actualizar tipo"));
 107            }
 108
 1109            return Ok(ApiResponse<object>.SuccessResult(new { actualizado = true }, "Tipo actualizado exitosamente"));
 110        }
 0111        catch (Exception ex)
 112        {
 0113            _logger.LogError(ex, "Error al actualizar tipo de compensación {Id}", id);
 0114            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 115        }
 2116    }
 117
 118    /// <summary>
 119    /// Eliminar tipo de compensación
 120    /// DELETE /api/tipos-compensacion/{id}
 121    /// </summary>
 122    [HttpDelete("api/tipos-compensacion/{id}")]
 123    public async Task<IActionResult> DeleteTipo(long id)
 124    {
 125        try
 126        {
 4127            var (success, error) = await _service.DeleteTipoAsync(id);
 4128            if (!success)
 129            {
 3130                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 1131                    return NotFound(ApiResponse<string>.ErrorResult(error));
 2132                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al eliminar tipo"));
 133            }
 134
 1135            return NoContent();
 136        }
 0137        catch (Exception ex)
 138        {
 0139            _logger.LogError(ex, "Error al eliminar tipo de compensación {Id}", id);
 0140            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 141        }
 4142    }
 143
 144    // ── Configuraciones de Compensación ─────────────────────────────────────
 145
 146    /// <summary>
 147    /// Listar configuraciones de compensación
 148    /// GET /api/config-compensaciones?tipoId=&regimenId=&gradoId=&activo=
 149    /// </summary>
 150    [HttpGet("api/config-compensaciones")]
 151    public async Task<IActionResult> GetConfigs(
 152        [FromQuery] long? tipoId = null,
 153        [FromQuery] long? regimenId = null,
 154        [FromQuery] long? gradoId = null,
 155        [FromQuery] bool? activo = null)
 156    {
 157        try
 158        {
 1159            var configs = await _service.GetConfigsAsync(tipoId, regimenId, gradoId, activo);
 1160            return Ok(ApiResponse<object>.SuccessResult(configs));
 161        }
 0162        catch (Exception ex)
 163        {
 0164            _logger.LogError(ex, "Error al obtener configuraciones de compensación");
 0165            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 166        }
 1167    }
 168
 169    /// <summary>
 170    /// Obtener configuración por ID (incluye tope, fórmula, unidad)
 171    /// GET /api/config-compensaciones/{id}
 172    /// </summary>
 173    [HttpGet("api/config-compensaciones/{id}")]
 174    public async Task<IActionResult> GetConfig(long id)
 175    {
 176        try
 177        {
 0178            var config = await _service.GetConfigByIdAsync(id);
 0179            if (config == null)
 0180                return NotFound(ApiResponse<string>.ErrorResult("Configuración no encontrada"));
 0181            return Ok(ApiResponse<object>.SuccessResult(config));
 182        }
 0183        catch (Exception ex)
 184        {
 0185            _logger.LogError(ex, "Error al obtener configuración {Id}", id);
 0186            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 187        }
 0188    }
 189
 190    /// <summary>
 191    /// Crear configuración de compensación
 192    /// POST /api/config-compensaciones
 193    /// </summary>
 194    [HttpPost("api/config-compensaciones")]
 195    public async Task<IActionResult> CreateConfig([FromBody] CreateConfigCompensacionRequest request)
 196    {
 197        try
 198        {
 2199            var (success, config, error) = await _service.CreateConfigAsync(request);
 2200            if (!success)
 201            {
 1202                if (error?.Contains("No se encontró", StringComparison.OrdinalIgnoreCase) == true)
 0203                    return BadRequest(ApiResponse<string>.ErrorResult(error));
 1204                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al crear configuración"));
 205            }
 206
 1207            return StatusCode(201, ApiResponse<object>.SuccessResult(new { id = config!.Id, creado = true }, "Configurac
 208        }
 0209        catch (Exception ex)
 210        {
 0211            _logger.LogError(ex, "Error al crear configuración de compensación");
 0212            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 213        }
 2214    }
 215
 216    /// <summary>
 217    /// Actualizar configuración de compensación
 218    /// PUT /api/config-compensaciones/{id}
 219    /// </summary>
 220    [HttpPut("api/config-compensaciones/{id}")]
 221    public async Task<IActionResult> UpdateConfig(long id, [FromBody] UpdateConfigCompensacionRequest request)
 222    {
 223        try
 224        {
 1225            var (success, _, error) = await _service.UpdateConfigAsync(id, request);
 1226            if (!success)
 227            {
 1228                if (error?.Contains("no encontrada", StringComparison.OrdinalIgnoreCase) == true)
 1229                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0230                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al actualizar configuración"));
 231            }
 232
 0233            return Ok(ApiResponse<object>.SuccessResult(new { actualizado = true }, "Configuración actualizada exitosame
 234        }
 0235        catch (Exception ex)
 236        {
 0237            _logger.LogError(ex, "Error al actualizar configuración {Id}", id);
 0238            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 239        }
 1240    }
 241
 242    /// <summary>
 243    /// Eliminar configuración de compensación
 244    /// DELETE /api/config-compensaciones/{id}
 245    /// </summary>
 246    [HttpDelete("api/config-compensaciones/{id}")]
 247    public async Task<IActionResult> DeleteConfig(long id)
 248    {
 249        try
 250        {
 3251            var (success, error) = await _service.DeleteConfigAsync(id);
 3252            if (!success)
 253            {
 2254                if (error?.Contains("no encontrada", StringComparison.OrdinalIgnoreCase) == true)
 1255                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1256                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al eliminar configuración"));
 257            }
 258
 1259            return NoContent();
 260        }
 0261        catch (Exception ex)
 262        {
 0263            _logger.LogError(ex, "Error al eliminar configuración {Id}", id);
 0264            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 265        }
 3266    }
 267
 268    // ── Lotes de Compensación ────────────────────────────────────────────────
 269
 270    /// <summary>
 271    /// Listar lotes de compensación con paginación
 272    /// GET /api/lotes-compensacion?tipoId=&anio=&mes=&estado=&page=&pageSize=
 273    /// </summary>
 274    [HttpGet("api/lotes-compensacion")]
 275    public async Task<IActionResult> GetLotes(
 276        [FromQuery] long? tipoId = null,
 277        [FromQuery] int? anio = null,
 278        [FromQuery] int? mes = null,
 279        [FromQuery] string? estado = null,
 280        [FromQuery] int page = 1,
 281        [FromQuery] int pageSize = 20)
 282    {
 283        try
 284        {
 1285            if (page < 1) page = 1;
 1286            if (pageSize < 1 || pageSize > 100) pageSize = 20;
 287
 1288            var resultado = await _service.GetLotesPagedAsync(page, pageSize, tipoId, anio, mes, estado);
 1289            return Ok(ApiResponse<object>.SuccessResult(resultado));
 290        }
 0291        catch (Exception ex)
 292        {
 0293            _logger.LogError(ex, "Error al obtener lotes de compensación");
 0294            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 295        }
 1296    }
 297
 298    /// <summary>
 299    /// Obtener lote con sus ítems
 300    /// GET /api/lotes-compensacion/{id}
 301    /// </summary>
 302    [HttpGet("api/lotes-compensacion/{id}")]
 303    public async Task<IActionResult> GetLote(long id)
 304    {
 305        try
 306        {
 1307            var lote = await _service.GetLoteByIdAsync(id);
 1308            if (lote == null)
 1309                return NotFound(ApiResponse<string>.ErrorResult("Lote no encontrado"));
 310
 0311            return Ok(ApiResponse<object>.SuccessResult(lote));
 312        }
 0313        catch (Exception ex)
 314        {
 0315            _logger.LogError(ex, "Error al obtener lote {Id}", id);
 0316            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 317        }
 1318    }
 319
 320    /// <summary>
 321    /// Crear lote de compensación
 322    /// POST /api/lotes-compensacion
 323    /// </summary>
 324    [HttpPost("api/lotes-compensacion")]
 325    public async Task<IActionResult> CreateLote([FromBody] CreateLoteCompensacionRequest request)
 326    {
 327        try
 328        {
 2329            var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
 2330            if (!long.TryParse(userIdStr, out var usuarioId))
 0331                return Unauthorized(ApiResponse<string>.ErrorResult("No se pudo identificar al usuario"));
 332
 2333            var (success, lote, error) = await _service.CreateLoteAsync(usuarioId, request);
 2334            if (!success)
 1335                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al crear lote"));
 336
 1337            return CreatedAtAction(nameof(GetLote), new { id = lote!.Id },
 1338                ApiResponse<object>.SuccessResult(new { id = lote.Id, creado = true }, "Lote creado exitosamente"));
 339        }
 0340        catch (Exception ex)
 341        {
 0342            _logger.LogError(ex, "Error al crear lote de compensación");
 0343            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 344        }
 2345    }
 346
 347    /// <summary>
 348    /// Actualizar lote (solo en estado borrador)
 349    /// PUT /api/lotes-compensacion/{id}
 350    /// </summary>
 351    [HttpPut("api/lotes-compensacion/{id}")]
 352    public async Task<IActionResult> UpdateLote(long id, [FromBody] UpdateLoteCompensacionRequest request)
 353    {
 354        try
 355        {
 1356            var (success, _, error) = await _service.UpdateLoteAsync(id, request);
 1357            if (!success)
 358            {
 1359                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0360                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1361                if (error?.Contains("estado", StringComparison.OrdinalIgnoreCase) == true)
 1362                    return Conflict(ApiResponse<string>.ErrorResult(error));
 0363                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al actualizar lote"));
 364            }
 365
 0366            return Ok(ApiResponse<object>.SuccessResult(new { actualizado = true }, "Lote actualizado exitosamente"));
 367        }
 0368        catch (Exception ex)
 369        {
 0370            _logger.LogError(ex, "Error al actualizar lote {Id}", id);
 0371            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 372        }
 1373    }
 374
 375    /// <summary>
 376    /// Aprobar lote (debe estar en estado pendiente)
 377    /// PUT /api/lotes-compensacion/{id}/aprobar
 378    /// </summary>
 379    [HttpPut("api/lotes-compensacion/{id}/aprobar")]
 380    public async Task<IActionResult> AprobarLote(long id)
 381    {
 382        try
 383        {
 2384            var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
 2385            if (!long.TryParse(userIdStr, out var aprobadoPorId))
 0386                return Unauthorized(ApiResponse<string>.ErrorResult("No se pudo identificar al usuario"));
 387
 2388            var (success, _, error) = await _service.AprobarLoteAsync(id, aprobadoPorId);
 2389            if (!success)
 390            {
 1391                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0392                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1393                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al aprobar lote"));
 394            }
 395
 1396            return Ok(ApiResponse<object>.SuccessResult(new { aprobado = true }, "Lote aprobado exitosamente"));
 397        }
 0398        catch (Exception ex)
 399        {
 0400            _logger.LogError(ex, "Error al aprobar lote {Id}", id);
 0401            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 402        }
 2403    }
 404
 405    /// <summary>
 406    /// Rechazar lote (desde cualquier estado editable)
 407    /// PUT /api/lotes-compensacion/{id}/rechazar
 408    /// </summary>
 409    [HttpPut("api/lotes-compensacion/{id}/rechazar")]
 410    public async Task<IActionResult> RechazarLote(long id)
 411    {
 412        try
 413        {
 0414            var (success, _, error) = await _service.RechazarLoteAsync(id);
 0415            if (!success)
 416            {
 0417                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0418                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0419                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al rechazar lote"));
 420            }
 0421            return Ok(ApiResponse<object>.SuccessResult(new { rechazado = true }, "Lote rechazado"));
 422        }
 0423        catch (Exception ex)
 424        {
 0425            _logger.LogError(ex, "Error al rechazar lote {Id}", id);
 0426            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 427        }
 0428    }
 429
 430    /// <summary>
 431    /// Revertir lote aprobado a borrador (solo admin, antes de exportar)
 432    /// PUT /api/lotes-compensacion/{id}/revertir
 433    /// </summary>
 434    [HttpPut("api/lotes-compensacion/{id}/revertir")]
 435    public async Task<IActionResult> RevertirLote(long id)
 436    {
 437        try
 438        {
 0439            var (success, _, error) = await _service.RevertirABorradorAsync(id);
 0440            if (!success)
 441            {
 0442                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0443                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0444                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al revertir lote"));
 445            }
 0446            return Ok(ApiResponse<object>.SuccessResult(new { revertido = true }, "Lote revertido a borrador"));
 447        }
 0448        catch (Exception ex)
 449        {
 0450            _logger.LogError(ex, "Error al revertir lote {Id}", id);
 0451            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 452        }
 0453    }
 454
 455    /// <summary>
 456    /// Cancelar y eliminar un lote en borrador
 457    /// DELETE /api/lotes-compensacion/{id}
 458    /// </summary>
 459    [HttpDelete("api/lotes-compensacion/{id}")]
 460    public async Task<IActionResult> CancelarLote(long id)
 461    {
 462        try
 463        {
 0464            var (success, error) = await _service.CancelarLoteAsync(id);
 0465            if (!success)
 466            {
 0467                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0468                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0469                return Conflict(ApiResponse<string>.ErrorResult(error ?? "Error al cancelar lote"));
 470            }
 0471            return Ok(ApiResponse<object>.SuccessResult(new { cancelado = true }, "Lote cancelado y eliminado"));
 472        }
 0473        catch (Exception ex)
 474        {
 0475            _logger.LogError(ex, "Error al cancelar lote {Id}", id);
 0476            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 477        }
 0478    }
 479
 480    // ── Ítems de Lote ────────────────────────────────────────────────────────
 481
 482    /// <summary>
 483    /// Listar ítems de un lote con paginación
 484    /// GET /api/lotes-compensacion/{id}/items?page=1&pageSize=50
 485    /// </summary>
 486    [HttpGet("api/lotes-compensacion/{id}/items")]
 487    public async Task<IActionResult> GetItems(long id,
 488        [FromQuery] int page = 1,
 489        [FromQuery] int pageSize = 50)
 490    {
 491        try
 492        {
 0493            if (page < 1) page = 1;
 0494            if (pageSize < 1 || pageSize > 200) pageSize = 50;
 0495            var resultado = await _service.GetItemsPagedAsync(id, page, pageSize);
 0496            return Ok(ApiResponse<object>.SuccessResult(resultado));
 497        }
 0498        catch (Exception ex)
 499        {
 0500            _logger.LogError(ex, "Error al obtener ítems del lote {Id}", id);
 0501            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 502        }
 0503    }
 504
 505    /// <summary>
 506    /// Agregar ítem a lote (debe estar en estado borrador)
 507    /// POST /api/lotes-compensacion/{id}/items
 508    /// </summary>
 509    [HttpPost("api/lotes-compensacion/{id}/items")]
 510    public async Task<IActionResult> CreateItem(long id, [FromBody] CreateItemLoteRequest request)
 511    {
 512        try
 513        {
 4514            var (success, item, error) = await _service.CreateItemAsync(id, request);
 4515            if (!success)
 516            {
 3517                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0518                    return NotFound(ApiResponse<string>.ErrorResult(error));
 3519                if (error?.Contains("Ya existe", StringComparison.OrdinalIgnoreCase) == true)
 1520                    return Conflict(ApiResponse<string>.ErrorResult(error));
 2521                if (error?.Contains("estado", StringComparison.OrdinalIgnoreCase) == true)
 1522                    return Conflict(ApiResponse<string>.ErrorResult(error));
 1523                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al agregar ítem"));
 524            }
 525
 1526            return StatusCode(201, ApiResponse<object>.SuccessResult(new { id = item!.Id, creado = true }, "Ítem agregad
 527        }
 0528        catch (Exception ex)
 529        {
 0530            _logger.LogError(ex, "Error al agregar ítem al lote {LoteId}", id);
 0531            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 532        }
 4533    }
 534
 535    /// <summary>
 536    /// Carga masiva de ítems vía JSON array
 537    /// POST /api/lotes-compensacion/{id}/items/bulk?modo=reemplazar|agregar&amp;dryRun=false
 538    /// modo=reemplazar (default): elimina todos los ítems existentes antes de insertar (comportamiento legacy)
 539    /// modo=agregar: agrega sin borrar los existentes (para correcciones parciales)
 540    /// dryRun=true: valida sin persistir — devuelve el mismo resultado pero sin guardar nada
 541    /// </summary>
 542    [HttpPost("api/lotes-compensacion/{id}/items/bulk")]
 543    public async Task<IActionResult> CreateItemsBulk(
 544        long id,
 545        [FromBody] List<CreateItemLoteRequest> items,
 546        [FromQuery] string modo = "reemplazar",
 547        [FromQuery] bool dryRun = false)
 548    {
 549        try
 550        {
 0551            if (items == null || items.Count == 0)
 0552                return BadRequest(ApiResponse<string>.ErrorResult("La lista de ítems no puede estar vacía"));
 553
 0554            if (modo != "reemplazar" && modo != "agregar")
 0555                return BadRequest(ApiResponse<string>.ErrorResult("modo debe ser 'reemplazar' o 'agregar'"));
 556
 0557            var resultado = await _service.CreateItemsBulkAsync(id, items, modo, dryRun);
 0558            var msg = dryRun
 0559                ? $"Dry run: {resultado.Exitosos} válidos, {resultado.Fallidos} con errores (sin cambios)"
 0560                : $"Bulk {modo}: {resultado.Exitosos} exitosos, {resultado.Fallidos} fallidos";
 0561            return Ok(ApiResponse<object>.SuccessResult(resultado, msg));
 562        }
 0563        catch (Exception ex)
 564        {
 0565            _logger.LogError(ex, "Error en bulk import lote {LoteId}", id);
 0566            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 567        }
 0568    }
 569
 570    /// <summary>
 571    /// Carga masiva de ítems vía archivo CSV (multipart/form-data)
 572    /// POST /api/lotes-compensacion/{id}/items/bulk-csv?modo=reemplazar|agregar&amp;dryRun=false
 573    /// Formato CSV: cedula,monto,unidad_cantidad,funcion,incidencia,observaciones
 574    /// SAR/PAN requieren columna 'funcion' con valor CA o SA
 575    /// </summary>
 576    [HttpPost("api/lotes-compensacion/{id}/items/bulk-csv")]
 577    public async Task<IActionResult> CreateItemsBulkCsv(
 578        long id,
 579        IFormFile archivo,
 580        [FromQuery] string modo = "reemplazar",
 581        [FromQuery] bool dryRun = false)
 582    {
 583        try
 584        {
 0585            if (archivo == null || archivo.Length == 0)
 0586                return BadRequest(ApiResponse<string>.ErrorResult("Debe adjuntar un archivo CSV"));
 587
 0588            if (!archivo.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
 0589                return BadRequest(ApiResponse<string>.ErrorResult("El archivo debe tener extensión .csv"));
 590
 0591            if (modo != "reemplazar" && modo != "agregar")
 0592                return BadRequest(ApiResponse<string>.ErrorResult("modo debe ser 'reemplazar' o 'agregar'"));
 593
 0594            using var stream = archivo.OpenReadStream();
 0595            var resultado = await _service.CreateItemsFromCsvAsync(id, stream, modo, dryRun);
 596
 0597            var msg = dryRun
 0598                ? $"Dry run: {resultado.Exitosos} válidos, {resultado.Fallidos} con errores (sin cambios)"
 0599                : $"CSV {modo}: {resultado.Exitosos} exitosos, {resultado.Fallidos} fallidos";
 0600            return Ok(ApiResponse<object>.SuccessResult(resultado, msg));
 601        }
 0602        catch (Exception ex)
 603        {
 0604            _logger.LogError(ex, "Error en bulk CSV lote {LoteId}", id);
 0605            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 606        }
 0607    }
 608
 609    /// <summary>
 610    /// Actualizar ítem de lote (debe estar en estado borrador)
 611    /// PUT /api/lotes-compensacion/{id}/items/{itemId}
 612    /// </summary>
 613    [HttpPut("api/lotes-compensacion/{id}/items/{itemId}")]
 614    public async Task<IActionResult> UpdateItem(long id, long itemId, [FromBody] UpdateItemLoteRequest request)
 615    {
 616        try
 617        {
 1618            var (success, _, error) = await _service.UpdateItemAsync(id, itemId, request);
 1619            if (!success)
 620            {
 1621                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0622                    return NotFound(ApiResponse<string>.ErrorResult(error));
 1623                if (error?.Contains("estado", StringComparison.OrdinalIgnoreCase) == true)
 1624                    return Conflict(ApiResponse<string>.ErrorResult(error));
 0625                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al actualizar ítem"));
 626            }
 627
 0628            return Ok(ApiResponse<object>.SuccessResult(new { actualizado = true }, "Ítem actualizado exitosamente"));
 629        }
 0630        catch (Exception ex)
 631        {
 0632            _logger.LogError(ex, "Error al actualizar ítem {ItemId} del lote {LoteId}", itemId, id);
 0633            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 634        }
 1635    }
 636
 637    /// <summary>
 638    /// Eliminar ítem de lote (debe estar en estado borrador)
 639    /// DELETE /api/lotes-compensacion/{id}/items/{itemId}
 640    /// </summary>
 641    [HttpDelete("api/lotes-compensacion/{id}/items/{itemId}")]
 642    public async Task<IActionResult> DeleteItem(long id, long itemId)
 643    {
 644        try
 645        {
 2646            var (success, error) = await _service.DeleteItemAsync(id, itemId);
 1647            if (!success)
 648            {
 0649                if (error?.Contains("no encontrado", StringComparison.OrdinalIgnoreCase) == true)
 0650                    return NotFound(ApiResponse<string>.ErrorResult(error));
 0651                if (error?.Contains("estado", StringComparison.OrdinalIgnoreCase) == true)
 0652                    return Conflict(ApiResponse<string>.ErrorResult(error));
 0653                return BadRequest(ApiResponse<string>.ErrorResult(error ?? "Error al eliminar ítem"));
 654            }
 655
 1656            return Ok(ApiResponse<object>.SuccessResult(new { eliminado = true }, "Ítem eliminado exitosamente"));
 657        }
 1658        catch (Exception ex)
 659        {
 1660            _logger.LogError(ex, "Error al eliminar ítem {ItemId} del lote {LoteId}", itemId, id);
 1661            return StatusCode(500, ApiResponse<string>.ErrorResult("Error interno del servidor"));
 662        }
 2663    }
 664}