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

@@ -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++;
}