using GestionIntegral.Api.Data; using GestionIntegral.Api.Data.Repositories.Contables; 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 EntradaSalidaDistService : IEntradaSalidaDistService { private readonly IEntradaSalidaDistRepository _esRepository; private readonly IPublicacionRepository _publicacionRepository; private readonly IDistribuidorRepository _distribuidorRepository; private readonly IPrecioRepository _precioRepository; private readonly IRecargoZonaRepository _recargoZonaRepository; private readonly IPorcPagoRepository _porcPagoRepository; private readonly ISaldoRepository _saldoRepository; private readonly IEmpresaRepository _empresaRepository; // Para obtener IdEmpresa de la publicación private readonly DbConnectionFactory _connectionFactory; private readonly ILogger _logger; public EntradaSalidaDistService( IEntradaSalidaDistRepository esRepository, IPublicacionRepository publicacionRepository, IDistribuidorRepository distribuidorRepository, IPrecioRepository precioRepository, IRecargoZonaRepository recargoZonaRepository, IPorcPagoRepository porcPagoRepository, ISaldoRepository saldoRepository, IEmpresaRepository empresaRepository, DbConnectionFactory connectionFactory, ILogger logger) { _esRepository = esRepository; _publicacionRepository = publicacionRepository; _distribuidorRepository = distribuidorRepository; _precioRepository = precioRepository; _recargoZonaRepository = recargoZonaRepository; _porcPagoRepository = porcPagoRepository; _saldoRepository = saldoRepository; _empresaRepository = empresaRepository; _connectionFactory = connectionFactory; _logger = logger; } private async Task MapToDto(EntradaSalidaDist es) { if (es == null) return null!; var publicacionData = await _publicacionRepository.GetByIdAsync(es.IdPublicacion); var distribuidorData = await _distribuidorRepository.GetByIdAsync(es.IdDistribuidor); decimal montoCalculado = await CalcularMontoMovimiento(es.IdPublicacion, es.IdDistribuidor, es.Fecha, es.Cantidad, es.TipoMovimiento, es.IdPrecio, es.IdRecargo, es.IdPorcentaje, distribuidorData.Distribuidor?.IdZona); return new EntradaSalidaDistDto { IdParte = es.IdParte, IdPublicacion = es.IdPublicacion, NombrePublicacion = publicacionData.Publicacion?.Nombre ?? "N/A", IdEmpresaPublicacion = publicacionData.Publicacion?.IdEmpresa ?? 0, NombreEmpresaPublicacion = publicacionData.NombreEmpresa ?? "N/A", IdDistribuidor = es.IdDistribuidor, NombreDistribuidor = distribuidorData.Distribuidor?.Nombre ?? "N/A", Fecha = es.Fecha.ToString("yyyy-MM-dd"), TipoMovimiento = es.TipoMovimiento, Cantidad = es.Cantidad, Remito = es.Remito, Observacion = es.Observacion, MontoCalculado = montoCalculado }; } private async Task CalcularMontoMovimiento(int idPublicacion, int idDistribuidor, DateTime fecha, int cantidad, string tipoMovimiento, int idPrecio, int idRecargo, int idPorcentaje, int? idZonaDistribuidor) { if (tipoMovimiento == "Entrada") return 0; // Las entradas (devoluciones) no generan "debe" inmediato, ajustan el saldo. var precioConfig = await _precioRepository.GetByIdAsync(idPrecio); if (precioConfig == null) throw new InvalidOperationException("Configuración de precio no encontrada para el movimiento."); decimal precioDia = 0; DayOfWeek diaSemana = 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 (idRecargo > 0 && idZonaDistribuidor.HasValue) // El recargo se aplica por la zona del distribuidor { // Necesitamos encontrar el recargo activo para la publicación, la zona del distribuidor y la fecha var recargoConfig = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(idPublicacion, idZonaDistribuidor.Value, fecha); if (recargoConfig != null) { valorRecargo = recargoConfig.Valor; } } decimal precioConRecargo = precioDia + valorRecargo; decimal montoBase = precioConRecargo * cantidad; if (idPorcentaje > 0) { var porcConfig = await _porcPagoRepository.GetByIdAsync(idPorcentaje); if (porcConfig != null) { return (montoBase / 100) * porcConfig.Porcentaje; } } return montoBase; // Si no hay porcentaje, se factura el 100% del precio con recargo } public async Task> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDistribuidor, string? tipoMovimiento) { var movimientos = await _esRepository.GetAllAsync(fechaDesde, fechaHasta, idPublicacion, idDistribuidor, tipoMovimiento); var dtos = new List(); foreach (var mov in movimientos) { dtos.Add(await MapToDto(mov)); } return dtos; } public async Task ObtenerPorIdAsync(int idParte) { var movimiento = await _esRepository.GetByIdAsync(idParte); return movimiento == null ? null : await MapToDto(movimiento); } public async Task<(EntradaSalidaDistDto? Movimiento, string? Error)> CrearMovimientoAsync(CreateEntradaSalidaDistDto createDto, int idUsuario) { var publicacion = await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion); if (publicacion == null || publicacion.Habilitada != true) return (null, "Publicación no válida o no habilitada."); var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor); if (distribuidor == null) return (null, "Distribuidor no válido."); if (await _esRepository.ExistsByRemitoAndTipoForPublicacionAsync(createDto.Remito, createDto.TipoMovimiento, createDto.IdPublicacion)) { return (null, $"Ya existe un movimiento de '{createDto.TipoMovimiento}' con el remito N°{createDto.Remito} para esta publicación."); } // Determinar IDs de Precio, Recargo y Porcentaje activos en la fecha del movimiento var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.Fecha.Date); if (precioActivo == null) return (null, $"No hay un precio definido para la publicación '{publicacion.Nombre}' en la fecha {createDto.Fecha:dd/MM/yyyy}."); RecargoZona? recargoActivo = null; if (distribuidor.IdZona.HasValue) { recargoActivo = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(createDto.IdPublicacion, distribuidor.IdZona.Value, createDto.Fecha.Date); } var porcPagoActivo = await _porcPagoRepository.GetActiveByPublicacionDistribuidorAndDateAsync(createDto.IdPublicacion, createDto.IdDistribuidor, createDto.Fecha.Date); var nuevoES = new EntradaSalidaDist { IdPublicacion = createDto.IdPublicacion, IdDistribuidor = createDto.IdDistribuidor, Fecha = createDto.Fecha.Date, TipoMovimiento = createDto.TipoMovimiento, Cantidad = createDto.Cantidad, Remito = createDto.Remito, Observacion = createDto.Observacion, IdPrecio = precioActivo.IdPrecio, IdRecargo = recargoActivo?.IdRecargo ?? 0, // 0 si no aplica IdPorcentaje = porcPagoActivo?.IdPorcentaje ?? 0 // 0 si no aplica }; 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 esCreada = await _esRepository.CreateAsync(nuevoES, idUsuario, transaction); if (esCreada == null) throw new DataException("Error al registrar el movimiento."); // Afectar Saldo decimal montoAfectacion = await CalcularMontoMovimiento( esCreada.IdPublicacion, esCreada.IdDistribuidor, esCreada.Fecha, esCreada.Cantidad, esCreada.TipoMovimiento, esCreada.IdPrecio, esCreada.IdRecargo, esCreada.IdPorcentaje, distribuidor.IdZona); // Si es Salida, montoAfectacion es positivo (aumenta deuda). Si es Entrada, es 0 por CalcularMontoMovimiento, // pero para el saldo, una Entrada (devolución) debería restar del saldo deudor. // Por lo tanto, el monto real a restar del saldo si es Entrada sería el valor de esos ejemplares devueltos. if(esCreada.TipoMovimiento == "Entrada") { // Recalcular como si fuera salida para obtener el valor a acreditar/restar del saldo montoAfectacion = await CalcularMontoMovimiento( esCreada.IdPublicacion, esCreada.IdDistribuidor, esCreada.Fecha, esCreada.Cantidad, "Salida", // Forzar tipo Salida para cálculo de valor esCreada.IdPrecio, esCreada.IdRecargo, esCreada.IdPorcentaje, distribuidor.IdZona); montoAfectacion *= -1; // Hacerlo negativo para restar del saldo } bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esCreada.IdDistribuidor, publicacion.IdEmpresa, montoAfectacion, transaction); if (!saldoActualizado) throw new DataException("Error al actualizar el saldo del distribuidor."); transaction.Commit(); _logger.LogInformation("Movimiento ID {Id} creado y saldo afectado por Usuario ID {UserId}.", esCreada.IdParte, idUsuario); return (await MapToDto(esCreada), null); } catch (Exception ex) { try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error CrearMovimientoAsync para Pub ID {IdPub}", createDto.IdPublicacion); return (null, $"Error interno: {ex.Message}"); } } public async Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaDistDto updateDto, int idUsuario) { // La actualización de un movimiento que afecta saldos es compleja. // Si cambia la cantidad, el monto original y el nuevo deben calcularse, // y la diferencia debe aplicarse al saldo. // Por ahora, este DTO solo permite cambiar Cantidad y Observacion. // Cambiar otros campos como Fecha, Publicacion, Distribuidor implicaría recalcular todo // y posiblemente anular el movimiento original y crear uno nuevo. 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 esExistente = await _esRepository.GetByIdAsync(idParte); if (esExistente == null) return (false, "Movimiento no encontrado."); var publicacion = await _publicacionRepository.GetByIdSimpleAsync(esExistente.IdPublicacion); if (publicacion == null) return (false, "Publicación asociada no encontrada."); // Muy raro var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor); if (distribuidor == null) return (false, "Distribuidor asociado no encontrado."); // 1. Calcular monto del movimiento original (antes de la actualización) decimal montoOriginal = await CalcularMontoMovimiento( esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, esExistente.TipoMovimiento, esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona); if(esExistente.TipoMovimiento == "Entrada") montoOriginal *= -1; // Para revertir // 2. Actualizar la entidad en la BD (esto también guarda en historial) var esParaActualizar = new EntradaSalidaDist { IdParte = esExistente.IdParte, IdPublicacion = esExistente.IdPublicacion, // No cambia IdDistribuidor = esExistente.IdDistribuidor, // No cambia Fecha = esExistente.Fecha, // No cambia TipoMovimiento = esExistente.TipoMovimiento, // No cambia Cantidad = updateDto.Cantidad, // Nuevo valor Remito = esExistente.Remito, // No cambia Observacion = updateDto.Observacion, // Nuevo valor IdPrecio = esExistente.IdPrecio, // No cambia IdRecargo = esExistente.IdRecargo, // No cambia IdPorcentaje = esExistente.IdPorcentaje // No cambia }; var actualizado = await _esRepository.UpdateAsync(esParaActualizar, idUsuario, transaction); if (!actualizado) throw new DataException("Error al actualizar el movimiento."); // 3. Calcular monto del movimiento nuevo (después de la actualización) decimal montoNuevo = await CalcularMontoMovimiento( esParaActualizar.IdPublicacion, esParaActualizar.IdDistribuidor, esParaActualizar.Fecha, esParaActualizar.Cantidad, esParaActualizar.TipoMovimiento, esParaActualizar.IdPrecio, esParaActualizar.IdRecargo, esParaActualizar.IdPorcentaje, distribuidor.IdZona); if(esParaActualizar.TipoMovimiento == "Entrada") montoNuevo *= -1; // 4. Ajustar saldo con la diferencia decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal; if (diferenciaAjusteSaldo != 0) { bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, diferenciaAjusteSaldo, transaction); if (!saldoActualizado) throw new DataException("Error al ajustar el saldo del distribuidor tras la actualización."); } transaction.Commit(); _logger.LogInformation("Movimiento ID {Id} actualizado y saldo ajustado por Usuario ID {UserId}.", idParte, idUsuario); return (true, null); } catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); } catch (Exception ex) { try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error ActualizarMovimientoAsync ID: {Id}", idParte); return (false, $"Error interno: {ex.Message}"); } } public async Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, 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 esExistente = await _esRepository.GetByIdAsync(idParte); if (esExistente == null) return (false, "Movimiento no encontrado."); var publicacion = await _publicacionRepository.GetByIdSimpleAsync(esExistente.IdPublicacion); if (publicacion == null) return (false, "Publicación asociada no encontrada."); var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor); if (distribuidor == null) return (false, "Distribuidor asociado no encontrado."); // 1. Calcular el monto del movimiento a eliminar para revertir el saldo decimal montoReversion = await CalcularMontoMovimiento( esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, esExistente.TipoMovimiento, esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona); // Si es Salida, el monto es positivo, al revertir restamos. // Si es Entrada, el monto es 0 (por Calcular...), pero su efecto en saldo fue negativo, al revertir sumamos el valor. if(esExistente.TipoMovimiento == "Salida") { montoReversion *= -1; // Para restar del saldo lo que se sumó } else if (esExistente.TipoMovimiento == "Entrada") { // Recalcular el valor como si fuera salida para saber cuánto se restó del saldo montoReversion = await CalcularMontoMovimiento( esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, "Salida", esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona); // No se multiplica por -1 aquí, porque el saldo ya lo tiene restado, al eliminar revertimos sumando. } // 2. Eliminar el registro (esto también guarda en historial) var eliminado = await _esRepository.DeleteAsync(idParte, idUsuario, transaction); if (!eliminado) throw new DataException("Error al eliminar el movimiento."); // 3. Ajustar Saldo bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, montoReversion, transaction); if (!saldoActualizado) throw new DataException("Error al revertir el saldo del distribuidor tras la eliminación."); transaction.Commit(); _logger.LogInformation("Movimiento ID {Id} eliminado y saldo revertido por Usuario ID {UserId}.", idParte, idUsuario); return (true, null); } catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); } catch (Exception ex) { try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error EliminarMovimientoAsync ID: {Id}", idParte); return (false, $"Error interno: {ex.Message}"); } } } }