using GestionIntegral.Api.Data; using GestionIntegral.Api.Data.Repositories.Impresion; using GestionIntegral.Api.Dtos.Impresion; using GestionIntegral.Api.Models.Impresion; using GestionIntegral.Api.Models.Distribucion; // Para Publicacion, PubliSeccion using GestionIntegral.Api.Data.Repositories.Distribucion; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; using GestionIntegral.Api.Dtos.Auditoria; namespace GestionIntegral.Api.Services.Impresion { public class StockBobinaService : IStockBobinaService { private readonly IStockBobinaRepository _stockBobinaRepository; private readonly ITipoBobinaRepository _tipoBobinaRepository; private readonly IPlantaRepository _plantaRepository; private readonly IEstadoBobinaRepository _estadoBobinaRepository; private readonly IPublicacionRepository _publicacionRepository; // Para validar IdPublicacion private readonly IPubliSeccionRepository _publiSeccionRepository; // Para validar IdSeccion private readonly DbConnectionFactory _connectionFactory; private readonly ILogger _logger; public StockBobinaService( IStockBobinaRepository stockBobinaRepository, ITipoBobinaRepository tipoBobinaRepository, IPlantaRepository plantaRepository, IEstadoBobinaRepository estadoBobinaRepository, IPublicacionRepository publicacionRepository, IPubliSeccionRepository publiSeccionRepository, DbConnectionFactory connectionFactory, ILogger logger) { _stockBobinaRepository = stockBobinaRepository; _tipoBobinaRepository = tipoBobinaRepository; _plantaRepository = plantaRepository; _estadoBobinaRepository = estadoBobinaRepository; _publicacionRepository = publicacionRepository; _publiSeccionRepository = publiSeccionRepository; _connectionFactory = connectionFactory; _logger = logger; } // Mapeo complejo porque necesitamos nombres de entidades relacionadas private async Task MapToDto(StockBobina bobina) { if (bobina == null) return null!; // Debería ser manejado por el llamador var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(bobina.IdTipoBobina); var planta = await _plantaRepository.GetByIdAsync(bobina.IdPlanta); var estado = await _estadoBobinaRepository.GetByIdAsync(bobina.IdEstadoBobina); Publicacion? publicacion = null; PubliSeccion? seccion = null; if (bobina.IdPublicacion.HasValue) publicacion = await _publicacionRepository.GetByIdSimpleAsync(bobina.IdPublicacion.Value); if (bobina.IdSeccion.HasValue) seccion = await _publiSeccionRepository.GetByIdAsync(bobina.IdSeccion.Value); // Asume que GetByIdAsync existe return new StockBobinaDto { IdBobina = bobina.IdBobina, IdTipoBobina = bobina.IdTipoBobina, NombreTipoBobina = tipoBobina?.Denominacion ?? "N/A", NroBobina = bobina.NroBobina, Peso = bobina.Peso, IdPlanta = bobina.IdPlanta, NombrePlanta = planta?.Nombre ?? "N/A", IdEstadoBobina = bobina.IdEstadoBobina, NombreEstadoBobina = estado?.Denominacion ?? "N/A", Remito = bobina.Remito, FechaRemito = bobina.FechaRemito.ToString("yyyy-MM-dd"), FechaEstado = bobina.FechaEstado?.ToString("yyyy-MM-dd"), IdPublicacion = bobina.IdPublicacion, NombrePublicacion = publicacion?.Nombre, IdSeccion = bobina.IdSeccion, NombreSeccion = seccion?.Nombre, Obs = bobina.Obs }; } public async Task> ObtenerTodosAsync( int? idTipoBobina, string? nroBobinaFilter, int? idPlanta, int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta) { var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta); var dtos = new List(); foreach (var bobina in bobinas) { dtos.Add(await MapToDto(bobina)); } return dtos; } public async Task ObtenerPorIdAsync(int idBobina) { var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); return bobina == null ? null : await MapToDto(bobina); } public async Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario) { if (await _tipoBobinaRepository.GetByIdAsync(createDto.IdTipoBobina) == null) return (null, "Tipo de bobina inválido."); if (await _plantaRepository.GetByIdAsync(createDto.IdPlanta) == null) return (null, "Planta inválida."); if (await _stockBobinaRepository.GetByNroBobinaAsync(createDto.NroBobina) != null) return (null, $"El número de bobina '{createDto.NroBobina}' ya existe."); var nuevaBobina = new StockBobina { IdTipoBobina = createDto.IdTipoBobina, NroBobina = createDto.NroBobina, Peso = createDto.Peso, IdPlanta = createDto.IdPlanta, IdEstadoBobina = 1, // 1 = Disponible (según contexto previo) Remito = createDto.Remito, FechaRemito = createDto.FechaRemito.Date, FechaEstado = createDto.FechaRemito.Date, // Estado inicial en fecha de remito IdPublicacion = null, IdSeccion = null, Obs = null }; using var connection = _connectionFactory.CreateConnection(); if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); using var transaction = connection.BeginTransaction(); try { var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction); if (bobinaCreada == null) throw new DataException("Error al ingresar la bobina."); transaction.Commit(); _logger.LogInformation("Bobina ID {Id} ingresada por Usuario ID {UserId}.", bobinaCreada.IdBobina, idUsuario); return (await MapToDto(bobinaCreada), null); } catch (Exception ex) { try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error IngresarBobinaAsync: {NroBobina}", createDto.NroBobina); return (null, $"Error interno: {ex.Message}"); } } public async Task<(bool Exito, string? Error)> ActualizarDatosBobinaDisponibleAsync(int idBobina, UpdateStockBobinaDto updateDto, int idUsuario) { using var connection = _connectionFactory.CreateConnection(); if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); using var transaction = connection.BeginTransaction(); try { var bobinaExistente = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de TX if (bobinaExistente == null) return (false, "Bobina no encontrada."); if (bobinaExistente.IdEstadoBobina != 1) // Solo se pueden editar datos si está "Disponible" return (false, "Solo se pueden modificar los datos de una bobina en estado 'Disponible'."); // Validar unicidad de NroBobina si cambió if (bobinaExistente.NroBobina != updateDto.NroBobina && await _stockBobinaRepository.GetByNroBobinaAsync(updateDto.NroBobina) != null) // Validar fuera de TX o pasar TX al repo { try { transaction.Rollback(); } catch { } // Rollback antes de retornar por validación return (false, $"El nuevo número de bobina '{updateDto.NroBobina}' ya existe."); } if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null) return (false, "Tipo de bobina inválido."); //if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null) // return (false, "Planta inválida."); bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina; bobinaExistente.NroBobina = updateDto.NroBobina; bobinaExistente.Peso = updateDto.Peso; //bobinaExistente.IdPlanta = updateDto.IdPlanta; //bobinaExistente.Remito = updateDto.Remito; //bobinaExistente.FechaRemito = updateDto.FechaRemito.Date; // FechaEstado se mantiene ya que el estado no cambia aquí var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados"); if (!actualizado) throw new DataException("Error al actualizar la bobina."); transaction.Commit(); return (true, null); } catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } catch (Exception ex) { try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error ActualizarDatosBobinaDisponibleAsync ID: {IdBobina}", idBobina); return (false, $"Error interno: {ex.Message}"); } } public async Task<(bool Exito, string? Error)> CambiarEstadoBobinaAsync(int idBobina, CambiarEstadoBobinaDto cambiarEstadoDto, int idUsuario) { using var connection = _connectionFactory.CreateConnection(); if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); using var transaction = connection.BeginTransaction(); try { var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción if (bobina == null) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId); if (nuevoEstado == null) { try { transaction.Rollback(); } catch { } return (false, "El nuevo estado especificado no es válido."); } // --- INICIO DE VALIDACIONES DE FLUJO DE ESTADOS --- if (bobina.IdEstadoBobina == cambiarEstadoDto.NuevoEstadoId) { try { transaction.Rollback(); } catch { } return (false, "La bobina ya se encuentra en ese estado."); } // Reglas específicas para salir de "Dañada" (ID 3) if (bobina.IdEstadoBobina == 3) // 3 = Dañada { if (cambiarEstadoDto.NuevoEstadoId != 1) // 1 = Disponible { try { transaction.Rollback(); } catch { } return (false, "Una bobina dañada solo puede ser revertida a 'Disponible' (si fue un error)."); } // Si se cambia de Dañada a Disponible, se limpiarán Publicacion y Seccion más abajo. } // Regla para salir de "Utilizada" (ID 2) else if (bobina.IdEstadoBobina == 2) // 2 = En Uso { if (cambiarEstadoDto.NuevoEstadoId == 1) // No se puede volver a Disponible directamente { try { transaction.Rollback(); } catch { } return (false, "Una bobina 'En Uso' no puede volver a 'Disponible' directamente mediante esta acción. Primero debe marcarse como Dañada si es necesario."); } // Si se cambia de Utilizada a cualquier otro estado que no sea Dañada (aunque aquí solo se permite Dañada), // y si el nuevo estado NO es En Uso, también se deberían limpiar Publicacion y Seccion. // La lógica actual ya lo hace si nuevoEstadoId no es 2. } // Regla para salir de "Disponible" (ID 1) else if (bobina.IdEstadoBobina == 1) // 1 = Disponible { // Desde Disponible puede pasar a En Uso (2) o Dañada (3). // El frontend ya debería filtrar esto, pero una validación aquí es buena. if (cambiarEstadoDto.NuevoEstadoId != 2 && cambiarEstadoDto.NuevoEstadoId != 3) { try { transaction.Rollback(); } catch { } return (false, "Desde 'Disponible', solo se puede cambiar a 'En Uso' o 'Dañada'."); } } // --- FIN VALIDACIONES DE FLUJO DE ESTADOS --- bobina.IdEstadoBobina = cambiarEstadoDto.NuevoEstadoId; bobina.FechaEstado = cambiarEstadoDto.FechaCambioEstado.Date; bobina.Obs = cambiarEstadoDto.Obs ?? bobina.Obs; if (cambiarEstadoDto.NuevoEstadoId == 2) // 2 = "En Uso" { if (!cambiarEstadoDto.IdPublicacion.HasValue || cambiarEstadoDto.IdPublicacion.Value <= 0) { try { transaction.Rollback(); } catch { } return (false, "Para el estado 'En Uso', se requiere Publicación."); } if (!cambiarEstadoDto.IdSeccion.HasValue || cambiarEstadoDto.IdSeccion.Value <= 0) { try { transaction.Rollback(); } catch { } return (false, "Para el estado 'En Uso', se requiere Sección."); } // Validar existencia de Publicación y Sección var publicacion = await _publicacionRepository.GetByIdSimpleAsync(cambiarEstadoDto.IdPublicacion.Value); if (publicacion == null) { try { transaction.Rollback(); } catch { } return (false, "Publicación inválida."); } // Asumiendo que GetByIdAsync en IPubliSeccionRepository valida si la sección pertenece a la publicación también var seccion = await _publiSeccionRepository.GetByIdAsync(cambiarEstadoDto.IdSeccion.Value); if (seccion == null || seccion.IdPublicacion != cambiarEstadoDto.IdPublicacion.Value) { try { transaction.Rollback(); } catch { } return (false, "Sección inválida o no pertenece a la publicación seleccionada."); } bobina.IdPublicacion = cambiarEstadoDto.IdPublicacion.Value; bobina.IdSeccion = cambiarEstadoDto.IdSeccion.Value; } else // Si el nuevo estado NO es "En Uso" (ej. Disponible, Dañada) { bobina.IdPublicacion = null; bobina.IdSeccion = null; // La observación se mantiene o se actualiza según venga en el DTO. // Si se pasa de "Dañada" a "Disponible", el DTO de cambio de estado debe incluir la nueva obs. } var actualizado = await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, $"Estado: {nuevoEstado.Denominacion}"); if (!actualizado) throw new DataException("Error al cambiar estado de la bobina."); transaction.Commit(); return (true, null); } catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } catch (Exception ex) { try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback CambiarEstadoBobinaAsync."); } _logger.LogError(ex, "Error CambiarEstadoBobinaAsync ID: {IdBobina}", idBobina); return (false, $"Error interno: {ex.Message}"); } } public async Task<(bool Exito, string? Error)> EliminarIngresoErroneoAsync(int idBobina, int idUsuario) { using var connection = _connectionFactory.CreateConnection(); if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); using var transaction = connection.BeginTransaction(); try { var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción if (bobina == null) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } // --- Permitir eliminar si está Disponible (1) o Dañada (3) --- if (bobina.IdEstadoBobina != 1 && bobina.IdEstadoBobina != 3) { try { transaction.Rollback(); } catch { } return (false, "Solo se pueden eliminar ingresos de bobinas que estén en estado 'Disponible' o 'Dañada'."); } var eliminado = await _stockBobinaRepository.DeleteAsync(idBobina, idUsuario, transaction); if (!eliminado) throw new DataException("Error al eliminar la bobina."); transaction.Commit(); return (true, null); } catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } catch (Exception ex) { try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback EliminarIngresoErroneoAsync."); } _logger.LogError(ex, "Error EliminarIngresoErroneoAsync ID: {IdBobina}", idBobina); return (false, $"Error interno: {ex.Message}"); } } public async Task> ObtenerHistorialAsync( DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModifico, string? tipoModificacion, int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro) { var historialData = await _stockBobinaRepository.GetHistorialDetalladoAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idBobinaAfectada, idTipoBobinaFiltro, idPlantaFiltro, idEstadoBobinaFiltro); return historialData.Select(h => new StockBobinaHistorialDto { Id_Bobina = h.Historial.Id_Bobina, Id_TipoBobina = h.Historial.Id_TipoBobina, NombreTipoBobina = h.NombreTipoBobina ?? "N/A", NroBobina = h.Historial.NroBobina, Peso = h.Historial.Peso, Id_Planta = h.Historial.Id_Planta, NombrePlanta = h.NombrePlanta ?? "N/A", Id_EstadoBobina = h.Historial.Id_EstadoBobina, NombreEstadoBobina = h.NombreEstadoBobina ?? "N/A", Remito = h.Historial.Remito, FechaRemito = h.Historial.FechaRemito, FechaEstado = h.Historial.FechaEstado, Id_Publicacion = h.Historial.Id_Publicacion, NombrePublicacion = h.NombrePublicacion, Id_Seccion = h.Historial.Id_Seccion, NombreSeccion = h.NombreSeccion, Obs = h.Historial.Obs, Id_Usuario = h.Historial.Id_Usuario, NombreUsuarioModifico = h.NombreUsuarioModifico, FechaMod = h.Historial.FechaMod, TipoMod = h.Historial.TipoMod }).ToList(); } public async Task> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito) { // Si la fecha tiene valor, filtramos por ese día exacto. Si no, busca en cualquier fecha. DateTime? fechaDesde = fechaRemito?.Date; DateTime? fechaHasta = fechaRemito?.Date; var bobinas = await _stockBobinaRepository.GetAllAsync(null, null, idPlanta, null, remito, fechaDesde, fechaHasta); var dtos = new List(); foreach (var bobina in bobinas) { dtos.Add(await MapToDto(bobina)); } return dtos; } public async Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario) { // 1. Buscar todas las bobinas que coinciden con el lote a modificar. var bobinasAActualizar = await _stockBobinaRepository.GetAllAsync( idTipoBobina: null, nroBobinaFilter: null, idPlanta: dto.IdPlanta, idEstadoBobina: null, remitoFilter: dto.Remito, fechaDesde: dto.FechaRemitoActual.Date, fechaHasta: dto.FechaRemitoActual.Date ); if (!bobinasAActualizar.Any()) { return (false, "No se encontraron bobinas para el remito, planta y fecha especificados. Es posible que ya hayan sido modificados."); } // 2. Iniciar una transacción para asegurar que todas las actualizaciones se completen o ninguna. using var connection = _connectionFactory.CreateConnection(); if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); using var transaction = connection.BeginTransaction(); try { // 3. Iterar sobre cada bobina y actualizarla. foreach (var bobina in bobinasAActualizar) { // Modificamos solo la fecha del remito. bobina.FechaRemito = dto.NuevaFechaRemito.Date; // Reutilizamos el método UpdateAsync que ya maneja la lógica de historial. // Le pasamos un mensaje específico para el historial. await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, "Fecha Remito Corregida"); } // 4. Si todo salió bien, confirmar la transacción. transaction.Commit(); _logger.LogInformation( "{Count} bobinas del remito {Remito} (Planta ID {IdPlanta}) actualizadas a nueva fecha {NuevaFecha} por Usuario ID {IdUsuario}.", bobinasAActualizar.Count(), dto.Remito, dto.IdPlanta, dto.NuevaFechaRemito.Date, idUsuario ); return (true, null); } catch (Exception ex) { // 5. Si algo falla, revertir todos los cambios. try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error transaccional al actualizar fecha de remito {Remito}.", dto.Remito); return (false, $"Error interno al actualizar el lote: {ex.Message}"); } } public async Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario) { // --- FASE 1: VALIDACIÓN PREVIA (FUERA DE LA TRANSACCIÓN) --- // Validación de la cabecera if (await _plantaRepository.GetByIdAsync(loteDto.IdPlanta) == null) return (false, "La planta especificada no es válida."); // Validación de cada bobina del lote foreach (var bobinaDetalle in loteDto.Bobinas) { if (await _tipoBobinaRepository.GetByIdAsync(bobinaDetalle.IdTipoBobina) == null) { return (false, $"El tipo de bobina con ID {bobinaDetalle.IdTipoBobina} no es válido."); } // Esta es la lectura que causaba el bloqueo. Ahora se hace ANTES de la transacción. if (await _stockBobinaRepository.GetByNroBobinaAsync(bobinaDetalle.NroBobina) != null) { return (false, $"El número de bobina '{bobinaDetalle.NroBobina}' ya existe en el sistema."); } } // Validación de números de bobina duplicados dentro del mismo lote var nrosBobinaEnLote = loteDto.Bobinas.Select(b => b.NroBobina.Trim()).ToList(); if (nrosBobinaEnLote.Count != nrosBobinaEnLote.Distinct().Count()) { var duplicado = nrosBobinaEnLote.GroupBy(n => n).Where(g => g.Count() > 1).First().Key; return (false, $"El número de bobina '{duplicado}' está duplicado en el lote que intenta ingresar."); } // --- FASE 2: ESCRITURA TRANSACCIONAL --- using var connection = _connectionFactory.CreateConnection(); if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); using var transaction = connection.BeginTransaction(); try { // Ahora este bucle solo contiene operaciones de escritura. No habrá bloqueos. foreach (var bobinaDetalle in loteDto.Bobinas) { var nuevaBobina = new StockBobina { IdTipoBobina = bobinaDetalle.IdTipoBobina, NroBobina = bobinaDetalle.NroBobina, Peso = bobinaDetalle.Peso, IdPlanta = loteDto.IdPlanta, Remito = loteDto.Remito, FechaRemito = loteDto.FechaRemito.Date, IdEstadoBobina = 1, // 1 = Disponible FechaEstado = loteDto.FechaRemito.Date, IdPublicacion = null, IdSeccion = null, Obs = null }; var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction); if (bobinaCreada == null) { throw new DataException($"No se pudo crear el registro para la bobina '{nuevaBobina.NroBobina}'."); } } transaction.Commit(); _logger.LogInformation("Lote de {Count} bobinas para remito {Remito} ingresado por Usuario ID {UserId}.", loteDto.Bobinas.Count, loteDto.Remito, idUsuario); return (true, null); } catch (Exception ex) { try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error al ingresar lote de bobinas para remito {Remito}", loteDto.Remito); return (false, $"Error interno al procesar el lote: {ex.Message}"); } } } }