using GestionIntegral.Api.Data; using GestionIntegral.Api.Data.Repositories.Distribucion; 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 la TX por si acaso if (precioExistente == null) return (false, "Período de precio no encontrado."); // En una actualización, principalmente actualizamos los montos de los días. // VigenciaH se puede actualizar para cerrar un período explícitamente. // No se permite cambiar IdPublicacion ni VigenciaD aquí. // Si VigenciaH se establece, debe ser >= VigenciaD if (updateDto.VigenciaH.HasValue && updateDto.VigenciaH.Value.Date < precioExistente.VigenciaD.Date) { return (false, "La Vigencia Hasta no puede ser anterior a la Vigencia Desde."); } // Adicional: si se establece VigenciaH, verificar que no haya precios posteriores que se solapen if (updateDto.VigenciaH.HasValue) { var preciosPosteriores = await _precioRepository.GetByPublicacionIdAsync(precioExistente.IdPublicacion); if (preciosPosteriores.Any(p => p.IdPrecio != idPrecio && p.VigenciaD.Date <= updateDto.VigenciaH.Value.Date && p.VigenciaD.Date > precioExistente.VigenciaD.Date )) { return (false, "No se puede cerrar este período porque existen períodos de precios posteriores que se solaparían. Elimine o ajuste los períodos posteriores primero."); } } 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; if (updateDto.VigenciaH.HasValue) // Solo actualizar VigenciaH si se proporciona { precioExistente.VigenciaH = updateDto.VigenciaH.Value.Date; } var actualizado = await _precioRepository.UpdateAsync(precioExistente, idUsuario, transaction); if (!actualizado) throw new DataException("Error al actualizar el período de precio."); transaction.Commit(); _logger.LogInformation("Precio ID {IdPrecio} actualizado 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}"); } } 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); if (precioAEliminar == null) return (false, "Período de precio no encontrado."); // Lógica de ajuste de VigenciaH del período anterior si este que se elimina era el "último" abierto. // Si el precio a eliminar tiene un VigenciaH == null (estaba activo indefinidamente) // Y existe un precio anterior para la misma publicación. // Entonces, el VigenciaH de ese precio anterior debe volver a ser NULL. 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]; // Solo si el precioAnteriorDirecto fue cerrado por este que se elimina if(precioAnteriorDirecto.VigenciaH.HasValue && precioAnteriorDirecto.VigenciaH.Value.Date == precioAEliminar.VigenciaD.AddDays(-1).Date) { precioAnteriorDirecto.VigenciaH = null; await _precioRepository.UpdateAsync(precioAnteriorDirecto, idUsuario, transaction); // Usar un ID de auditoría adecuado _logger.LogInformation("Precio anterior ID {IdPrecioAnterior} reabierto (VigenciaH a NULL) 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}"); } } } }