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:
		| @@ -1,17 +1,16 @@ | ||||
| // Archivo: GestionIntegral.Api/Services/Suscripciones/DebitoAutomaticoService.cs | ||||
|  | ||||
| using GestionIntegral.Api.Data; | ||||
| using GestionIntegral.Api.Data.Repositories.Suscripciones; | ||||
| using GestionIntegral.Api.Models.Suscripciones; | ||||
| using System.Data; | ||||
| using System.Text; | ||||
| using GestionIntegral.Api.Dtos.Suscripciones; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System; | ||||
| using System.Data; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Collections.Generic; | ||||
| using GestionIntegral.Api.Dtos.Suscripciones; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using System.IO; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Suscripciones | ||||
| { | ||||
| @@ -19,21 +18,18 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|     { | ||||
|         private readonly IFacturaRepository _facturaRepository; | ||||
|         private readonly ISuscriptorRepository _suscriptorRepository; | ||||
|         private readonly ISuscripcionRepository _suscripcionRepository; | ||||
|         private readonly ILoteDebitoRepository _loteDebitoRepository; | ||||
|         private readonly IFormaPagoRepository _formaPagoRepository; | ||||
|         private readonly IPagoRepository _pagoRepository; | ||||
|         private readonly DbConnectionFactory _connectionFactory; | ||||
|         private readonly ILogger<DebitoAutomaticoService> _logger; | ||||
|  | ||||
|         // --- CONSTANTES DEL BANCO (Mover a appsettings.json si es necesario) --- | ||||
|         private const string NRO_PRESTACION = "123456"; // Nro. de prestación asignado por el banco | ||||
|         private const string ORIGEN_EMPRESA = "ELDIA";   // Nombre de la empresa (7 chars) | ||||
|  | ||||
|         public DebitoAutomaticoService( | ||||
|             IFacturaRepository facturaRepository, | ||||
|             ISuscriptorRepository suscriptorRepository, | ||||
|             ISuscripcionRepository suscripcionRepository, | ||||
|             ILoteDebitoRepository loteDebitoRepository, | ||||
|             IFormaPagoRepository formaPagoRepository, | ||||
|             IPagoRepository pagoRepository, | ||||
| @@ -42,7 +38,6 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|         { | ||||
|             _facturaRepository = facturaRepository; | ||||
|             _suscriptorRepository = suscriptorRepository; | ||||
|             _suscripcionRepository = suscripcionRepository; | ||||
|             _loteDebitoRepository = loteDebitoRepository; | ||||
|             _formaPagoRepository = formaPagoRepository; | ||||
|             _pagoRepository = pagoRepository; | ||||
| @@ -61,9 +56,7 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 // Buscamos facturas que están listas para ser enviadas al cobro. | ||||
|                 var facturasParaDebito = await GetFacturasParaDebito(periodo, transaction); | ||||
|  | ||||
|                 if (!facturasParaDebito.Any()) | ||||
|                 { | ||||
|                     return (null, null, "No se encontraron facturas pendientes de cobro por débito automático para el período seleccionado."); | ||||
| @@ -73,7 +66,6 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|                 var cantidadRegistros = facturasParaDebito.Count(); | ||||
|                 var nombreArchivo = $"PD_{ORIGEN_EMPRESA.Trim()}_{fechaGeneracion:yyyyMMdd}.txt"; | ||||
|  | ||||
|                 // 1. Crear el Lote de Débito | ||||
|                 var nuevoLote = new LoteDebito | ||||
|                 { | ||||
|                     Periodo = periodo, | ||||
| @@ -85,18 +77,14 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|                 var loteCreado = await _loteDebitoRepository.CreateAsync(nuevoLote, transaction); | ||||
|                 if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito."); | ||||
|  | ||||
|                 // 2. Generar el contenido del archivo | ||||
|                 var sb = new StringBuilder(); | ||||
|                 sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros)); | ||||
|  | ||||
|                 foreach (var item in facturasParaDebito) | ||||
|                 { | ||||
|                     sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor)); | ||||
|                 } | ||||
|  | ||||
|                 sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros)); | ||||
|  | ||||
|                 // 3. Actualizar las facturas con el ID del lote | ||||
|                 var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura); | ||||
|                 bool actualizadas = await _facturaRepository.UpdateLoteDebitoAsync(idsFacturas, loteCreado.IdLoteDebito, transaction); | ||||
|                 if (!actualizadas) throw new DataException("No se pudieron actualizar las facturas con la información del lote."); | ||||
| @@ -115,17 +103,12 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|  | ||||
|         private async Task<List<(Factura Factura, Suscriptor Suscriptor)>> GetFacturasParaDebito(string periodo, IDbTransaction transaction) | ||||
|         { | ||||
|             // Idealmente, esto debería estar en el repositorio para optimizar la consulta. | ||||
|             // Por simplicidad del ejemplo, lo hacemos aquí. | ||||
|             var facturas = await _facturaRepository.GetByPeriodoAsync(periodo); | ||||
|             var facturasDelPeriodo = await _facturaRepository.GetByPeriodoAsync(periodo); | ||||
|             var resultado = new List<(Factura, Suscriptor)>(); | ||||
|  | ||||
|             foreach (var f in facturas.Where(fa => fa.Estado == "Pendiente de Cobro")) | ||||
|              | ||||
|             foreach (var f in facturasDelPeriodo.Where(fa => fa.EstadoPago == "Pendiente")) | ||||
|             { | ||||
|                 var suscripcion = await _suscripcionRepository.GetByIdAsync(f.IdSuscripcion); | ||||
|                 if (suscripcion == null) continue; | ||||
|  | ||||
|                 var suscriptor = await _suscriptorRepository.GetByIdAsync(suscripcion.IdSuscriptor); | ||||
|                 var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor); | ||||
|                 if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU)) continue; | ||||
|  | ||||
|                 var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida); | ||||
| @@ -231,28 +214,17 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|                 string? linea; | ||||
|                 while ((linea = await reader.ReadLineAsync()) != null) | ||||
|                 { | ||||
|                     // Ignorar header/trailer si los hubiera (basado en el formato real) | ||||
|                     if (linea.Length < 20) continue; | ||||
|  | ||||
|                     respuesta.TotalRegistrosLeidos++; | ||||
|  | ||||
|                     // ================================================================= | ||||
|                     // === ESTA ES LA LÓGICA DE PARSEO QUE SE DEBE AJUSTAR === | ||||
|                     // === CON EL FORMATO REAL DEL ARCHIVO DE RESPUESTA    === | ||||
|                     // ================================================================= | ||||
|                     // Asunción: Pos 1-15: Referencia, Pos 16-17: Estado, Pos 18-20: Rechazo | ||||
|                     var referencia = linea.Substring(0, 15).Trim(); | ||||
|                     var estadoProceso = linea.Substring(15, 2).Trim(); | ||||
|                     var motivoRechazo = linea.Substring(17, 3).Trim(); | ||||
|                     // Asumimos que podemos extraer el IdFactura de la referencia | ||||
|                     if (!int.TryParse(referencia.Replace("SUSC-", ""), out int idFactura)) | ||||
|                     { | ||||
|                         respuesta.Errores.Add($"Línea #{respuesta.TotalRegistrosLeidos}: No se pudo extraer un ID de factura válido de la referencia '{referencia}'."); | ||||
|                         continue; | ||||
|                     } | ||||
|                     // ================================================================= | ||||
|                     // === FIN DE LA LÓGICA DE PARSEO                         === | ||||
|                     // ================================================================= | ||||
|  | ||||
|                     var factura = await _facturaRepository.GetByIdAsync(idFactura); | ||||
|                     if (factura == null) | ||||
| @@ -264,27 +236,24 @@ namespace GestionIntegral.Api.Services.Suscripciones | ||||
|                     var nuevoPago = new Pago | ||||
|                     { | ||||
|                         IdFactura = idFactura, | ||||
|                         FechaPago = DateTime.Now.Date, // O la fecha que venga en el archivo | ||||
|                         IdFormaPago = 1, // 1 = Débito Automático | ||||
|                         FechaPago = DateTime.Now.Date, | ||||
|                         IdFormaPago = 1, | ||||
|                         Monto = factura.ImporteFinal, | ||||
|                         IdUsuarioRegistro = idUsuario, | ||||
|                         Referencia = $"Lote {factura.IdLoteDebito} - Banco" | ||||
|                     }; | ||||
|  | ||||
|                     if (estadoProceso == "AP") // "AP" = Aprobado (Asunción) | ||||
|                     if (estadoProceso == "AP") | ||||
|                     { | ||||
|                         nuevoPago.Estado = "Aprobado"; | ||||
|                         await _pagoRepository.CreateAsync(nuevoPago, transaction); | ||||
|                         await _facturaRepository.UpdateEstadoAsync(idFactura, "Pagada", transaction); | ||||
|                         await _facturaRepository.UpdateEstadoPagoAsync(idFactura, "Pagada", transaction); | ||||
|                         respuesta.PagosAprobados++; | ||||
|                     } | ||||
|                     else // Asumimos que cualquier otra cosa es Rechazado | ||||
|                     else | ||||
|                     { | ||||
|                         nuevoPago.Estado = "Rechazado"; | ||||
|                         await _pagoRepository.CreateAsync(nuevoPago, transaction); | ||||
|                         factura.Estado = "Rechazada"; | ||||
|                         factura.MotivoRechazo = motivoRechazo; | ||||
|                         // Necesitamos un método en el repo para actualizar estado y motivo | ||||
|                         await _facturaRepository.UpdateEstadoYMotivoAsync(idFactura, "Rechazada", motivoRechazo, transaction); | ||||
|                         respuesta.PagosRechazados++; | ||||
|                     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user