Ya perdí el hilo de los cambios pero ahi van.

This commit is contained in:
2025-05-23 15:47:39 -03:00
parent e7e185a9cb
commit 3c1fe15b1f
141 changed files with 9764 additions and 190 deletions

View File

@@ -0,0 +1,19 @@
using GestionIntegral.Api.Dtos.Contables;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Contables
{
public interface INotaCreditoDebitoService
{
Task<IEnumerable<NotaCreditoDebitoDto>> ObtenerTodosAsync(
DateTime? fechaDesde, DateTime? fechaHasta,
string? destino, int? idDestino, int? idEmpresa, string? tipoNota);
Task<NotaCreditoDebitoDto?> ObtenerPorIdAsync(int idNota);
Task<(NotaCreditoDebitoDto? Nota, string? Error)> CrearAsync(CreateNotaDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int idNota, UpdateNotaDto updateDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarAsync(int idNota, int idUsuario);
}
}

View File

@@ -0,0 +1,19 @@
using GestionIntegral.Api.Dtos.Contables;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Contables
{
public interface IPagoDistribuidorService
{
Task<IEnumerable<PagoDistribuidorDto>> ObtenerTodosAsync(
DateTime? fechaDesde, DateTime? fechaHasta,
int? idDistribuidor, int? idEmpresa, string? tipoMovimiento);
Task<PagoDistribuidorDto?> ObtenerPorIdAsync(int idPago);
Task<(PagoDistribuidorDto? Pago, string? Error)> CrearAsync(CreatePagoDistribuidorDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int idPago, UpdatePagoDistribuidorDto updateDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarAsync(int idPago, int idUsuario);
}
}

View File

@@ -0,0 +1,226 @@
using GestionIntegral.Api.Data;
using GestionIntegral.Api.Data.Repositories.Contables;
using GestionIntegral.Api.Data.Repositories.Distribucion;
using GestionIntegral.Api.Dtos.Contables;
using GestionIntegral.Api.Models.Contables;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Contables
{
public class NotaCreditoDebitoService : INotaCreditoDebitoService
{
private readonly INotaCreditoDebitoRepository _notaRepo;
private readonly IDistribuidorRepository _distribuidorRepo;
private readonly ICanillaRepository _canillaRepo;
private readonly IEmpresaRepository _empresaRepo;
private readonly ISaldoRepository _saldoRepo;
private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<NotaCreditoDebitoService> _logger;
public NotaCreditoDebitoService(
INotaCreditoDebitoRepository notaRepo,
IDistribuidorRepository distribuidorRepo,
ICanillaRepository canillaRepo,
IEmpresaRepository empresaRepo,
ISaldoRepository saldoRepo,
DbConnectionFactory connectionFactory,
ILogger<NotaCreditoDebitoService> logger)
{
_notaRepo = notaRepo;
_distribuidorRepo = distribuidorRepo;
_canillaRepo = canillaRepo;
_empresaRepo = empresaRepo;
_saldoRepo = saldoRepo;
_connectionFactory = connectionFactory;
_logger = logger;
}
private async Task<NotaCreditoDebitoDto> MapToDto(NotaCreditoDebito nota)
{
if (nota == null) return null!;
string nombreDestinatario = "N/A";
if (nota.Destino == "Distribuidores")
{
var distData = await _distribuidorRepo.GetByIdAsync(nota.IdDestino);
nombreDestinatario = distData.Distribuidor?.Nombre ?? "Distribuidor Desconocido";
}
else if (nota.Destino == "Canillas")
{
var canData = await _canillaRepo.GetByIdAsync(nota.IdDestino); // Asumiendo que GetByIdAsync devuelve una tupla
nombreDestinatario = canData.Canilla?.NomApe ?? "Canillita Desconocido";
}
var empresa = await _empresaRepo.GetByIdAsync(nota.IdEmpresa);
return new NotaCreditoDebitoDto
{
IdNota = nota.IdNota,
Destino = nota.Destino,
IdDestino = nota.IdDestino,
NombreDestinatario = nombreDestinatario,
Referencia = nota.Referencia,
Tipo = nota.Tipo,
Fecha = nota.Fecha.ToString("yyyy-MM-dd"),
Monto = nota.Monto,
Observaciones = nota.Observaciones,
IdEmpresa = nota.IdEmpresa,
NombreEmpresa = empresa?.Nombre ?? "Empresa Desconocida"
};
}
public async Task<IEnumerable<NotaCreditoDebitoDto>> ObtenerTodosAsync(
DateTime? fechaDesde, DateTime? fechaHasta,
string? destino, int? idDestino, int? idEmpresa, string? tipoNota)
{
var notas = await _notaRepo.GetAllAsync(fechaDesde, fechaHasta, destino, idDestino, idEmpresa, tipoNota);
var dtos = new List<NotaCreditoDebitoDto>();
foreach (var nota in notas)
{
dtos.Add(await MapToDto(nota));
}
return dtos;
}
public async Task<NotaCreditoDebitoDto?> ObtenerPorIdAsync(int idNota)
{
var nota = await _notaRepo.GetByIdAsync(idNota);
return nota == null ? null : await MapToDto(nota);
}
public async Task<(NotaCreditoDebitoDto? Nota, string? Error)> CrearAsync(CreateNotaDto createDto, int idUsuario)
{
// Validar Destinatario
if (createDto.Destino == "Distribuidores")
{
if (await _distribuidorRepo.GetByIdSimpleAsync(createDto.IdDestino) == null)
return (null, "El distribuidor especificado no existe.");
}
else if (createDto.Destino == "Canillas")
{
if (await _canillaRepo.GetByIdSimpleAsync(createDto.IdDestino) == null) // Asumiendo GetByIdSimpleAsync en ICanillaRepository
return (null, "El canillita especificado no existe.");
}
else { return (null, "Tipo de destino inválido."); }
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
return (null, "La empresa especificada no existe.");
var nuevaNota = new NotaCreditoDebito
{
Destino = createDto.Destino,
IdDestino = createDto.IdDestino,
Referencia = createDto.Referencia,
Tipo = createDto.Tipo,
Fecha = createDto.Fecha.Date,
Monto = createDto.Monto,
Observaciones = createDto.Observaciones,
IdEmpresa = createDto.IdEmpresa
};
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 notaCreada = await _notaRepo.CreateAsync(nuevaNota, idUsuario, transaction);
if (notaCreada == null) throw new DataException("Error al registrar la nota.");
// Afectar Saldo
// Nota de Crédito: Disminuye la deuda del destinatario (monto positivo para el servicio de saldo)
// Nota de Débito: Aumenta la deuda del destinatario (monto negativo para el servicio de saldo)
decimal montoAjusteSaldo = createDto.Tipo == "Credito" ? createDto.Monto : -createDto.Monto;
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaCreada.Destino, notaCreada.IdDestino, notaCreada.IdEmpresa, montoAjusteSaldo, transaction);
if (!saldoActualizado) throw new DataException($"Error al actualizar el saldo para {notaCreada.Destino} ID {notaCreada.IdDestino}.");
transaction.Commit();
_logger.LogInformation("NotaC/D ID {Id} creada y saldo afectado por Usuario ID {UserId}.", notaCreada.IdNota, idUsuario);
return (await MapToDto(notaCreada), null);
}
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error CrearAsync NotaCreditoDebito.");
return (null, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> ActualizarAsync(int idNota, UpdateNotaDto 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 notaExistente = await _notaRepo.GetByIdAsync(idNota);
if (notaExistente == null) return (false, "Nota no encontrada.");
// Calcular diferencia de monto para ajustar saldo
decimal montoOriginal = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto;
decimal montoNuevo = notaExistente.Tipo == "Credito" ? updateDto.Monto : -updateDto.Monto; // Tipo no cambia
decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal;
notaExistente.Monto = updateDto.Monto;
notaExistente.Observaciones = updateDto.Observaciones;
var actualizado = await _notaRepo.UpdateAsync(notaExistente, idUsuario, transaction);
if (!actualizado) throw new DataException("Error al actualizar la nota.");
if (diferenciaAjusteSaldo != 0)
{
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, diferenciaAjusteSaldo, transaction);
if (!saldoActualizado) throw new DataException("Error al ajustar el saldo tras la actualización de la nota.");
}
transaction.Commit();
_logger.LogInformation("NotaC/D ID {Id} actualizada por Usuario ID {UserId}.", idNota, idUsuario);
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Nota no encontrada."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error ActualizarAsync NotaC/D ID: {Id}", idNota);
return (false, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> EliminarAsync(int idNota, 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 notaExistente = await _notaRepo.GetByIdAsync(idNota);
if (notaExistente == null) return (false, "Nota no encontrada.");
// Revertir el efecto en el saldo
decimal montoReversion = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto;
var eliminado = await _notaRepo.DeleteAsync(idNota, idUsuario, transaction);
if (!eliminado) throw new DataException("Error al eliminar la nota.");
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, montoReversion, transaction);
if (!saldoActualizado) throw new DataException("Error al revertir el saldo tras la eliminación de la nota.");
transaction.Commit();
_logger.LogInformation("NotaC/D ID {Id} eliminada y saldo revertido por Usuario ID {UserId}.", idNota, idUsuario);
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Nota no encontrada."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error EliminarAsync NotaC/D ID: {Id}", idNota);
return (false, $"Error interno: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,218 @@
using GestionIntegral.Api.Data;
using GestionIntegral.Api.Data.Repositories.Contables;
using GestionIntegral.Api.Data.Repositories.Distribucion;
using GestionIntegral.Api.Dtos.Contables;
using GestionIntegral.Api.Models.Contables;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Contables
{
public class PagoDistribuidorService : IPagoDistribuidorService
{
private readonly IPagoDistribuidorRepository _pagoRepo;
private readonly IDistribuidorRepository _distribuidorRepo;
private readonly ITipoPagoRepository _tipoPagoRepo;
private readonly IEmpresaRepository _empresaRepo;
private readonly ISaldoRepository _saldoRepo;
private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<PagoDistribuidorService> _logger;
public PagoDistribuidorService(
IPagoDistribuidorRepository pagoRepo,
IDistribuidorRepository distribuidorRepo,
ITipoPagoRepository tipoPagoRepo,
IEmpresaRepository empresaRepo,
ISaldoRepository saldoRepo,
DbConnectionFactory connectionFactory,
ILogger<PagoDistribuidorService> logger)
{
_pagoRepo = pagoRepo;
_distribuidorRepo = distribuidorRepo;
_tipoPagoRepo = tipoPagoRepo;
_empresaRepo = empresaRepo;
_saldoRepo = saldoRepo;
_connectionFactory = connectionFactory;
_logger = logger;
}
private async Task<PagoDistribuidorDto> MapToDto(PagoDistribuidor pago)
{
if (pago == null) return null!;
var distribuidorData = await _distribuidorRepo.GetByIdAsync(pago.IdDistribuidor);
var tipoPago = await _tipoPagoRepo.GetByIdAsync(pago.IdTipoPago);
var empresa = await _empresaRepo.GetByIdAsync(pago.IdEmpresa);
return new PagoDistribuidorDto
{
IdPago = pago.IdPago,
IdDistribuidor = pago.IdDistribuidor,
NombreDistribuidor = distribuidorData.Distribuidor?.Nombre ?? "N/A",
Fecha = pago.Fecha.ToString("yyyy-MM-dd"),
TipoMovimiento = pago.TipoMovimiento,
Recibo = pago.Recibo,
Monto = pago.Monto,
IdTipoPago = pago.IdTipoPago,
NombreTipoPago = tipoPago?.Nombre ?? "N/A",
Detalle = pago.Detalle,
IdEmpresa = pago.IdEmpresa,
NombreEmpresa = empresa?.Nombre ?? "N/A"
};
}
public async Task<IEnumerable<PagoDistribuidorDto>> ObtenerTodosAsync(
DateTime? fechaDesde, DateTime? fechaHasta,
int? idDistribuidor, int? idEmpresa, string? tipoMovimiento)
{
var pagos = await _pagoRepo.GetAllAsync(fechaDesde, fechaHasta, idDistribuidor, idEmpresa, tipoMovimiento);
var dtos = new List<PagoDistribuidorDto>();
foreach (var pago in pagos)
{
dtos.Add(await MapToDto(pago));
}
return dtos;
}
public async Task<PagoDistribuidorDto?> ObtenerPorIdAsync(int idPago)
{
var pago = await _pagoRepo.GetByIdAsync(idPago);
return pago == null ? null : await MapToDto(pago);
}
public async Task<(PagoDistribuidorDto? Pago, string? Error)> CrearAsync(CreatePagoDistribuidorDto createDto, int idUsuario)
{
if (await _distribuidorRepo.GetByIdSimpleAsync(createDto.IdDistribuidor) == null)
return (null, "Distribuidor no válido.");
if (await _tipoPagoRepo.GetByIdAsync(createDto.IdTipoPago) == null)
return (null, "Tipo de pago no válido.");
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
return (null, "Empresa no válida.");
if (await _pagoRepo.ExistsByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento))
return (null, $"Ya existe un pago '{createDto.TipoMovimiento}' con el número de recibo '{createDto.Recibo}'.");
var nuevoPago = new PagoDistribuidor
{
IdDistribuidor = createDto.IdDistribuidor,
Fecha = createDto.Fecha.Date,
TipoMovimiento = createDto.TipoMovimiento,
Recibo = createDto.Recibo,
Monto = createDto.Monto,
IdTipoPago = createDto.IdTipoPago,
Detalle = createDto.Detalle,
IdEmpresa = createDto.IdEmpresa
};
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 pagoCreado = await _pagoRepo.CreateAsync(nuevoPago, idUsuario, transaction);
if (pagoCreado == null) throw new DataException("Error al registrar el pago.");
// Afectar Saldo
// Si TipoMovimiento es "Recibido", el monto DISMINUYE la deuda del distribuidor (monto positivo para el servicio de saldo).
// Si TipoMovimiento es "Realizado" (empresa paga a distribuidor), el monto AUMENTA la deuda (monto negativo para el servicio de saldo).
decimal montoAjusteSaldo = createDto.TipoMovimiento == "Recibido" ? createDto.Monto : -createDto.Monto;
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoCreado.IdDistribuidor, pagoCreado.IdEmpresa, montoAjusteSaldo, transaction);
if (!saldoActualizado) throw new DataException("Error al actualizar el saldo del distribuidor.");
transaction.Commit();
_logger.LogInformation("PagoDistribuidor ID {Id} creado y saldo afectado por Usuario ID {UserId}.", pagoCreado.IdPago, idUsuario);
return (await MapToDto(pagoCreado), null);
}
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error CrearAsync PagoDistribuidor.");
return (null, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> ActualizarAsync(int idPago, UpdatePagoDistribuidorDto 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 pagoExistente = await _pagoRepo.GetByIdAsync(idPago);
if (pagoExistente == null) return (false, "Pago no encontrado.");
if (await _tipoPagoRepo.GetByIdAsync(updateDto.IdTipoPago) == null)
return (false, "Tipo de pago no válido.");
// Calcular la diferencia de monto para ajustar el saldo
decimal montoOriginal = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto;
decimal montoNuevo = pagoExistente.TipoMovimiento == "Recibido" ? updateDto.Monto : -updateDto.Monto;
decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal;
// Actualizar campos permitidos
pagoExistente.Monto = updateDto.Monto;
pagoExistente.IdTipoPago = updateDto.IdTipoPago;
pagoExistente.Detalle = updateDto.Detalle;
var actualizado = await _pagoRepo.UpdateAsync(pagoExistente, idUsuario, transaction);
if (!actualizado) throw new DataException("Error al actualizar el pago.");
if (diferenciaAjusteSaldo != 0)
{
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, diferenciaAjusteSaldo, transaction);
if (!saldoActualizado) throw new DataException("Error al ajustar el saldo del distribuidor tras la actualización del pago.");
}
transaction.Commit();
_logger.LogInformation("PagoDistribuidor ID {Id} actualizado por Usuario ID {UserId}.", idPago, idUsuario);
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Pago no encontrado."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error ActualizarAsync PagoDistribuidor ID: {Id}", idPago);
return (false, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> EliminarAsync(int idPago, 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 pagoExistente = await _pagoRepo.GetByIdAsync(idPago);
if (pagoExistente == null) return (false, "Pago no encontrado.");
// Revertir el efecto en el saldo
// Si fue "Recibido", el saldo disminuyó (montoAjusteSaldo fue +Monto). Al eliminar, revertimos sumando -Monto (o restando +Monto).
// Si fue "Realizado", el saldo aumentó (montoAjusteSaldo fue -Monto). Al eliminar, revertimos sumando +Monto (o restando -Monto).
decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? -pagoExistente.Monto : pagoExistente.Monto;
var eliminado = await _pagoRepo.DeleteAsync(idPago, idUsuario, transaction);
if (!eliminado) throw new DataException("Error al eliminar el pago.");
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, montoReversion, transaction);
if (!saldoActualizado) throw new DataException("Error al revertir el saldo del distribuidor tras la eliminación del pago.");
transaction.Commit();
_logger.LogInformation("PagoDistribuidor ID {Id} eliminado por Usuario ID {UserId}.", idPago, idUsuario);
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Pago no encontrado."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error EliminarAsync PagoDistribuidor ID: {Id}", idPago);
return (false, $"Error interno: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,171 @@
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.Linq;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Distribucion
{
public class ControlDevolucionesService : IControlDevolucionesService
{
private readonly IControlDevolucionesRepository _controlDevRepo;
private readonly IEmpresaRepository _empresaRepository; // Para validar IdEmpresa y obtener nombre
private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<ControlDevolucionesService> _logger;
public ControlDevolucionesService(
IControlDevolucionesRepository controlDevRepo,
IEmpresaRepository empresaRepository,
DbConnectionFactory connectionFactory,
ILogger<ControlDevolucionesService> logger)
{
_controlDevRepo = controlDevRepo;
_empresaRepository = empresaRepository;
_connectionFactory = connectionFactory;
_logger = logger;
}
private async Task<ControlDevolucionesDto> MapToDto(ControlDevoluciones control)
{
if (control == null) return null!;
var empresa = await _empresaRepository.GetByIdAsync(control.IdEmpresa);
return new ControlDevolucionesDto
{
IdControl = control.IdControl,
IdEmpresa = control.IdEmpresa,
NombreEmpresa = empresa?.Nombre ?? "Empresa Desconocida",
Fecha = control.Fecha.ToString("yyyy-MM-dd"),
Entrada = control.Entrada,
Sobrantes = control.Sobrantes,
Detalle = control.Detalle,
SinCargo = control.SinCargo
};
}
public async Task<IEnumerable<ControlDevolucionesDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa)
{
var controles = await _controlDevRepo.GetAllAsync(fechaDesde, fechaHasta, idEmpresa);
var dtos = new List<ControlDevolucionesDto>();
foreach (var control in controles)
{
dtos.Add(await MapToDto(control));
}
return dtos;
}
public async Task<ControlDevolucionesDto?> ObtenerPorIdAsync(int idControl)
{
var control = await _controlDevRepo.GetByIdAsync(idControl);
return control == null ? null : await MapToDto(control);
}
public async Task<(ControlDevolucionesDto? Control, string? Error)> CrearAsync(CreateControlDevolucionesDto createDto, int idUsuario)
{
var empresa = await _empresaRepository.GetByIdAsync(createDto.IdEmpresa);
if (empresa == null)
return (null, "La empresa especificada no existe.");
// Validar que no exista ya un control para esa empresa y fecha
if (await _controlDevRepo.GetByEmpresaAndFechaAsync(createDto.IdEmpresa, createDto.Fecha.Date) != null)
{
return (null, $"Ya existe un control de devoluciones para la empresa '{empresa.Nombre}' en la fecha {createDto.Fecha:dd/MM/yyyy}.");
}
var nuevoControl = new ControlDevoluciones
{
IdEmpresa = createDto.IdEmpresa,
Fecha = createDto.Fecha.Date,
Entrada = createDto.Entrada,
Sobrantes = createDto.Sobrantes,
Detalle = createDto.Detalle,
SinCargo = createDto.SinCargo
};
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 controlCreado = await _controlDevRepo.CreateAsync(nuevoControl, idUsuario, transaction);
if (controlCreado == null) throw new DataException("Error al registrar el control de devoluciones.");
transaction.Commit();
_logger.LogInformation("ControlDevoluciones ID {Id} creado por Usuario ID {UserId}.", controlCreado.IdControl, idUsuario);
return (await MapToDto(controlCreado), null);
}
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error CrearAsync ControlDevoluciones para Empresa ID {IdEmpresa}, Fecha {Fecha}", createDto.IdEmpresa, createDto.Fecha);
return (null, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> ActualizarAsync(int idControl, UpdateControlDevolucionesDto 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 controlExistente = await _controlDevRepo.GetByIdAsync(idControl); // Obtener dentro de TX
if (controlExistente == null) return (false, "Control de devoluciones no encontrado.");
// IdEmpresa y Fecha no se modifican
controlExistente.Entrada = updateDto.Entrada;
controlExistente.Sobrantes = updateDto.Sobrantes;
controlExistente.Detalle = updateDto.Detalle;
controlExistente.SinCargo = updateDto.SinCargo;
var actualizado = await _controlDevRepo.UpdateAsync(controlExistente, idUsuario, transaction);
if (!actualizado) throw new DataException("Error al actualizar el control de devoluciones.");
transaction.Commit();
_logger.LogInformation("ControlDevoluciones ID {Id} actualizado por Usuario ID {UserId}.", idControl, idUsuario);
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error ActualizarAsync ControlDevoluciones ID: {Id}", idControl);
return (false, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> EliminarAsync(int idControl, 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 controlExistente = await _controlDevRepo.GetByIdAsync(idControl); // Obtener dentro de TX
if (controlExistente == null) return (false, "Control de devoluciones no encontrado.");
// Aquí no hay dependencias directas que afecten saldos, es un registro informativo.
// Se podría verificar si está "en uso" si alguna lógica de reporte o cierre depende de él,
// pero por ahora, la eliminación es directa.
var eliminado = await _controlDevRepo.DeleteAsync(idControl, idUsuario, transaction);
if (!eliminado) throw new DataException("Error al eliminar el control de devoluciones.");
transaction.Commit();
_logger.LogInformation("ControlDevoluciones ID {Id} eliminado por Usuario ID {UserId}.", idControl, idUsuario);
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error EliminarAsync ControlDevoluciones ID: {Id}", idControl);
return (false, $"Error interno: {ex.Message}");
}
}
}
}

View File

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

View File

@@ -57,9 +57,29 @@ namespace GestionIntegral.Api.Services.Distribucion
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);
// Obtener el valor bruto del movimiento
decimal valorBrutoMovimiento = await CalcularMontoMovimiento(
es.IdPublicacion, es.IdDistribuidor, es.Fecha, es.Cantidad,
es.TipoMovimiento, // Pasamos el tipo de movimiento original aquí
es.IdPrecio, es.IdRecargo, es.IdPorcentaje, distribuidorData.Distribuidor?.IdZona
);
// Ajustar para el DTO: si es "Entrada", el monto calculado es un crédito (negativo o positivo según convención)
// Para consistencia con el ajuste de saldo, si es Entrada, el MontoCalculado para el DTO puede ser el valor
// que se le "acredita" al distribuidor (o sea, el valor de la mercadería devuelta).
// La lógica de +/- para el saldo ya está en Crear/Actualizar/Eliminar.
// Aquí solo mostramos el valor del movimiento. Si es entrada, es el valor de lo devuelto.
// Si es salida, es el valor de lo que se le factura.
// El método CalcularMonto ya devuelve el monto que el distribuidor DEBE pagar por una SALIDA.
// Para una ENTRADA (devolución), el valor de esa mercadería es el mismo, pero opera en sentido contrario al saldo.
decimal montoCalculadoParaDto = valorBrutoMovimiento;
// Si queremos que el DTO muestre las entradas como un valor que "reduce la deuda",
// podría ser positivo. Si queremos que refleje el impacto directo en la factura (salidas suman, entradas restan),
// podríamos hacerlo negativo.
// Por ahora, dejaremos que CalcularMontoMovimiento devuelva el valor de una "Salida",
// y si es "Entrada", este mismo valor es el que se acredita.
// La columna `MontoCalculado` en el DTO representará el valor de la transacción.
return new EntradaSalidaDistDto
{
@@ -75,17 +95,29 @@ namespace GestionIntegral.Api.Services.Distribucion
Cantidad = es.Cantidad,
Remito = es.Remito,
Observacion = es.Observacion,
MontoCalculado = montoCalculado
MontoCalculado = montoCalculadoParaDto // Representa el valor de los N ejemplares
};
}
private async Task<decimal> 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.
// YA NO SE DEVUELVE 0 PARA ENTRADA AQUÍ
// if (tipoMovimiento == "Entrada") return 0;
var precioConfig = await _precioRepository.GetByIdAsync(idPrecio);
if (precioConfig == null) throw new InvalidOperationException("Configuración de precio no encontrada para el movimiento.");
// Es crucial que idPrecio sea válido y se haya determinado correctamente antes de llamar aquí.
// Si precioConfig es null, se lanzará una excepción abajo, lo cual está bien si es un estado inesperado.
if (precioConfig == null)
{
_logger.LogError("Configuración de precio ID {IdPrecio} no encontrada al calcular monto para Pub {IdPublicacion}, Dist {IdDistribuidor}, Fecha {Fecha}", idPrecio, idPublicacion, idDistribuidor, fecha);
// Dependiendo de la regla de negocio, podrías devolver 0 o lanzar una excepción.
// Si un precio es OBLIGATORIO para cualquier movimiento, lanzar excepción es más apropiado.
// Si puede haber movimientos sin precio (ej. cortesía que no se factura), entonces 0.
// En este contexto, un precio es fundamental para el cálculo.
throw new InvalidOperationException($"Configuración de precio ID {idPrecio} no encontrada. No se puede calcular el monto.");
}
decimal precioDia = 0;
DayOfWeek diaSemana = fecha.DayOfWeek;
@@ -101,9 +133,8 @@ namespace GestionIntegral.Api.Services.Distribucion
}
decimal valorRecargo = 0;
if (idRecargo > 0 && idZonaDistribuidor.HasValue) // El recargo se aplica por la zona del distribuidor
if (idRecargo > 0 && idZonaDistribuidor.HasValue)
{
// 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)
{
@@ -119,10 +150,13 @@ namespace GestionIntegral.Api.Services.Distribucion
var porcConfig = await _porcPagoRepository.GetByIdAsync(idPorcentaje);
if (porcConfig != null)
{
return (montoBase / 100) * porcConfig.Porcentaje;
// El porcentaje de pago del distribuidor es lo que ÉL PAGA a la editorial.
// Entonces, el monto es (precio_tapa_con_recargo * cantidad) * (porcentaje_pago_dist / 100)
return (montoBase * porcConfig.Porcentaje) / 100;
}
}
return montoBase; // Si no hay porcentaje, se factura el 100% del precio con recargo
// Si no hay porcentaje de pago específico, se asume que el distribuidor paga el 100% del monto base.
return montoBase;
}

View File

@@ -0,0 +1,16 @@
using GestionIntegral.Api.Dtos.Distribucion;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Distribucion
{
public interface IControlDevolucionesService
{
Task<IEnumerable<ControlDevolucionesDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa);
Task<ControlDevolucionesDto?> ObtenerPorIdAsync(int idControl);
Task<(ControlDevolucionesDto? Control, string? Error)> CrearAsync(CreateControlDevolucionesDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int idControl, UpdateControlDevolucionesDto updateDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarAsync(int idControl, int idUsuario);
}
}

View File

@@ -0,0 +1,23 @@
using GestionIntegral.Api.Dtos.Distribucion;
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Distribucion
{
public interface IEntradaSalidaCanillaService
{
Task<IEnumerable<EntradaSalidaCanillaDto>> ObtenerTodosAsync(
DateTime? fechaDesde, DateTime? fechaHasta,
int? idPublicacion, int? idCanilla, bool? liquidados, bool? incluirNoLiquidados);
Task<EntradaSalidaCanillaDto?> ObtenerPorIdAsync(int idParte);
Task<(IEnumerable<EntradaSalidaCanillaDto>? MovimientosCreados, string? Error)> CrearMovimientosEnLoteAsync(CreateBulkEntradaSalidaCanillaDto createBulkDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaCanillaDto updateDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, int idUsuario, ClaimsPrincipal userPrincipal);
Task<(bool Exito, string? Error)> LiquidarMovimientosAsync(LiquidarMovimientosCanillaRequestDto liquidarDto, int idUsuarioLiquidador);
}
}

View File

@@ -0,0 +1,173 @@
using GestionIntegral.Api.Data.Repositories.Radios;
using GestionIntegral.Api.Dtos.Radios;
using GestionIntegral.Api.Models.Radios;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
// using GestionIntegral.Api.Data; // Para DbConnectionFactory si se usa transacción
// using System.Data; // Para IsolationLevel si se usa transacción
namespace GestionIntegral.Api.Services.Radios
{
public class CancionService : ICancionService
{
private readonly ICancionRepository _cancionRepository;
private readonly IRitmoRepository _ritmoRepository;
private readonly ILogger<CancionService> _logger;
public CancionService(
ICancionRepository cancionRepository,
IRitmoRepository ritmoRepository,
ILogger<CancionService> logger)
{
_cancionRepository = cancionRepository;
_ritmoRepository = ritmoRepository;
_logger = logger;
}
private async Task<CancionDto> MapToDto(Cancion cancion)
{
if (cancion == null) return null!;
string? nombreRitmo = null;
if (cancion.Ritmo.HasValue && cancion.Ritmo.Value > 0)
{
var ritmoDb = await _ritmoRepository.GetByIdAsync(cancion.Ritmo.Value);
nombreRitmo = ritmoDb?.NombreRitmo;
}
return new CancionDto
{
Id = cancion.Id,
Tema = cancion.Tema,
CompositorAutor = cancion.CompositorAutor,
Interprete = cancion.Interprete,
Sello = cancion.Sello,
Placa = cancion.Placa,
Pista = cancion.Pista,
Introduccion = cancion.Introduccion,
IdRitmo = cancion.Ritmo, // Pasar el valor de cancion.Ritmo
NombreRitmo = nombreRitmo,
Formato = cancion.Formato,
Album = cancion.Album
};
}
public async Task<IEnumerable<CancionDto>> ObtenerTodasAsync(string? temaFilter, string? interpreteFilter, int? idRitmoFilter)
{
var canciones = await _cancionRepository.GetAllAsync(temaFilter, interpreteFilter, idRitmoFilter);
var dtos = new List<CancionDto>();
foreach (var cancion in canciones)
{
dtos.Add(await MapToDto(cancion));
}
return dtos;
}
public async Task<CancionDto?> ObtenerPorIdAsync(int id)
{
var cancion = await _cancionRepository.GetByIdAsync(id);
return cancion == null ? null : await MapToDto(cancion);
}
public async Task<(CancionDto? Cancion, string? Error)> CrearAsync(CreateCancionDto createDto, int idUsuario)
{
if (createDto.IdRitmo.HasValue && createDto.IdRitmo.Value > 0) // Asegurar que > 0 para evitar buscar ritmo con ID 0
{
if(await _ritmoRepository.GetByIdAsync(createDto.IdRitmo.Value) == null)
return (null, "El ritmo seleccionado no es válido.");
}
if (!string.IsNullOrWhiteSpace(createDto.Tema) && !string.IsNullOrWhiteSpace(createDto.Interprete) &&
await _cancionRepository.ExistsByTemaAndInterpreteAsync(createDto.Tema, createDto.Interprete))
{
return (null, "Ya existe una canción con el mismo tema e intérprete.");
}
var nuevaCancion = new Cancion
{
Tema = createDto.Tema, CompositorAutor = createDto.CompositorAutor, Interprete = createDto.Interprete,
Sello = createDto.Sello, Placa = createDto.Placa, Pista = createDto.Pista,
Introduccion = createDto.Introduccion,
Ritmo = createDto.IdRitmo, // Asignar createDto.IdRitmo a la propiedad Ritmo del modelo
Formato = createDto.Formato, Album = createDto.Album
};
try
{
var cancionCreada = await _cancionRepository.CreateAsync(nuevaCancion);
if (cancionCreada == null) return (null, "Error al crear la canción.");
_logger.LogInformation("Canción ID {Id} creada por Usuario ID {UserId}.", cancionCreada.Id, idUsuario);
return (await MapToDto(cancionCreada), null);
}
catch (System.Exception ex)
{
_logger.LogError(ex, "Error CrearAsync Cancion: {Tema}", createDto.Tema);
return (null, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCancionDto updateDto, int idUsuario)
{
var cancionExistente = await _cancionRepository.GetByIdAsync(id);
if (cancionExistente == null) return (false, "Canción no encontrada.");
if (updateDto.IdRitmo.HasValue && updateDto.IdRitmo.Value > 0) // Asegurar que > 0
{
if (await _ritmoRepository.GetByIdAsync(updateDto.IdRitmo.Value) == null)
return (false, "El ritmo seleccionado no es válido.");
}
if ((!string.IsNullOrWhiteSpace(updateDto.Tema) && !string.IsNullOrWhiteSpace(updateDto.Interprete)) &&
(cancionExistente.Tema != updateDto.Tema || cancionExistente.Interprete != updateDto.Interprete) &&
await _cancionRepository.ExistsByTemaAndInterpreteAsync(updateDto.Tema!, updateDto.Interprete!, id))
{
return (false, "Ya existe otra canción con el mismo tema e intérprete."); // Devolver tupla bool,string
}
cancionExistente.Tema = updateDto.Tema; cancionExistente.CompositorAutor = updateDto.CompositorAutor;
cancionExistente.Interprete = updateDto.Interprete; cancionExistente.Sello = updateDto.Sello;
cancionExistente.Placa = updateDto.Placa; cancionExistente.Pista = updateDto.Pista;
cancionExistente.Introduccion = updateDto.Introduccion;
cancionExistente.Ritmo = updateDto.IdRitmo; // Asignar updateDto.IdRitmo a la propiedad Ritmo del modelo
cancionExistente.Formato = updateDto.Formato; cancionExistente.Album = updateDto.Album;
try
{
var actualizado = await _cancionRepository.UpdateAsync(cancionExistente);
if (!actualizado) return (false, "Error al actualizar la canción.");
_logger.LogInformation("Canción ID {Id} actualizada por Usuario ID {UserId}.", id, idUsuario);
return (true, null);
}
catch (System.Exception ex)
{
_logger.LogError(ex, "Error ActualizarAsync Canción ID: {Id}", id);
return (false, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario)
{
var cancionExistente = await _cancionRepository.GetByIdAsync(id);
if (cancionExistente == null) return (false, "Canción no encontrada.");
try
{
var eliminado = await _cancionRepository.DeleteAsync(id);
if (!eliminado) return (false, "Error al eliminar la canción.");
_logger.LogInformation("Canción ID {Id} eliminada por Usuario ID {UserId}.", id, idUsuario);
return (true, null);
}
catch (System.Exception ex)
{
_logger.LogError(ex, "Error EliminarAsync Canción ID: {Id}", id);
return (false, $"Error interno: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,15 @@
using GestionIntegral.Api.Dtos.Radios;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Radios
{
public interface ICancionService
{
Task<IEnumerable<CancionDto>> ObtenerTodasAsync(string? temaFilter, string? interpreteFilter, int? idRitmoFilter);
Task<CancionDto?> ObtenerPorIdAsync(int id);
Task<(CancionDto? Cancion, string? Error)> CrearAsync(CreateCancionDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCancionDto updateDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario);
}
}

View File

@@ -0,0 +1,12 @@
using GestionIntegral.Api.Dtos.Radios;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; // Para IActionResult (o devolver byte[])
namespace GestionIntegral.Api.Services.Radios
{
public interface IRadioListaService
{
// Devuelve byte[] para el archivo y el nombre del archivo sugerido
Task<(byte[] FileContents, string ContentType, string FileName, string? Error)> GenerarListaRadioAsync(GenerarListaRadioRequestDto request);
}
}

View File

@@ -0,0 +1,15 @@
using GestionIntegral.Api.Dtos.Radios;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Radios
{
public interface IRitmoService
{
Task<IEnumerable<RitmoDto>> ObtenerTodosAsync(string? nombreFilter);
Task<RitmoDto?> ObtenerPorIdAsync(int id);
Task<(RitmoDto? Ritmo, string? Error)> CrearAsync(CreateRitmoDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateRitmoDto updateDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario);
}
}

View File

@@ -0,0 +1,224 @@
using GestionIntegral.Api.Data.Repositories.Radios;
using GestionIntegral.Api.Dtos.Radios;
using GestionIntegral.Api.Models.Radios; // Para Cancion
using Microsoft.Extensions.Logging;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Radios
{
public class RadioListaService : IRadioListaService
{
private readonly ICancionRepository _cancionRepository;
// private readonly IRitmoRepository _ritmoRepository; // No se usa en la nueva lógica de generación de Excel
private readonly ILogger<RadioListaService> _logger;
public RadioListaService(
ICancionRepository cancionRepository,
// IRitmoRepository ritmoRepository, // Puede ser removido si no se usa en otro lado
ILogger<RadioListaService> logger)
{
_cancionRepository = cancionRepository;
// _ritmoRepository = ritmoRepository;
_logger = logger;
}
public async Task<(byte[] FileContents, string ContentType, string FileName, string? Error)> GenerarListaRadioAsync(GenerarListaRadioRequestDto request)
{
try
{
_logger.LogInformation("Iniciando generación de lista de radio para Mes: {Mes}, Año: {Anio}, Institución: {Institucion}, Radio: {Radio}",
request.Mes, request.Anio, request.Institucion, request.Radio);
var programacionParaExcel = new List<ProgramacionHorariaExcelDto>();
int diasEnMes = DateTime.DaysInMonth(request.Anio, request.Mes);
for (int dia = 1; dia <= diasEnMes; dia++)
{
int horaInicio = (request.Radio == "FM 99.1") ? 0 : 9;
int horaFin = 23;
for (int hora = horaInicio; hora <= horaFin; hora++)
{
int cantidadRegistros;
if (request.Radio == "FM 99.1")
{
cantidadRegistros = (hora == 23 || hora == 7 || hora == 15) ? 1 : 5;
}
else // FM 100.3
{
cantidadRegistros = (hora == 23 || hora == 15) ? 1 : 2;
}
if (cantidadRegistros > 0)
{
var cancionesSeleccionadas = await _cancionRepository.GetRandomCancionesAsync(cantidadRegistros);
foreach (var cancion in cancionesSeleccionadas)
{
programacionParaExcel.Add(new ProgramacionHorariaExcelDto
{
Dia = dia,
Hora = hora,
TituloObra = cancion.Tema,
CompositorAutor = cancion.CompositorAutor,
Interprete = cancion.Interprete,
Sello = cancion.Sello,
Album = cancion.Album
});
}
}
}
}
if (!programacionParaExcel.Any())
{
_logger.LogWarning("No se generaron datos para la lista de radio con los criterios: {@Request}", request);
return (Array.Empty<byte>(), "", "", "No se generaron datos para la lista con los criterios dados.");
}
string mesText = request.Mes.ToString("00");
string anioFullText = request.Anio.ToString();
string anioShortText = anioFullText.Length > 2 ? anioFullText.Substring(anioFullText.Length - 2) : anioFullText;
byte[] excelBytes = GenerarExcel(programacionParaExcel, request.Institucion, request.Radio, mesText, anioFullText, anioShortText);
string baseFileName;
if (request.Institucion == "AADI")
{
baseFileName = request.Radio == "FM 99.1" ? $"AADI-FM99.1-FM-{mesText}{anioShortText}" : $"AADI-FM100.3-FM-{mesText}{anioShortText}";
}
else // SADAIC
{
baseFileName = request.Radio == "FM 99.1" ? $"FM99.1-FM-{mesText}{anioShortText}" : $"FM100.3-FM-{mesText}{anioShortText}";
}
string excelFileNameInZip = $"{baseFileName}.xlsx";
string zipFileName = $"{baseFileName}.xlsx.zip"; // Para replicar nombre original del zip
using (var memoryStream = new MemoryStream())
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
var excelEntry = archive.CreateEntry(excelFileNameInZip, CompressionLevel.Optimal);
using (var entryStream = excelEntry.Open())
{
await entryStream.WriteAsync(excelBytes, 0, excelBytes.Length);
}
}
_logger.LogInformation("Lista de radio generada y empaquetada en ZIP exitosamente: {ZipFileName}", zipFileName);
return (memoryStream.ToArray(), "application/zip", zipFileName, null);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al generar la lista de radio para Mes: {Mes}, Año: {Anio}, Institucion: {Institucion}, Radio: {Radio}", request.Mes, request.Anio, request.Institucion, request.Radio);
return (Array.Empty<byte>(), "", "", $"Error interno al generar la lista: {ex.Message}");
}
}
private byte[] GenerarExcel(List<ProgramacionHorariaExcelDto> programacion, string institucion, string radio, string mesText, string anioFullText, string anioShortText)
{
IWorkbook workbook = new XSSFWorkbook();
var coreProps = ((XSSFWorkbook)workbook).GetProperties().CoreProperties;
coreProps.Creator = "GestionIntegral"; // Puedes cambiarlo
coreProps.Title = $"Reporte {institucion} - {radio} - {mesText}/{anioFullText}";
// coreProps.Description = "Descripción del reporte"; // Opcional
// var extendedProps = ((XSSFWorkbook)workbook).GetProperties().ExtendedProperties.GetUnderlyingProperties();
// extendedProps.Application = "GestionIntegral System"; // Opcional
// extendedProps.AppVersion = "1.0.0"; // Opcional
string sheetName;
if (institucion == "AADI")
{
sheetName = radio == "FM 99.1" ? $"SA99{mesText}{anioShortText}" : $"SARE{mesText}{anioShortText}";
}
else // SADAIC
{
sheetName = radio == "FM 99.1" ? $"FM99{mesText}{anioShortText}" : $"FMRE{mesText}{anioShortText}";
}
ISheet sheet = workbook.CreateSheet(sheetName);
int currentRowIdx = 0;
if (institucion == "AADI")
{
sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue(radio == "FM 99.1" ? "Nombre: FM LA 99.1" : "Nombre: FM 100.3 LA REDONDA");
sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue("Localidad: La Plata");
sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue("FM");
sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue(radio == "FM 99.1" ? "Frecuencia: 99.1" : "Frecuencia: 100.3");
sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue($"Mes: {mesText}/{anioFullText}");
}
IRow headerDataRow = sheet.CreateRow(currentRowIdx++);
headerDataRow.CreateCell(0).SetCellValue("Día");
headerDataRow.CreateCell(1).SetCellValue("Hora");
headerDataRow.CreateCell(2).SetCellValue("Título de la Obra");
headerDataRow.CreateCell(3).SetCellValue("Compositor-Autor");
headerDataRow.CreateCell(4).SetCellValue("Intérprete");
headerDataRow.CreateCell(5).SetCellValue("Sello");
headerDataRow.CreateCell(6).SetCellValue("Álbum");
IFont font = workbook.CreateFont();
font.FontHeightInPoints = 10;
font.FontName = "Arial";
ICellStyle cellStyle = workbook.CreateCellStyle();
cellStyle.SetFont(font);
foreach (var item in programacion)
{
IRow dataRow = sheet.CreateRow(currentRowIdx++);
dataRow.CreateCell(0).SetCellValue(item.Dia);
dataRow.CreateCell(1).SetCellValue(item.Hora);
dataRow.CreateCell(2).SetCellValue(item.TituloObra);
dataRow.CreateCell(3).SetCellValue(item.CompositorAutor);
dataRow.CreateCell(4).SetCellValue(item.Interprete);
dataRow.CreateCell(5).SetCellValue(item.Sello);
dataRow.CreateCell(6).SetCellValue(item.Album);
for (int i = 0; i < 7; i++)
{
ICell cell = dataRow.GetCell(i) ?? dataRow.CreateCell(i); // Asegurarse que la celda exista
cell.CellStyle = cellStyle;
}
}
sheet.SetColumnWidth(0, 4 * 256);
sheet.SetColumnWidth(1, 5 * 256);
sheet.SetColumnWidth(2, 25 * 256);
sheet.SetColumnWidth(3, 14 * 256);
sheet.SetColumnWidth(4, 11 * 256);
sheet.SetColumnWidth(5, 11 * 256);
sheet.SetColumnWidth(6, 30 * 256);
short rowHeight = 255; // 12.75 puntos
for (int i = 0; i < currentRowIdx; i++)
{
IRow row = sheet.GetRow(i) ?? sheet.CreateRow(i);
row.Height = rowHeight;
// Aplicar estilo a todas las celdas de las filas de encabezado también
if (i < (institucion == "AADI" ? 5 : 0) || i == (institucion == "AADI" ? 5 : 0)) // Filas de cabecera de AADI o la fila de títulos de columnas
{
// Iterar sobre las celdas que realmente existen o deberían existir
int lastCellNum = (institucion == "AADI" && i < 5) ? 1 : 7; // Cabecera AADI solo tiene 1 celda, títulos de datos tienen 7
for (int j = 0; j < lastCellNum; j++)
{
ICell cell = row.GetCell(j) ?? row.CreateCell(j);
cell.CellStyle = cellStyle;
}
}
}
using (var memoryStream = new MemoryStream())
{
workbook.Write(memoryStream, true); // El 'true' es para dejar el stream abierto si se necesita
return memoryStream.ToArray();
}
}
}
}

View File

@@ -0,0 +1,133 @@
using GestionIntegral.Api.Data.Repositories.Radios;
using GestionIntegral.Api.Dtos.Radios;
using GestionIntegral.Api.Models.Radios;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
// using GestionIntegral.Api.Data; // Para DbConnectionFactory si se usa transacción
// using System.Data; // Para IsolationLevel si se usa transacción
namespace GestionIntegral.Api.Services.Radios
{
public class RitmoService : IRitmoService
{
private readonly IRitmoRepository _ritmoRepository;
private readonly ILogger<RitmoService> _logger;
// private readonly DbConnectionFactory _connectionFactory; // Si se implementa historial
public RitmoService(IRitmoRepository ritmoRepository, ILogger<RitmoService> logger /*, DbConnectionFactory cf */)
{
_ritmoRepository = ritmoRepository;
_logger = logger;
// _connectionFactory = cf;
}
private RitmoDto MapToDto(Ritmo ritmo) => new RitmoDto
{
Id = ritmo.Id,
NombreRitmo = ritmo.NombreRitmo
};
public async Task<IEnumerable<RitmoDto>> ObtenerTodosAsync(string? nombreFilter)
{
var ritmos = await _ritmoRepository.GetAllAsync(nombreFilter);
return ritmos.Select(MapToDto);
}
public async Task<RitmoDto?> ObtenerPorIdAsync(int id)
{
var ritmo = await _ritmoRepository.GetByIdAsync(id);
return ritmo == null ? null : MapToDto(ritmo);
}
public async Task<(RitmoDto? Ritmo, string? Error)> CrearAsync(CreateRitmoDto createDto, int idUsuario)
{
if (await _ritmoRepository.ExistsByNameAsync(createDto.NombreRitmo))
{
return (null, "El nombre del ritmo ya existe.");
}
var nuevoRitmo = new Ritmo { NombreRitmo = createDto.NombreRitmo };
// Sin historial, la transacción es opcional para una sola operación,
// pero se deja la estructura por si se añade más lógica o historial.
// 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 ritmoCreado = await _ritmoRepository.CreateAsync(nuevoRitmo /*, idUsuario, transaction */);
if (ritmoCreado == null) return (null, "Error al crear el ritmo.");
// transaction.Commit();
_logger.LogInformation("Ritmo ID {Id} creado por Usuario ID {UserId}.", ritmoCreado.Id, idUsuario);
return (MapToDto(ritmoCreado), null);
}
catch (System.Exception ex)
{
// try { transaction.Rollback(); } catch {}
_logger.LogError(ex, "Error CrearAsync Ritmo: {NombreRitmo}", createDto.NombreRitmo);
return (null, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateRitmoDto updateDto, int idUsuario)
{
var ritmoExistente = await _ritmoRepository.GetByIdAsync(id);
if (ritmoExistente == null) return (false, "Ritmo no encontrado.");
if (ritmoExistente.NombreRitmo != updateDto.NombreRitmo && await _ritmoRepository.ExistsByNameAsync(updateDto.NombreRitmo, id))
{
return (false, "El nombre del ritmo ya existe para otro registro.");
}
ritmoExistente.NombreRitmo = updateDto.NombreRitmo;
// Sin historial, la transacción es opcional...
try
{
var actualizado = await _ritmoRepository.UpdateAsync(ritmoExistente /*, idUsuario, transaction */);
if (!actualizado) return (false, "Error al actualizar el ritmo.");
// transaction.Commit();
_logger.LogInformation("Ritmo ID {Id} actualizado por Usuario ID {UserId}.", id, idUsuario);
return (true, null);
}
catch (System.Exception ex)
{
// try { transaction.Rollback(); } catch {}
_logger.LogError(ex, "Error ActualizarAsync Ritmo ID: {Id}", id);
return (false, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario)
{
var ritmoExistente = await _ritmoRepository.GetByIdAsync(id);
if (ritmoExistente == null) return (false, "Ritmo no encontrado.");
if (await _ritmoRepository.IsInUseAsync(id))
{
return (false, "No se puede eliminar. El ritmo está asignado a una o más canciones.");
}
// Sin historial, la transacción es opcional...
try
{
var eliminado = await _ritmoRepository.DeleteAsync(id /*, idUsuario, transaction */);
if (!eliminado) return (false, "Error al eliminar el ritmo.");
// transaction.Commit();
_logger.LogInformation("Ritmo ID {Id} eliminado por Usuario ID {UserId}.", id, idUsuario);
return (true, null);
}
catch (System.Exception ex)
{
// try { transaction.Rollback(); } catch {}
_logger.LogError(ex, "Error EliminarAsync Ritmo ID: {Id}", id);
return (false, $"Error interno: {ex.Message}");
}
}
}
}

View File

@@ -1,4 +1,5 @@
using GestionIntegral.Api.Dtos.Usuarios;
using GestionIntegral.Api.Dtos.Usuarios.Auditoria;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -13,6 +14,7 @@ namespace GestionIntegral.Api.Services.Usuarios
Task<(bool Exito, string? Error)> SetPasswordAsync(int userId, SetPasswordRequestDto setPasswordDto, int idUsuarioModificador);
// Habilitar/Deshabilitar podría ser un método separado o parte de UpdateAsync
Task<(bool Exito, string? Error)> CambiarEstadoHabilitadoAsync(int userId, bool habilitar, int idUsuarioModificador);
Task<IEnumerable<UsuarioHistorialDto>> ObtenerHistorialPorUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta);
Task<IEnumerable<UsuarioHistorialDto>> ObtenerTodoElHistorialAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModificoFilter, string? tipoModFilter);
}
}

View File

@@ -1,6 +1,7 @@
using GestionIntegral.Api.Data;
using GestionIntegral.Api.Data.Repositories.Usuarios;
using GestionIntegral.Api.Dtos.Usuarios;
using GestionIntegral.Api.Dtos.Usuarios.Auditoria;
using GestionIntegral.Api.Models.Usuarios; // Para Usuario
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
@@ -79,7 +80,7 @@ namespace GestionIntegral.Api.Services.Usuarios
{
return (null, "El perfil seleccionado no es válido.");
}
if(createDto.User.Equals(createDto.Password, System.StringComparison.OrdinalIgnoreCase))
if (createDto.User.Equals(createDto.Password, System.StringComparison.OrdinalIgnoreCase))
{
return (null, "La contraseña no puede ser igual al nombre de usuario.");
}
@@ -112,11 +113,18 @@ namespace GestionIntegral.Api.Services.Usuarios
transaction.Commit();
// Construir el DTO para la respuesta
var dto = new UsuarioDto {
Id = usuarioCreado.Id, User = usuarioCreado.User, Habilitada = usuarioCreado.Habilitada, SupAdmin = usuarioCreado.SupAdmin,
Nombre = usuarioCreado.Nombre, Apellido = usuarioCreado.Apellido, IdPerfil = usuarioCreado.IdPerfil,
var dto = new UsuarioDto
{
Id = usuarioCreado.Id,
User = usuarioCreado.User,
Habilitada = usuarioCreado.Habilitada,
SupAdmin = usuarioCreado.SupAdmin,
Nombre = usuarioCreado.Nombre,
Apellido = usuarioCreado.Apellido,
IdPerfil = usuarioCreado.IdPerfil,
NombrePerfil = perfilSeleccionado.NombrePerfil, // Usamos el nombre del perfil ya obtenido
DebeCambiarClave = usuarioCreado.DebeCambiarClave, VerLog = usuarioCreado.VerLog
DebeCambiarClave = usuarioCreado.DebeCambiarClave,
VerLog = usuarioCreado.VerLog
};
_logger.LogInformation("Usuario ID {UsuarioId} creado por Usuario ID {CreadorId}.", usuarioCreado.Id, idUsuarioCreador);
return (dto, null);
@@ -160,8 +168,9 @@ namespace GestionIntegral.Api.Services.Usuarios
_logger.LogInformation("Usuario ID {UsuarioId} actualizado por Usuario ID {ModificadorId}.", id, idUsuarioModificador);
return (true, null);
}
catch (KeyNotFoundException) {
try { transaction.Rollback(); } catch { /* Log */ }
catch (KeyNotFoundException)
{
try { transaction.Rollback(); } catch { /* Log */ }
return (false, "Usuario no encontrado durante la actualización.");
}
catch (Exception ex)
@@ -176,9 +185,9 @@ namespace GestionIntegral.Api.Services.Usuarios
var usuario = await _usuarioRepository.GetByIdAsync(userId);
if (usuario == null) return (false, "Usuario no encontrado.");
if(usuario.User.Equals(setPasswordDto.NewPassword, System.StringComparison.OrdinalIgnoreCase))
if (usuario.User.Equals(setPasswordDto.NewPassword, System.StringComparison.OrdinalIgnoreCase))
{
return (false, "La nueva contraseña no puede ser igual al nombre de usuario.");
return (false, "La nueva contraseña no puede ser igual al nombre de usuario.");
}
(string hash, string salt) = _passwordHasher.HashPassword(setPasswordDto.NewPassword);
@@ -189,14 +198,15 @@ namespace GestionIntegral.Api.Services.Usuarios
try
{
var success = await _usuarioRepository.SetPasswordAsync(userId, hash, salt, setPasswordDto.ForceChangeOnNextLogin, idUsuarioModificador, transaction);
if(!success) throw new DataException("Error al actualizar la contraseña en el repositorio.");
if (!success) throw new DataException("Error al actualizar la contraseña en el repositorio.");
transaction.Commit();
_logger.LogInformation("Contraseña establecida para Usuario ID {TargetUserId} por Usuario ID {AdminUserId}.", userId, idUsuarioModificador);
return (true, null);
}
catch (KeyNotFoundException) {
try { transaction.Rollback(); } catch { /* Log */ }
catch (KeyNotFoundException)
{
try { transaction.Rollback(); } catch { /* Log */ }
return (false, "Usuario no encontrado durante el cambio de contraseña.");
}
catch (Exception ex)
@@ -207,7 +217,7 @@ namespace GestionIntegral.Api.Services.Usuarios
}
}
public async Task<(bool Exito, string? Error)> CambiarEstadoHabilitadoAsync(int userId, bool habilitar, int idUsuarioModificador)
public async Task<(bool Exito, string? Error)> CambiarEstadoHabilitadoAsync(int userId, bool habilitar, int idUsuarioModificador)
{
var usuario = await _usuarioRepository.GetByIdAsync(userId);
if (usuario == null) return (false, "Usuario no encontrado.");
@@ -225,22 +235,34 @@ namespace GestionIntegral.Api.Services.Usuarios
try
{
var actualizado = await _usuarioRepository.UpdateAsync(usuario, idUsuarioModificador, transaction);
if (!actualizado) throw new DataException("Error al cambiar estado de habilitación del usuario en el repositorio.");
if (!actualizado) throw new DataException("Error al cambiar estado de habilitación del usuario en el repositorio.");
transaction.Commit();
_logger.LogInformation("Estado de habilitación cambiado a {Estado} para Usuario ID {TargetUserId} por Usuario ID {AdminUserId}.", habilitar, userId, idUsuarioModificador);
return (true, null);
}
catch (KeyNotFoundException) {
try { transaction.Rollback(); } catch { /* Log */ }
catch (KeyNotFoundException)
{
try { transaction.Rollback(); } catch { /* Log */ }
return (false, "Usuario no encontrado durante el cambio de estado.");
}
catch (Exception ex)
{
try { transaction.Rollback(); } catch { /* Log */ }
try { transaction.Rollback(); } catch { /* Log */ }
_logger.LogError(ex, "Error al cambiar estado de habilitación para Usuario ID {TargetUserId}.", userId);
return (false, $"Error interno al cambiar estado de habilitación: {ex.Message}");
}
}
public async Task<IEnumerable<UsuarioHistorialDto>> ObtenerHistorialPorUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta)
{
// Aquí podrías añadir validaciones extra si fuera necesario antes de llamar al repo
return await _usuarioRepository.GetHistorialByUsuarioIdAsync(idUsuarioAfectado, fechaDesde, fechaHasta);
}
public async Task<IEnumerable<UsuarioHistorialDto>> ObtenerTodoElHistorialAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModificoFilter, string? tipoModFilter)
{
// Aquí podrías añadir validaciones extra
return await _usuarioRepository.GetAllHistorialAsync(fechaDesde, fechaHasta, idUsuarioModificoFilter, tipoModFilter);
}
}
}