481 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			481 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
|  | using GestionIntegral.Api.Data; | ||
|  | using GestionIntegral.Api.Data.Repositories.Distribucion; | ||
|  | using GestionIntegral.Api.Data.Repositories.Usuarios; | ||
|  | using GestionIntegral.Api.Dtos.Distribucion; | ||
|  | using GestionIntegral.Api.Models.Distribucion; | ||
|  | using Microsoft.Extensions.Logging; | ||
|  | using System; | ||
|  | using System.Collections.Generic; | ||
|  | using System.Data; | ||
|  | using System.Globalization; | ||
|  | using System.Linq; | ||
|  | using System.Threading.Tasks; | ||
|  | using System.Security.Claims; | ||
|  | 
 | ||
|  | namespace GestionIntegral.Api.Services.Distribucion | ||
|  | { | ||
|  |     public class EntradaSalidaCanillaService : IEntradaSalidaCanillaService | ||
|  |     { | ||
|  |         private readonly IEntradaSalidaCanillaRepository _esCanillaRepository; | ||
|  |         private readonly IPublicacionRepository _publicacionRepository; | ||
|  |         private readonly ICanillaRepository _canillaRepository; | ||
|  |         private readonly IPrecioRepository _precioRepository; | ||
|  |         private readonly IRecargoZonaRepository _recargoZonaRepository; | ||
|  |         private readonly IPorcMonCanillaRepository _porcMonCanillaRepository; | ||
|  |         private readonly IUsuarioRepository _usuarioRepository; | ||
|  |         private readonly DbConnectionFactory _connectionFactory; | ||
|  |         private readonly ILogger<EntradaSalidaCanillaService> _logger; | ||
|  | 
 | ||
|  |         public EntradaSalidaCanillaService( | ||
|  |             IEntradaSalidaCanillaRepository esCanillaRepository, | ||
|  |             IPublicacionRepository publicacionRepository, | ||
|  |             ICanillaRepository canillaRepository, | ||
|  |             IPrecioRepository precioRepository, | ||
|  |             IRecargoZonaRepository recargoZonaRepository, | ||
|  |             IPorcMonCanillaRepository porcMonCanillaRepository, | ||
|  |             IUsuarioRepository usuarioRepository, | ||
|  |             DbConnectionFactory connectionFactory, | ||
|  |             ILogger<EntradaSalidaCanillaService> logger) | ||
|  |         { | ||
|  |             _esCanillaRepository = esCanillaRepository; | ||
|  |             _publicacionRepository = publicacionRepository; | ||
|  |             _canillaRepository = canillaRepository; | ||
|  |             _precioRepository = precioRepository; | ||
|  |             _recargoZonaRepository = recargoZonaRepository; | ||
|  |             _porcMonCanillaRepository = porcMonCanillaRepository; | ||
|  |             _usuarioRepository = usuarioRepository; | ||
|  |             _connectionFactory = connectionFactory; | ||
|  |             _logger = logger; | ||
|  |         } | ||
|  | 
 | ||
|  |         private async Task<EntradaSalidaCanillaDto?> MapToDto(EntradaSalidaCanilla? es) | ||
|  |         { | ||
|  |             if (es == null) return null; | ||
|  | 
 | ||
|  |             var publicacionDataResult = await _publicacionRepository.GetByIdAsync(es.IdPublicacion); | ||
|  |             var canillaDataResult = await _canillaRepository.GetByIdAsync(es.IdCanilla); // Devuelve tupla | ||
|  | 
 | ||
|  |             Publicacion? publicacionEntity = publicacionDataResult.Publicacion; // Componente nullable de la tupla | ||
|  |             Canilla? canillaEntity = canillaDataResult.Canilla; // Componente nullable de la tupla | ||
|  | 
 | ||
|  |             var usuarioLiq = es.UserLiq.HasValue ? await _usuarioRepository.GetByIdAsync(es.UserLiq.Value) : null; | ||
|  | 
 | ||
|  |             decimal montoARendir = await CalcularMontoARendir(es, canillaEntity, publicacionEntity); | ||
|  | 
 | ||
|  |             return new EntradaSalidaCanillaDto | ||
|  |             { | ||
|  |                 IdParte = es.IdParte, | ||
|  |                 IdPublicacion = es.IdPublicacion, | ||
|  |                 NombrePublicacion = publicacionEntity?.Nombre ?? "Pub. Desconocida", | ||
|  |                 IdCanilla = es.IdCanilla, | ||
|  |                 NomApeCanilla = canillaEntity?.NomApe ?? "Can. Desconocido", | ||
|  |                 CanillaEsAccionista = canillaEntity?.Accionista ?? false, | ||
|  |                 Fecha = es.Fecha.ToString("yyyy-MM-dd"), | ||
|  |                 CantSalida = es.CantSalida, | ||
|  |                 CantEntrada = es.CantEntrada, | ||
|  |                 Vendidos = es.CantSalida - es.CantEntrada, | ||
|  |                 Observacion = es.Observacion, | ||
|  |                 Liquidado = es.Liquidado, | ||
|  |                 FechaLiquidado = es.FechaLiquidado?.ToString("yyyy-MM-dd"), | ||
|  |                 UserLiq = es.UserLiq, | ||
|  |                 NombreUserLiq = usuarioLiq != null ? $"{usuarioLiq.Nombre} {usuarioLiq.Apellido}" : null, | ||
|  |                 MontoARendir = montoARendir, | ||
|  |                 PrecioUnitarioAplicado = (await _precioRepository.GetByIdAsync(es.IdPrecio))?.Lunes ?? 0, | ||
|  |                 RecargoAplicado = es.IdRecargo > 0 ? (await _recargoZonaRepository.GetByIdAsync(es.IdRecargo))?.Valor ?? 0 : 0, | ||
|  |                 PorcentajeOMontoCanillaAplicado = es.IdPorcMon > 0 ? (await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon))?.PorcMon ?? 0 : 0, | ||
|  |                 EsPorcentajeCanilla = es.IdPorcMon > 0 ? (await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon))?.EsPorcentaje ?? false : false | ||
|  |             }; | ||
|  |         } | ||
|  | 
 | ||
|  |         private async Task<decimal> CalcularMontoARendir(EntradaSalidaCanilla es, Canilla? canilla, Publicacion? publicacion) // Acepta nullable Canilla y Publicacion | ||
|  |         { | ||
|  |             if (es.CantSalida - es.CantEntrada <= 0) return 0; | ||
|  | 
 | ||
|  |             var precioConfig = await _precioRepository.GetByIdAsync(es.IdPrecio); | ||
|  |             if (precioConfig == null) | ||
|  |             { | ||
|  |                 _logger.LogError("Configuración de precio ID {IdPrecio} no encontrada para movimiento ID {IdParte}.", es.IdPrecio, es.IdParte); | ||
|  |                 throw new InvalidOperationException($"Configuración de precio ID {es.IdPrecio} no encontrada para el movimiento."); | ||
|  |             } | ||
|  | 
 | ||
|  |             decimal precioDia = 0; | ||
|  |             DayOfWeek diaSemana = es.Fecha.DayOfWeek; | ||
|  |             switch (diaSemana) | ||
|  |             { | ||
|  |                 case DayOfWeek.Monday: precioDia = precioConfig.Lunes ?? 0; break; | ||
|  |                 case DayOfWeek.Tuesday: precioDia = precioConfig.Martes ?? 0; break; | ||
|  |                 case DayOfWeek.Wednesday: precioDia = precioConfig.Miercoles ?? 0; break; | ||
|  |                 case DayOfWeek.Thursday: precioDia = precioConfig.Jueves ?? 0; break; | ||
|  |                 case DayOfWeek.Friday: precioDia = precioConfig.Viernes ?? 0; break; | ||
|  |                 case DayOfWeek.Saturday: precioDia = precioConfig.Sabado ?? 0; break; | ||
|  |                 case DayOfWeek.Sunday: precioDia = precioConfig.Domingo ?? 0; break; | ||
|  |             } | ||
|  | 
 | ||
|  |             decimal valorRecargo = 0; | ||
|  |             if (es.IdRecargo > 0) | ||
|  |             { | ||
|  |                 var recargoConfig = await _recargoZonaRepository.GetByIdAsync(es.IdRecargo); | ||
|  |                 if (recargoConfig != null) valorRecargo = recargoConfig.Valor; | ||
|  |             } | ||
|  | 
 | ||
|  |             decimal precioFinalUnitario = precioDia + valorRecargo; | ||
|  |             int cantidadVendida = es.CantSalida - es.CantEntrada; | ||
|  | 
 | ||
|  |             if (canilla != null && canilla.Accionista && es.IdPorcMon > 0) // Check null para canilla | ||
|  |             { | ||
|  |                 var porcMonConfig = await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon); | ||
|  |                 if (porcMonConfig != null) | ||
|  |                 { | ||
|  |                     if (porcMonConfig.EsPorcentaje) | ||
|  |                     { | ||
|  |                         return Math.Round((precioFinalUnitario * cantidadVendida * porcMonConfig.PorcMon) / 100, 2); | ||
|  |                     } | ||
|  |                     else | ||
|  |                     { | ||
|  |                         return Math.Round(cantidadVendida * porcMonConfig.PorcMon, 2); | ||
|  |                     } | ||
|  |                 } | ||
|  |             } | ||
|  |             return Math.Round(precioFinalUnitario * cantidadVendida, 2); | ||
|  |         } | ||
|  | 
 | ||
|  |         public async Task<IEnumerable<EntradaSalidaCanillaDto>> ObtenerTodosAsync( | ||
|  |             DateTime? fechaDesde, DateTime? fechaHasta, | ||
|  |             int? idPublicacion, int? idCanilla, bool? liquidados, bool? incluirNoLiquidados) | ||
|  |         { | ||
|  |             bool? filtroLiquidadoFinal = null; | ||
|  |             if (liquidados.HasValue) | ||
|  |             { | ||
|  |                 filtroLiquidadoFinal = liquidados.Value; | ||
|  |             } | ||
|  |             else | ||
|  |             { | ||
|  |                 if (incluirNoLiquidados.HasValue && !incluirNoLiquidados.Value) | ||
|  |                 { | ||
|  |                     filtroLiquidadoFinal = true; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             var movimientos = await _esCanillaRepository.GetAllAsync(fechaDesde, fechaHasta, idPublicacion, idCanilla, filtroLiquidadoFinal); | ||
|  |             var dtos = new List<EntradaSalidaCanillaDto>(); | ||
|  |             foreach (var mov in movimientos) | ||
|  |             { | ||
|  |                 var dto = await MapToDto(mov); | ||
|  |                 if (dto != null) dtos.Add(dto); | ||
|  |             } | ||
|  |             return dtos; | ||
|  |         } | ||
|  | 
 | ||
|  |         public async Task<EntradaSalidaCanillaDto?> ObtenerPorIdAsync(int idParte) | ||
|  |         { | ||
|  |             var movimiento = await _esCanillaRepository.GetByIdAsync(idParte); | ||
|  |             return await MapToDto(movimiento); | ||
|  |         } | ||
|  | 
 | ||
|  |         public async Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaCanillaDto updateDto, int idUsuario) | ||
|  |         { | ||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||
|  |             if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open(); | ||
|  |             using var transaction = connection.BeginTransaction(); | ||
|  |             try | ||
|  |             { | ||
|  |                 var esExistente = await _esCanillaRepository.GetByIdAsync(idParte); | ||
|  |                 if (esExistente == null) return (false, "Movimiento no encontrado."); | ||
|  |                 if (esExistente.Liquidado) return (false, "No se puede modificar un movimiento ya liquidado."); | ||
|  | 
 | ||
|  |                 esExistente.CantSalida = updateDto.CantSalida; | ||
|  |                 esExistente.CantEntrada = updateDto.CantEntrada; | ||
|  |                 esExistente.Observacion = updateDto.Observacion; | ||
|  | 
 | ||
|  |                 var actualizado = await _esCanillaRepository.UpdateAsync(esExistente, idUsuario, transaction); | ||
|  |                 if (!actualizado) throw new DataException("Error al actualizar el movimiento."); | ||
|  | 
 | ||
|  |                 transaction.Commit(); | ||
|  |                 _logger.LogInformation("Movimiento Canillita ID {Id} actualizado por Usuario ID {UserId}.", idParte, idUsuario); | ||
|  |                 return (true, null); | ||
|  |             } | ||
|  |             catch (KeyNotFoundException) { if (transaction.Connection != null) try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); } | ||
|  |             catch (Exception ex) | ||
|  |             { | ||
|  |                 if (transaction.Connection != null) try { transaction.Rollback(); } catch { } | ||
|  |                 _logger.LogError(ex, "Error ActualizarMovimientoAsync Canillita ID: {Id}", idParte); | ||
|  |                 return (false, $"Error interno: {ex.Message}"); | ||
|  |             } | ||
|  |             finally | ||
|  |             { | ||
|  |                 if (connection?.State == ConnectionState.Open) | ||
|  |                 { | ||
|  |                     if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close(); | ||
|  |                 } | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         public async Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, int idUsuario, ClaimsPrincipal userPrincipal) | ||
|  |         { | ||
|  |             // Helper interno para verificar permisos desde el ClaimsPrincipal proporcionado | ||
|  |             bool TienePermisoEspecifico(string codAcc) => userPrincipal.IsInRole("SuperAdmin") || userPrincipal.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||
|  | 
 | ||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||
|  |             IDbTransaction? transaction = null; | ||
|  | 
 | ||
|  |             try | ||
|  |             { | ||
|  |                 if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); | ||
|  |                 else if (connection.State == ConnectionState.Closed) connection.Open(); | ||
|  |                  | ||
|  |                 transaction = connection.BeginTransaction(); | ||
|  | 
 | ||
|  |                 var esExistente = await _esCanillaRepository.GetByIdAsync(idParte); | ||
|  |                 if (esExistente == null) | ||
|  |                 { | ||
|  |                     if (transaction?.Connection != null) transaction.Rollback(); | ||
|  |                     return (false, "Movimiento no encontrado."); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 if (esExistente.Liquidado) | ||
|  |                 { | ||
|  |                     // Permiso MC006 es para "Eliminar Movimientos de Canillita Liquidados" | ||
|  |                     if (!TienePermisoEspecifico("MC006")) | ||
|  |                     { | ||
|  |                         if (transaction?.Connection != null) transaction.Rollback(); | ||
|  |                         return (false, "No tiene permiso para eliminar movimientos ya liquidados. Se requiere permiso especial (MC006) o ser SuperAdmin."); | ||
|  |                     } | ||
|  |                     _logger.LogWarning("Usuario ID {IdUsuario} está eliminando un movimiento LIQUIDADO (IDParte: {IdParte}). Permiso MC006 verificado.", idUsuario, idParte); | ||
|  |                 } | ||
|  |                 // Si no está liquidado, el permiso MC004 ya fue verificado en el controlador. | ||
|  | 
 | ||
|  |                 var eliminado = await _esCanillaRepository.DeleteAsync(idParte, idUsuario, transaction); | ||
|  |                 if (!eliminado) | ||
|  |                 { | ||
|  |                     // No es necesario hacer rollback aquí si DeleteAsync lanza una excepción, | ||
|  |                     // ya que el bloque catch lo manejará. Si DeleteAsync devuelve false sin lanzar, | ||
|  |                     // entonces sí sería necesario un rollback. | ||
|  |                     if (transaction?.Connection != null) transaction.Rollback(); | ||
|  |                     throw new DataException("Error al eliminar el movimiento desde el repositorio."); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 if (transaction?.Connection != null) transaction.Commit(); | ||
|  |                 _logger.LogInformation("Movimiento Canillita ID {IdParte} eliminado por Usuario ID {IdUsuario}.", idParte, idUsuario); | ||
|  |                 return (true, null); | ||
|  |             } | ||
|  |             catch (KeyNotFoundException)  | ||
|  |             {  | ||
|  |                 if (transaction?.Connection != null) try { transaction.Rollback(); } catch (Exception exR) { _logger.LogError(exR, "Rollback fallido KeyNotFoundException."); } | ||
|  |                 return (false, "Movimiento no encontrado.");  | ||
|  |             } | ||
|  |             catch (Exception ex) | ||
|  |             { | ||
|  |                 if (transaction?.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } } | ||
|  |                 _logger.LogError(ex, "Error EliminarMovimientoAsync Canillita ID: {IdParte}", idParte); | ||
|  |                 return (false, $"Error interno: {ex.Message}"); | ||
|  |             } | ||
|  |             finally | ||
|  |             { | ||
|  |                 if (transaction != null) transaction.Dispose(); | ||
|  |                 if (connection?.State == ConnectionState.Open) | ||
|  |                 { | ||
|  |                     if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close(); | ||
|  |                 } | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         public async Task<(bool Exito, string? Error)> LiquidarMovimientosAsync(LiquidarMovimientosCanillaRequestDto liquidarDto, int idUsuarioLiquidador) | ||
|  |         { | ||
|  |             if (liquidarDto.IdsPartesALiquidar == null || !liquidarDto.IdsPartesALiquidar.Any()) | ||
|  |                 return (false, "No se especificaron movimientos para liquidar."); | ||
|  | 
 | ||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||
|  |             if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open(); | ||
|  |             using var transaction = connection.BeginTransaction(); | ||
|  |             try | ||
|  |             { | ||
|  |                 bool liquidacionExitosa = await _esCanillaRepository.LiquidarAsync(liquidarDto.IdsPartesALiquidar, liquidarDto.FechaLiquidacion.Date, idUsuarioLiquidador, transaction); | ||
|  | 
 | ||
|  |                 if (!liquidacionExitosa) | ||
|  |                 { | ||
|  |                     _logger.LogWarning("Liquidación de movimientos de canillita pudo no haber afectado a todos los IDs solicitados. IDs: {Ids}", string.Join(",", liquidarDto.IdsPartesALiquidar)); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 if (transaction.Connection != null) transaction.Commit(); | ||
|  |                 _logger.LogInformation("Movimientos de Canillita liquidados. Ids: {Ids} por Usuario ID {UserId}.", string.Join(",", liquidarDto.IdsPartesALiquidar), idUsuarioLiquidador); | ||
|  |                 return (true, null); | ||
|  |             } | ||
|  |             catch (Exception ex) | ||
|  |             { | ||
|  |                 if (transaction.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } } | ||
|  |                 _logger.LogError(ex, "Error al liquidar movimientos de canillita."); | ||
|  |                 return (false, $"Error interno al liquidar movimientos: {ex.Message}"); | ||
|  |             } | ||
|  |             finally | ||
|  |             { | ||
|  |                 if (connection?.State == ConnectionState.Open) | ||
|  |                 { | ||
|  |                     if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close(); | ||
|  |                 } | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         public async Task<(IEnumerable<EntradaSalidaCanillaDto>? MovimientosCreados, string? Error)> CrearMovimientosEnLoteAsync(CreateBulkEntradaSalidaCanillaDto createBulkDto, int idUsuario) | ||
|  |         { | ||
|  |             var canillaDataResult = await _canillaRepository.GetByIdAsync(createBulkDto.IdCanilla); | ||
|  |             Canilla? canillaActual = canillaDataResult.Canilla; | ||
|  |             if (canillaActual == null) return (null, "Canillita no válido."); | ||
|  |             if (canillaActual.Baja) return (null, "El canillita está dado de baja."); | ||
|  | 
 | ||
|  |             List<EntradaSalidaCanilla> movimientosCreadosEntidades = new List<EntradaSalidaCanilla>(); | ||
|  |             List<string> erroresItems = new List<string>(); | ||
|  | 
 | ||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||
|  |             if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open(); | ||
|  |             using var transaction = connection.BeginTransaction(); | ||
|  | 
 | ||
|  |             try | ||
|  |             { | ||
|  |                 foreach (var itemDto in createBulkDto.Items.Where(i => i.IdPublicacion > 0)) | ||
|  |                 { | ||
|  |                     if (await _esCanillaRepository.ExistsByPublicacionCanillaFechaAsync(itemDto.IdPublicacion, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, transaction: transaction)) | ||
|  |                     { | ||
|  |                         var pubInfo = await _publicacionRepository.GetByIdSimpleAsync(itemDto.IdPublicacion); | ||
|  |                         erroresItems.Add($"Ya existe un registro para la publicación '{pubInfo?.Nombre ?? itemDto.IdPublicacion.ToString()}' para {canillaActual.NomApe} en la fecha {createBulkDto.Fecha:dd/MM/yyyy}."); | ||
|  |                     } | ||
|  |                 } | ||
|  |                 if (erroresItems.Any()) | ||
|  |                 { | ||
|  |                     if (transaction.Connection != null) transaction.Rollback(); | ||
|  |                     return (null, string.Join(" ", erroresItems)); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 foreach (var itemDto in createBulkDto.Items) | ||
|  |                 { | ||
|  |                     if (itemDto.IdPublicacion == 0 && itemDto.CantSalida == 0 && itemDto.CantEntrada == 0 && string.IsNullOrWhiteSpace(itemDto.Observacion)) continue; | ||
|  |                     if (itemDto.IdPublicacion == 0) | ||
|  |                     { | ||
|  |                         if (itemDto.CantSalida > 0 || itemDto.CantEntrada > 0 || !string.IsNullOrWhiteSpace(itemDto.Observacion)) | ||
|  |                         { | ||
|  |                             erroresItems.Add($"Falta seleccionar la publicación para una de las líneas con cantidades/observación."); | ||
|  |                         } | ||
|  |                         continue; | ||
|  |                     } | ||
|  | 
 | ||
|  |                     var publicacionItem = await _publicacionRepository.GetByIdSimpleAsync(itemDto.IdPublicacion); | ||
|  |                     bool noEsValidaONoHabilitada = false; | ||
|  |                     if (publicacionItem == null) | ||
|  |                     { | ||
|  |                         noEsValidaONoHabilitada = true; | ||
|  |                     } | ||
|  |                     else | ||
|  |                     { | ||
|  |                         // Si Habilitada es bool? y NULL significa que toma el DEFAULT 1 (true) de la BD | ||
|  |                         // entonces solo consideramos error si es explícitamente false. | ||
|  |                         if (publicacionItem.Habilitada.HasValue && publicacionItem.Habilitada.Value == false) | ||
|  |                         { | ||
|  |                             noEsValidaONoHabilitada = true; | ||
|  |                         } | ||
|  |                         // Si publicacionItem.Habilitada es null o true, noEsValidaONoHabilitada permanece false. | ||
|  |                     } | ||
|  | 
 | ||
|  |                     if (noEsValidaONoHabilitada) | ||
|  |                     { | ||
|  |                         erroresItems.Add($"Publicación ID {itemDto.IdPublicacion} no es válida o no está habilitada."); | ||
|  |                         continue; | ||
|  |                     } | ||
|  | 
 | ||
|  |                     var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(itemDto.IdPublicacion, createBulkDto.Fecha.Date, transaction); | ||
|  |                     if (precioActivo == null) | ||
|  |                     { | ||
|  |                         string nombrePubParaError = publicacionItem?.Nombre ?? $"ID {itemDto.IdPublicacion}"; | ||
|  |                         erroresItems.Add($"No hay precio definido para '{nombrePubParaError}' en {createBulkDto.Fecha:dd/MM/yyyy}."); | ||
|  |                         continue; | ||
|  |                     } | ||
|  | 
 | ||
|  |                     RecargoZona? recargoActivo = null; | ||
|  |                     // Aquí usamos canillaActual! porque ya verificamos que no es null al inicio del método. | ||
|  |                     if (canillaActual!.IdZona > 0) | ||
|  |                     { | ||
|  |                         recargoActivo = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(itemDto.IdPublicacion, canillaActual.IdZona, createBulkDto.Fecha.Date, transaction); | ||
|  |                     } | ||
|  | 
 | ||
|  |                     PorcMonCanilla? porcMonActivo = null; | ||
|  |                     // Aquí usamos canillaActual! porque ya verificamos que no es null al inicio del método. | ||
|  |                     if (canillaActual!.Accionista) | ||
|  |                     { | ||
|  |                         porcMonActivo = await _porcMonCanillaRepository.GetActiveByPublicacionCanillaAndDateAsync(itemDto.IdPublicacion, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, transaction); | ||
|  |                         if (porcMonActivo == null) | ||
|  |                         { | ||
|  |                             // Dentro de este bloque, canillaActual.NomApe es seguro porque Accionista era true. | ||
|  |                             string nombreCanParaError = canillaActual.NomApe; | ||
|  |                             string nombrePubParaError = publicacionItem?.Nombre ?? $"Publicación ID {itemDto.IdPublicacion}"; | ||
|  |                             erroresItems.Add($"'{nombreCanParaError}' es accionista pero no tiene %/monto para '{nombrePubParaError}' en {createBulkDto.Fecha:dd/MM/yyyy}."); | ||
|  |                             continue; | ||
|  |                         } | ||
|  |                     } | ||
|  | 
 | ||
|  |                     var nuevoES = new EntradaSalidaCanilla | ||
|  |                     { | ||
|  |                         IdPublicacion = itemDto.IdPublicacion, | ||
|  |                         IdCanilla = createBulkDto.IdCanilla, | ||
|  |                         Fecha = createBulkDto.Fecha.Date, | ||
|  |                         CantSalida = itemDto.CantSalida, | ||
|  |                         CantEntrada = itemDto.CantEntrada, | ||
|  |                         Observacion = itemDto.Observacion, | ||
|  |                         IdPrecio = precioActivo.IdPrecio, | ||
|  |                         IdRecargo = recargoActivo?.IdRecargo ?? 0, | ||
|  |                         IdPorcMon = porcMonActivo?.IdPorcMon ?? 0, | ||
|  |                         Liquidado = false, | ||
|  |                         FechaLiquidado = null, | ||
|  |                         UserLiq = null | ||
|  |                     }; | ||
|  | 
 | ||
|  |                     var esCreada = await _esCanillaRepository.CreateAsync(nuevoES, idUsuario, transaction); | ||
|  |                     if (esCreada == null) throw new DataException($"Error al registrar movimiento para Publicación ID {itemDto.IdPublicacion}."); | ||
|  |                     movimientosCreadosEntidades.Add(esCreada); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 if (erroresItems.Any()) | ||
|  |                 { | ||
|  |                     if (transaction.Connection != null) transaction.Rollback(); | ||
|  |                     return (null, string.Join(" ", erroresItems)); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 // CORRECCIÓN PARA CS0019 (línea 394 original): | ||
|  |                 bool tieneItemsSignificativos = false; | ||
|  |                 if (createBulkDto.Items != null) // Checkear si Items es null antes de llamar a Any() | ||
|  |                 { | ||
|  |                     tieneItemsSignificativos = createBulkDto.Items.Any(i => i.IdPublicacion > 0 && | ||
|  |                                                                     (i.CantSalida > 0 || i.CantEntrada > 0 || !string.IsNullOrWhiteSpace(i.Observacion))); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 if (!movimientosCreadosEntidades.Any() && tieneItemsSignificativos) | ||
|  |                 { | ||
|  |                     if (transaction.Connection != null) transaction.Rollback(); | ||
|  |                     return (null, "No se pudo procesar ningún ítem válido con datos significativos."); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 if (transaction.Connection != null) transaction.Commit(); | ||
|  |                 _logger.LogInformation("Lote de {Count} movimientos Canillita para Canilla ID {IdCanilla} en Fecha {Fecha} creados por Usuario ID {UserId}.", | ||
|  |                     movimientosCreadosEntidades.Count, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, idUsuario); | ||
|  | 
 | ||
|  |                 var dtosCreados = new List<EntradaSalidaCanillaDto>(); | ||
|  |                 foreach (var entidad in movimientosCreadosEntidades) | ||
|  |                 { | ||
|  |                     var dto = await MapToDto(entidad); | ||
|  |                     if (dto != null) dtosCreados.Add(dto); | ||
|  |                 } | ||
|  |                 return (dtosCreados, null); | ||
|  |             } | ||
|  |             catch (Exception ex) | ||
|  |             { | ||
|  |                 if (transaction.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } } | ||
|  |                 _logger.LogError(ex, "Error CrearMovimientosEnLoteAsync para Canilla ID {IdCanilla}, Fecha {Fecha}", createBulkDto.IdCanilla, createBulkDto.Fecha); | ||
|  |                 return (null, $"Error interno al procesar el lote: {ex.Message}"); | ||
|  |             } | ||
|  |             finally | ||
|  |             { | ||
|  |                 if (connection?.State == ConnectionState.Open) | ||
|  |                 { | ||
|  |                     if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close(); | ||
|  |                 } | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | } |