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 _logger; public EntradaSalidaCanillaService( IEntradaSalidaCanillaRepository esCanillaRepository, IPublicacionRepository publicacionRepository, ICanillaRepository canillaRepository, IPrecioRepository precioRepository, IRecargoZonaRepository recargoZonaRepository, IPorcMonCanillaRepository porcMonCanillaRepository, IUsuarioRepository usuarioRepository, DbConnectionFactory connectionFactory, ILogger logger) { _esCanillaRepository = esCanillaRepository; _publicacionRepository = publicacionRepository; _canillaRepository = canillaRepository; _precioRepository = precioRepository; _recargoZonaRepository = recargoZonaRepository; _porcMonCanillaRepository = porcMonCanillaRepository; _usuarioRepository = usuarioRepository; _connectionFactory = connectionFactory; _logger = logger; } private async Task 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 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> 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(); foreach (var mov in movimientos) { var dto = await MapToDto(mov); if (dto != null) dtos.Add(dto); } return dtos; } public async Task 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? 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 movimientosCreadosEntidades = new List(); List erroresItems = new List(); 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(); 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(); } } } } }