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:
2025-08-08 09:48:15 -03:00
parent 9cfe9d012e
commit 899e0a173f
87 changed files with 2947 additions and 1231 deletions

View File

@@ -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.");
}
}