Refactor: Mejora la lógica de facturación y la UI
Este commit introduce una refactorización significativa en el módulo de
suscripciones para alinear el sistema con reglas de negocio clave:
facturación consolidada por empresa, cobro a mes adelantado con
imputación de ajustes diferida, y una interfaz de usuario más clara.
Backend:
- **Facturación por Empresa:** Se modifica `FacturacionService` para
  agrupar las suscripciones por cliente y empresa, generando una
  factura consolidada para cada combinación. Esto asegura la correcta
  separación fiscal.
- **Imputación de Ajustes:** Se ajusta la lógica para que la facturación
  de un período (ej. Septiembre) aplique únicamente los ajustes
  pendientes cuya fecha corresponde al período anterior (Agosto).
- **Cierre Secuencial:** Se implementa una validación en
  `GenerarFacturacionMensual` que impide generar la facturación de un
  período si el anterior no ha sido cerrado, garantizando el orden
  cronológico.
- **Emails Consolidados:** El proceso de notificación automática al
  generar el cierre ahora envía un único email consolidado por
  suscriptor, detallando los cargos de todas sus facturas/empresas.
- **Envío de PDF Individual:** Se refactoriza el endpoint de envío manual
  para que opere sobre una `idFactura` individual y adjunte el PDF
  correspondiente si existe.
- **Repositorios Mejorados:** Se optimizan y añaden métodos en
  `FacturaRepository` y `AjusteRepository` para soportar los nuevos
  requisitos de filtrado y consulta de datos consolidados.
Frontend:
- **Separación de Vistas:** La página de "Facturación" se divide en dos:
  - `ProcesosPage`: Para acciones masivas (generar cierre, archivo de
    débito, procesar respuesta).
  - `ConsultaFacturasPage`: Una nueva página dedicada a buscar,
    filtrar y gestionar facturas individuales con una interfaz de doble
    acordeón (Suscriptor -> Empresa).
- **Filtros Avanzados:** La página `ConsultaFacturasPage` ahora incluye
  filtros por nombre de suscriptor, estado de pago y estado de
  facturación.
- **Filtros de Fecha por Defecto:** La página de "Cuenta Corriente"
  ahora filtra por el mes actual por defecto para mejorar el rendimiento
  y la usabilidad.
- **Validación de Fechas:** Se añade lógica en los filtros de fecha para
  impedir la selección de rangos inválidos.
- **Validación de Monto de Pago:** El modal de pago manual ahora impide
  registrar un monto superior al saldo pendiente de la factura.
			
			
This commit is contained in:
		| @@ -3,7 +3,12 @@ using GestionIntegral.Api.Data.Repositories.Distribucion; | ||||
| using GestionIntegral.Api.Data.Repositories.Suscripciones; | ||||
| using GestionIntegral.Api.Dtos.Suscripciones; | ||||
| using GestionIntegral.Api.Models.Suscripciones; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Data; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Suscripciones | ||||
| { | ||||
| @@ -36,8 +41,10 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|         { | ||||
|             IdPromocion = promo.IdPromocion, | ||||
|             Descripcion = promo.Descripcion, | ||||
|             TipoPromocion = promo.TipoPromocion, | ||||
|             Valor = promo.Valor, | ||||
|             TipoEfecto = promo.TipoEfecto, | ||||
|             ValorEfecto = promo.ValorEfecto, | ||||
|             TipoCondicion = promo.TipoCondicion, | ||||
|             ValorCondicion = promo.ValorCondicion, | ||||
|             FechaInicio = promo.FechaInicio.ToString("yyyy-MM-dd"), | ||||
|             FechaFin = promo.FechaFin?.ToString("yyyy-MM-dd"), | ||||
|             Activa = promo.Activa | ||||
| @@ -154,47 +161,67 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<PromocionDto>> ObtenerPromocionesAsignadas(int idSuscripcion) | ||||
|         public async Task<IEnumerable<PromocionAsignadaDto>> ObtenerPromocionesAsignadas(int idSuscripcion) | ||||
|         { | ||||
|             var promociones = await _suscripcionRepository.GetPromocionesBySuscripcionIdAsync(idSuscripcion); | ||||
|             return promociones.Select(MapPromocionToDto); | ||||
|             var asignaciones = await _suscripcionRepository.GetPromocionesAsignadasBySuscripcionIdAsync(idSuscripcion); | ||||
|             return asignaciones.Select(a => new PromocionAsignadaDto | ||||
|             { | ||||
|                 IdPromocion = a.Promocion.IdPromocion, | ||||
|                 Descripcion = a.Promocion.Descripcion, | ||||
|                 TipoEfecto = a.Promocion.TipoEfecto, | ||||
|                 ValorEfecto = a.Promocion.ValorEfecto, | ||||
|                 TipoCondicion = a.Promocion.TipoCondicion, | ||||
|                 ValorCondicion = a.Promocion.ValorCondicion, | ||||
|                 FechaInicio = a.Promocion.FechaInicio.ToString("yyyy-MM-dd"), | ||||
|                 FechaFin = a.Promocion.FechaFin?.ToString("yyyy-MM-dd"), | ||||
|                 Activa = a.Promocion.Activa, | ||||
|                 VigenciaDesdeAsignacion = a.Asignacion.VigenciaDesde.ToString("yyyy-MM-dd"), | ||||
|                 VigenciaHastaAsignacion = a.Asignacion.VigenciaHasta?.ToString("yyyy-MM-dd") | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<PromocionDto>> ObtenerPromocionesDisponibles(int idSuscripcion) | ||||
|         { | ||||
|             var todasLasPromosActivas = await _promocionRepository.GetAllAsync(true); | ||||
|             var promosAsignadas = await _suscripcionRepository.GetPromocionesBySuscripcionIdAsync(idSuscripcion); | ||||
|             var idsAsignadas = promosAsignadas.Select(p => p.IdPromocion).ToHashSet(); | ||||
|             var promosAsignadasData = await _suscripcionRepository.GetPromocionesAsignadasBySuscripcionIdAsync(idSuscripcion); | ||||
|             var idsAsignadas = promosAsignadasData.Select(p => p.Promocion.IdPromocion).ToHashSet(); | ||||
|  | ||||
|             return todasLasPromosActivas | ||||
|                 .Where(p => !idsAsignadas.Contains(p.IdPromocion)) | ||||
|                 .Select(MapPromocionToDto); | ||||
|                 .Select(MapPromocionToDto); // Usa el helper que ya creamos | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> AsignarPromocion(int idSuscripcion, int idPromocion, int idUsuario) | ||||
|         public async Task<(bool Exito, string? Error)> AsignarPromocion(int idSuscripcion, AsignarPromocionDto dto, int idUsuario) | ||||
|         { | ||||
|             using var connection = _connectionFactory.CreateConnection(); | ||||
|             await (connection as System.Data.Common.DbConnection)!.OpenAsync(); | ||||
|             using var transaction = connection.BeginTransaction(); | ||||
|             try | ||||
|             { | ||||
|                 // Validaciones | ||||
|                 if (await _suscripcionRepository.GetByIdAsync(idSuscripcion) == null) return (false, "Suscripción no encontrada."); | ||||
|                 if (await _promocionRepository.GetByIdAsync(idPromocion) == null) return (false, "Promoción no encontrada."); | ||||
|                 if (await _promocionRepository.GetByIdAsync(dto.IdPromocion) == null) return (false, "Promoción no encontrada."); | ||||
|  | ||||
|                 await _suscripcionRepository.AsignarPromocionAsync(idSuscripcion, idPromocion, idUsuario, transaction); | ||||
|                 var nuevaAsignacion = new SuscripcionPromocion | ||||
|                 { | ||||
|                     IdSuscripcion = idSuscripcion, | ||||
|                     IdPromocion = dto.IdPromocion, | ||||
|                     IdUsuarioAsigno = idUsuario, | ||||
|                     VigenciaDesde = dto.VigenciaDesde, | ||||
|                     VigenciaHasta = dto.VigenciaHasta | ||||
|                 }; | ||||
|  | ||||
|                 await _suscripcionRepository.AsignarPromocionAsync(nuevaAsignacion, transaction); | ||||
|                 transaction.Commit(); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 // Capturar error de Primary Key duplicada | ||||
|                 if (ex.Message.Contains("PRIMARY KEY constraint")) | ||||
|                 { | ||||
|                     return (false, "Esta promoción ya está asignada a la suscripción."); | ||||
|                 } | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error al asignar promoción {IdPromocion} a suscripción {IdSuscripcion}", idPromocion, idSuscripcion); | ||||
|                 _logger.LogError(ex, "Error al asignar promoción {IdPromocion} a suscripción {IdSuscripcion}", dto.IdPromocion, idSuscripcion); | ||||
|                 return (false, "Error interno al asignar la promoción."); | ||||
|             } | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user