Se introduce una nueva funcionalidad de reporte crucial para la logística y se realiza una refactorización mayor del sistema de ajustes para garantizar la correcta imputación contable. ### ✨ Nuevas Características - **Nuevo Reporte de Distribución de Suscripciones (RR011):** - Se añade un nuevo reporte en PDF que genera un listado de todas las suscripciones activas en un rango de fechas. - El reporte está diseñado para el equipo de reparto, incluyendo datos clave como nombre del suscriptor, dirección, teléfono, días de entrega y observaciones. - Se implementa el endpoint, la lógica de servicio, la consulta a la base de datos y el template de QuestPDF en el backend. - Se crea la página correspondiente en el frontend (React) con su selector de fechas y se añade la ruta y el enlace en el menú de reportes. ### 🔄 Refactorización Mayor - **Asociación de Ajustes a Empresas:** - Se refactoriza por completo la entidad `Ajuste` para incluir una referencia obligatoria a una `IdEmpresa`. - **Motivo:** Corregir un error crítico en la lógica de negocio donde los ajustes de un suscriptor se aplicaban a la primera factura generada, sin importar a qué empresa correspondía el ajuste. - Se provee un script de migración SQL para actualizar el esquema de la base de datos (`susc_Ajustes`). - Se actualizan todos los DTOs, repositorios y servicios (backend) para manejar la nueva relación. - Se modifica el `FacturacionService` para que ahora aplique los ajustes pendientes correspondientes a cada empresa dentro de su bucle de facturación. - Se actualiza el formulario de creación/edición de ajustes en el frontend (React) para incluir un selector de empresa obligatorio. ### ⚡️ Optimizaciones de Rendimiento - **Solución de N+1 Queries:** - Se optimiza el método `ObtenerTodos` en `SuscriptorService` para obtener todas las formas de pago en una única consulta en lugar de una por cada suscriptor. - Se optimiza el método `ObtenerAjustesPorSuscriptor` en `AjusteService` para obtener todos los nombres de usuarios y empresas en dos consultas masivas, en lugar de N consultas individuales. - Se añade el método `GetByIdsAsync` al `IUsuarioRepository` y su implementación para soportar esta optimización. ### 🐛 Corrección de Errores - Se corrigen múltiples errores en el script de migración de base de datos (uso de `GO` dentro de transacciones, error de "columna inválida"). - Se soluciona un error de SQL (`INSERT` statement) en `AjusteRepository` que impedía la creación de nuevos ajustes. - Se corrige un bug en el `AjusteService` que causaba que el nombre de la empresa apareciera como "N/A" en la UI debido a un mapeo incompleto en el método optimizado. - Se corrige la lógica de generación de emails en `FacturacionService` para mostrar correctamente el nombre de la empresa en cada sección del resumen de cuenta.
		
			
				
	
	
		
			139 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			139 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using Dapper;
 | |
| using GestionIntegral.Api.Models.Suscripciones;
 | |
| using System.Data;
 | |
| using System.Text;
 | |
| 
 | |
| namespace GestionIntegral.Api.Data.Repositories.Suscripciones
 | |
| {
 | |
|     public class AjusteRepository : IAjusteRepository
 | |
|     {
 | |
|         private readonly DbConnectionFactory _connectionFactory;
 | |
|         private readonly ILogger<AjusteRepository> _logger;
 | |
| 
 | |
|         public AjusteRepository(DbConnectionFactory factory, ILogger<AjusteRepository> logger)
 | |
|         {
 | |
|             _connectionFactory = factory;
 | |
|             _logger = logger;
 | |
|         }
 | |
| 
 | |
|         public async Task<bool> UpdateAsync(Ajuste ajuste, IDbTransaction transaction)
 | |
|         {
 | |
|             const string sql = @"
 | |
|                 UPDATE dbo.susc_Ajustes SET
 | |
|                     IdEmpresa = @IdEmpresa,
 | |
|                     FechaAjuste = @FechaAjuste,
 | |
|                     TipoAjuste = @TipoAjuste,
 | |
|                     Monto = @Monto,
 | |
|                     Motivo = @Motivo
 | |
|                 WHERE IdAjuste = @IdAjuste AND Estado = 'Pendiente';";
 | |
|             if (transaction?.Connection == null)
 | |
|             {
 | |
|                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula.");
 | |
|             }
 | |
|             var rows = await transaction.Connection.ExecuteAsync(sql, ajuste, transaction);
 | |
|             return rows == 1;
 | |
|         }
 | |
| 
 | |
|         public async Task<Ajuste?> CreateAsync(Ajuste nuevoAjuste, IDbTransaction transaction)
 | |
|         {
 | |
|             const string sql = @"
 | |
|                 INSERT INTO dbo.susc_Ajustes (IdSuscriptor, IdEmpresa, FechaAjuste, TipoAjuste, Monto, Motivo, Estado, IdUsuarioAlta, FechaAlta)
 | |
|                 OUTPUT INSERTED.*
 | |
|                 VALUES (@IdSuscriptor, @IdEmpresa, @FechaAjuste, @TipoAjuste, @Monto, @Motivo, 'Pendiente', @IdUsuarioAlta, GETDATE());";
 | |
|             if (transaction?.Connection == null)
 | |
|             {
 | |
|                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula.");
 | |
|             }
 | |
|             return await transaction.Connection.QuerySingleOrDefaultAsync<Ajuste>(sql, nuevoAjuste, transaction);
 | |
|         }
 | |
| 
 | |
|         public async Task<IEnumerable<Ajuste>> GetAjustesPorSuscriptorAsync(int idSuscriptor, DateTime? fechaDesde, DateTime? fechaHasta)
 | |
|         {
 | |
|             var sqlBuilder = new StringBuilder("SELECT * FROM dbo.susc_Ajustes WHERE IdSuscriptor = @IdSuscriptor");
 | |
|             var parameters = new DynamicParameters();
 | |
|             parameters.Add("IdSuscriptor", idSuscriptor);
 | |
| 
 | |
|             if (fechaDesde.HasValue)
 | |
|             {
 | |
|                 sqlBuilder.Append(" AND FechaAjuste >= @FechaDesde");
 | |
|                 parameters.Add("FechaDesde", fechaDesde.Value.Date);
 | |
|             }
 | |
|             if (fechaHasta.HasValue)
 | |
|             {
 | |
|                 sqlBuilder.Append(" AND FechaAjuste <= @FechaHasta");
 | |
|                 parameters.Add("FechaHasta", fechaHasta.Value.Date);
 | |
|             }
 | |
| 
 | |
|             sqlBuilder.Append(" ORDER BY FechaAlta DESC;");
 | |
| 
 | |
|             using var connection = _connectionFactory.CreateConnection();
 | |
|             return await connection.QueryAsync<Ajuste>(sqlBuilder.ToString(), parameters);
 | |
|         }
 | |
| 
 | |
|         public async Task<IEnumerable<Ajuste>> GetAjustesPendientesHastaFechaAsync(int idSuscriptor, int idEmpresa, DateTime fechaHasta, IDbTransaction transaction)
 | |
|         {
 | |
|             const string sql = @"
 | |
|                 SELECT * FROM dbo.susc_Ajustes 
 | |
|                 WHERE IdSuscriptor = @IdSuscriptor 
 | |
|                 AND IdEmpresa = @IdEmpresa
 | |
|                 AND Estado = 'Pendiente'
 | |
|                 AND FechaAjuste <= @FechaHasta;";
 | |
|             
 | |
|             if (transaction?.Connection == null)
 | |
|             {
 | |
|                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula.");
 | |
|             }
 | |
|             return await transaction.Connection.QueryAsync<Ajuste>(sql, new { idSuscriptor, idEmpresa, FechaHasta = fechaHasta }, transaction);
 | |
|         }
 | |
| 
 | |
|         public async Task<bool> MarcarAjustesComoAplicadosAsync(IEnumerable<int> idsAjustes, int idFactura, IDbTransaction transaction)
 | |
|         {
 | |
|             if (!idsAjustes.Any()) return true;
 | |
| 
 | |
|             const string sql = @"
 | |
|                 UPDATE dbo.susc_Ajustes SET
 | |
|                     Estado = 'Aplicado',
 | |
|                     IdFacturaAplicado = @IdFactura
 | |
|                 WHERE IdAjuste IN @IdsAjustes;";
 | |
| 
 | |
|             if (transaction?.Connection == null)
 | |
|             {
 | |
|                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula.");
 | |
|             }
 | |
|             var rowsAffected = await transaction.Connection.ExecuteAsync(sql, new { IdsAjustes = idsAjustes, IdFactura = idFactura }, transaction);
 | |
|             return rowsAffected == idsAjustes.Count();
 | |
|         }
 | |
| 
 | |
|         public async Task<Ajuste?> GetByIdAsync(int idAjuste)
 | |
|         {
 | |
|             const string sql = "SELECT * FROM dbo.susc_Ajustes WHERE IdAjuste = @IdAjuste;";
 | |
|             using var connection = _connectionFactory.CreateConnection();
 | |
|             return await connection.QuerySingleOrDefaultAsync<Ajuste>(sql, new { idAjuste });
 | |
|         }
 | |
| 
 | |
|         public async Task<bool> AnularAjusteAsync(int idAjuste, int idUsuario, IDbTransaction transaction)
 | |
|         {
 | |
|             const string sql = @"
 | |
|                 UPDATE dbo.susc_Ajustes SET
 | |
|                     Estado = 'Anulado',
 | |
|                     IdUsuarioAnulo = @IdUsuario,
 | |
|                     FechaAnulacion = GETDATE()
 | |
|                 WHERE IdAjuste = @IdAjuste AND Estado = 'Pendiente';";
 | |
| 
 | |
|             if (transaction?.Connection == null)
 | |
|             {
 | |
|                 throw new ArgumentNullException(nameof(transaction.Connection), "La conexión de la transacción no puede ser nula.");
 | |
|             }
 | |
| 
 | |
|             var rows = await transaction.Connection.ExecuteAsync(sql, new { IdAjuste = idAjuste, IdUsuario = idUsuario }, transaction);
 | |
|             return rows == 1;
 | |
|         }
 | |
| 
 | |
|         public async Task<IEnumerable<Ajuste>> GetAjustesPorIdFacturaAsync(int idFactura)
 | |
|         {
 | |
|             const string sql = "SELECT * FROM dbo.susc_Ajustes WHERE IdFacturaAplicado = @IdFactura;";
 | |
|             using var connection = _connectionFactory.CreateConnection();
 | |
|             return await connection.QueryAsync<Ajuste>(sql, new { IdFactura = idFactura });
 | |
|         }
 | |
|     }
 | |
| } |