using GestionIntegral.Api.Data; using GestionIntegral.Api.Data.Repositories.Distribucion; using GestionIntegral.Api.Dtos.Auditoria; 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; namespace GestionIntegral.Api.Services.Distribucion { public class PrecioService : IPrecioService { private readonly IPrecioRepository _precioRepository; private readonly IPublicacionRepository _publicacionRepository; // Para validar IdPublicacion private readonly DbConnectionFactory _connectionFactory; private readonly ILogger _logger; public PrecioService( IPrecioRepository precioRepository, IPublicacionRepository publicacionRepository, DbConnectionFactory connectionFactory, ILogger logger) { _precioRepository = precioRepository; _publicacionRepository = publicacionRepository; _connectionFactory = connectionFactory; _logger = logger; } private PrecioDto MapToDto(Precio precio) => new PrecioDto { IdPrecio = precio.IdPrecio, IdPublicacion = precio.IdPublicacion, VigenciaD = precio.VigenciaD.ToString("yyyy-MM-dd"), VigenciaH = precio.VigenciaH?.ToString("yyyy-MM-dd"), Lunes = precio.Lunes, Martes = precio.Martes, Miercoles = precio.Miercoles, Jueves = precio.Jueves, Viernes = precio.Viernes, Sabado = precio.Sabado, Domingo = precio.Domingo }; public async Task> ObtenerPorPublicacionIdAsync(int idPublicacion) { var precios = await _precioRepository.GetByPublicacionIdAsync(idPublicacion); return precios.Select(MapToDto); } public async Task ObtenerPorIdAsync(int idPrecio) { var precio = await _precioRepository.GetByIdAsync(idPrecio); return precio == null ? null : MapToDto(precio); } public async Task<(PrecioDto? Precio, string? Error)> CrearAsync(CreatePrecioDto createDto, int idUsuario) { if (await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion) == null) return (null, "La publicación especificada no existe."); // Validar que no haya solapamiento de fechas o que VigenciaD sea lógica // Un precio no puede empezar antes que el VigenciaH de un precio anterior no cerrado. // Y no puede empezar en una fecha donde ya existe un precio activo para esa publicación. var precioActivoEnFecha = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.VigenciaD); if (precioActivoEnFecha != null) { return (null, $"Ya existe un período de precios activo para esta publicación en la fecha {createDto.VigenciaD:dd/MM/yyyy}. Primero debe cerrar el período anterior."); } var nuevoPrecio = new Precio { IdPublicacion = createDto.IdPublicacion, VigenciaD = createDto.VigenciaD.Date, // Asegurar que solo sea fecha VigenciaH = null, // Se establece al crear el siguiente o al cerrar manualmente Lunes = createDto.Lunes ?? 0, Martes = createDto.Martes ?? 0, Miercoles = createDto.Miercoles ?? 0, Jueves = createDto.Jueves ?? 0, Viernes = createDto.Viernes ?? 0, Sabado = createDto.Sabado ?? 0, Domingo = createDto.Domingo ?? 0 }; 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 { // 1. Buscar el precio anterior activo para esta publicación que no tenga VigenciaH var precioAnterior = await _precioRepository.GetPreviousActivePriceAsync(createDto.IdPublicacion, nuevoPrecio.VigenciaD, transaction); if (precioAnterior != null) { if (precioAnterior.VigenciaD >= nuevoPrecio.VigenciaD) { transaction.Rollback(); return (null, $"La fecha de inicio del nuevo precio ({nuevoPrecio.VigenciaD:dd/MM/yyyy}) no puede ser anterior o igual a la del último precio vigente ({precioAnterior.VigenciaD:dd/MM/yyyy})."); } // 2. Si existe, actualizar su VigenciaH al día anterior de la VigenciaD del nuevo precio precioAnterior.VigenciaH = nuevoPrecio.VigenciaD.AddDays(-1); bool actualizado = await _precioRepository.UpdateAsync(precioAnterior, idUsuario, transaction); // Usar un idUsuario de sistema/auditoría para esta acción automática si es necesario if (!actualizado) { throw new DataException("No se pudo cerrar el período de precio anterior."); } _logger.LogInformation("Precio anterior ID {IdPrecioAnterior} cerrado con VigenciaH {VigenciaH}.", precioAnterior.IdPrecio, precioAnterior.VigenciaH); } // 3. Crear el nuevo registro de precio var precioCreado = await _precioRepository.CreateAsync(nuevoPrecio, idUsuario, transaction); if (precioCreado == null) throw new DataException("Error al crear el nuevo precio."); transaction.Commit(); _logger.LogInformation("Precio ID {IdPrecio} creado para Publicación ID {IdPublicacion} por Usuario ID {IdUsuario}.", precioCreado.IdPrecio, precioCreado.IdPublicacion, idUsuario); return (MapToDto(precioCreado), null); } catch (Exception ex) { try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error CrearAsync Precio para Publicación ID {IdPublicacion}", createDto.IdPublicacion); return (null, $"Error interno al crear el precio: {ex.Message}"); } } public async Task<(bool Exito, string? Error)> ActualizarAsync(int idPrecio, UpdatePrecioDto 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 precioExistente = await _precioRepository.GetByIdAsync(idPrecio); // Obtener dentro de TX if (precioExistente == null) { transaction.Rollback(); return (false, "Período de precio no encontrado."); } bool preciosDiasCambiaron = (updateDto.Lunes.HasValue && updateDto.Lunes != precioExistente.Lunes) || (updateDto.Martes.HasValue && updateDto.Martes != precioExistente.Martes) || (updateDto.Miercoles.HasValue && updateDto.Miercoles != precioExistente.Miercoles) || (updateDto.Jueves.HasValue && updateDto.Jueves != precioExistente.Jueves) || (updateDto.Viernes.HasValue && updateDto.Viernes != precioExistente.Viernes) || (updateDto.Sabado.HasValue && updateDto.Sabado != precioExistente.Sabado) || (updateDto.Domingo.HasValue && updateDto.Domingo != precioExistente.Domingo); if (precioExistente.VigenciaH != null && preciosDiasCambiaron) { transaction.Rollback(); return (false, "No se pueden modificar los precios de los días de un período que ya ha sido cerrado."); } // Validación 2: No modificar precios de días si el período está en uso if (preciosDiasCambiaron && await _precioRepository.IsInUseAsync(idPrecio, precioExistente.VigenciaD, precioExistente.VigenciaH)) { transaction.Rollback(); return (false, "No se pueden modificar los precios de los días de este período porque ya existen movimientos asociados."); } // Solo realizar validaciones de VigenciaH si se está intentando SETEAR o CAMBIAR VigenciaH if (updateDto.VigenciaH.HasValue) // <<--- CHEQUEO IMPORTANTE { // Validación 3: Nueva VigenciaH debe ser >= VigenciaD if (updateDto.VigenciaH.Value.Date < precioExistente.VigenciaD.Date) { transaction.Rollback(); return (false, "La Vigencia Hasta no puede ser anterior a la Vigencia Desde."); } // Validación 4: Nueva VigenciaH no debe solaparse con períodos posteriores var todosLosPreciosPub = await _precioRepository.GetByPublicacionIdAsync(precioExistente.IdPublicacion); if (todosLosPreciosPub.Any(p => p.IdPrecio != idPrecio && p.VigenciaD.Date <= updateDto.VigenciaH.Value.Date && p.VigenciaD.Date > precioExistente.VigenciaD.Date && (p.VigenciaH == null || p.VigenciaH.Value.Date >= p.VigenciaD.Date) )) { transaction.Rollback(); return (false, "No se puede cerrar este período con la Vigencia Hasta especificada porque se solaparía con un período posterior existente. Ajuste los períodos posteriores primero."); } // Si pasa las validaciones, se asigna la nueva VigenciaH más abajo } // Si updateDto.VigenciaH es null, estas validaciones de fecha no aplican. // Aplicar actualizaciones bool seRealizoAlgunaActualizacion = false; // Aplicar actualizaciones if (preciosDiasCambiaron) { precioExistente.Lunes = updateDto.Lunes ?? precioExistente.Lunes; precioExistente.Martes = updateDto.Martes ?? precioExistente.Martes; precioExistente.Miercoles = updateDto.Miercoles ?? precioExistente.Miercoles; precioExistente.Jueves = updateDto.Jueves ?? precioExistente.Jueves; precioExistente.Viernes = updateDto.Viernes ?? precioExistente.Viernes; precioExistente.Sabado = updateDto.Sabado ?? precioExistente.Sabado; precioExistente.Domingo = updateDto.Domingo ?? precioExistente.Domingo; seRealizoAlgunaActualizacion = true; } // Actualizar VigenciaH solo si el valor recibido en el DTO es diferente al existente, // o si se quiere pasar de un valor a null (reabrir) o de null a un valor (cerrar). if ((updateDto.VigenciaH.HasValue && (!precioExistente.VigenciaH.HasValue || updateDto.VigenciaH.Value.Date != precioExistente.VigenciaH.Value.Date)) || (!updateDto.VigenciaH.HasValue && precioExistente.VigenciaH.HasValue)) { precioExistente.VigenciaH = updateDto.VigenciaH.HasValue ? updateDto.VigenciaH.Value.Date : (DateTime?)null; seRealizoAlgunaActualizacion = true; } if (seRealizoAlgunaActualizacion) { var actualizado = await _precioRepository.UpdateAsync(precioExistente, idUsuario, transaction); if (!actualizado) throw new DataException("Error al actualizar el período de precio."); } else { _logger.LogInformation("No se detectaron cambios para Precio ID {IdPrecio}. No se realizó actualización.", idPrecio); } transaction.Commit(); _logger.LogInformation("Precio ID {IdPrecio} procesado (actualizado si hubo cambios) por Usuario ID {IdUsuario}.", idPrecio, idUsuario); return (true, null); } catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch { } return (false, "Período de precio no encontrado."); } catch (Exception ex) { try { transaction?.Rollback(); } catch { } _logger.LogError(ex, "Error ActualizarAsync Precio ID: {IdPrecio}", idPrecio); return (false, $"Error interno al actualizar el período de precio: {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)> EliminarAsync(int idPrecio, 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 precioAEliminar = await _precioRepository.GetByIdAsync(idPrecio); // Obtener dentro de TX if (precioAEliminar == null) return (false, "Período de precio no encontrado."); // --- VERIFICACIÓN DE USO --- if (await _precioRepository.IsInUseAsync(idPrecio, precioAEliminar.VigenciaD, precioAEliminar.VigenciaH)) { transaction.Rollback(); // No olvidar rollback si la verificación falla return (false, "No se puede eliminar. Este período de precio está referenciado en movimientos existentes."); } if (precioAEliminar.VigenciaH == null) { var todosLosPreciosPub = (await _precioRepository.GetByPublicacionIdAsync(precioAEliminar.IdPublicacion)) .OrderByDescending(p => p.VigenciaD).ToList(); var indiceActual = todosLosPreciosPub.FindIndex(p => p.IdPrecio == idPrecio); if (indiceActual != -1 && (indiceActual + 1) < todosLosPreciosPub.Count) { var precioAnteriorDirecto = todosLosPreciosPub[indiceActual + 1]; if (precioAnteriorDirecto.VigenciaH.HasValue && precioAnteriorDirecto.VigenciaH.Value.Date == precioAEliminar.VigenciaD.AddDays(-1).Date) { precioAnteriorDirecto.VigenciaH = null; // Asegurar que UpdateAsync tome la transacción await _precioRepository.UpdateAsync(precioAnteriorDirecto, idUsuario, transaction); _logger.LogInformation("Precio anterior ID {IdPrecioAnterior} reabierto tras eliminación de Precio ID {IdPrecioEliminado}.", precioAnteriorDirecto.IdPrecio, idPrecio); } } } var eliminado = await _precioRepository.DeleteAsync(idPrecio, idUsuario, transaction); if (!eliminado) throw new DataException("Error al eliminar el período de precio."); transaction.Commit(); _logger.LogInformation("Precio ID {IdPrecio} eliminado por Usuario ID {IdUsuario}.", idPrecio, idUsuario); return (true, null); } catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Período de precio no encontrado."); } catch (Exception ex) { try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error EliminarAsync Precio ID: {IdPrecio}", idPrecio); return (false, $"Error interno al eliminar el período de precio: {ex.Message}"); } } public async Task> ObtenerHistorialAsync( DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModifico, string? tipoModificacion, int? idPrecioAfectado, int? idPublicacionAfectada) { var historialData = await _precioRepository.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPrecioAfectado, idPublicacionAfectada); return historialData.Select(h => new PrecioHistorialDto { Id_Precio = h.Historial.Id_Precio, Id_Publicacion = h.Historial.Id_Publicacion, VigenciaD = h.Historial.VigenciaD, VigenciaH = h.Historial.VigenciaH, Lunes = h.Historial.Lunes, Martes = h.Historial.Martes, Miercoles = h.Historial.Miercoles, Jueves = h.Historial.Jueves, Viernes = h.Historial.Viernes, Sabado = h.Historial.Sabado, Domingo = h.Historial.Domingo, Id_Usuario = h.Historial.Id_Usuario, NombreUsuarioModifico = h.NombreUsuarioModifico, FechaMod = h.Historial.FechaMod, TipoMod = h.Historial.TipoMod }).ToList(); } } }