Este commit introduce la funcionalidad completa para la facturación mensual, la gestión de promociones y la comunicación con el cliente en el módulo de suscripciones. Backend: - Se añade el servicio de Facturación que calcula automáticamente los importes mensuales basándose en las suscripciones activas, días de entrega y precios. - Se implementa el servicio DebitoAutomaticoService, capaz de generar el archivo de texto plano para "Pago Directo Galicia" y de procesar el archivo de respuesta para la conciliación de pagos. - Se desarrolla el ABM completo para Promociones (Servicio, Repositorio, Controlador y DTOs), permitiendo la creación de descuentos por porcentaje o monto fijo. - Se implementa la lógica para asignar y desasignar promociones a suscripciones específicas. - Se añade un servicio de envío de email (EmailService) integrado con MailKit y un endpoint para notificar facturas a los clientes. - Se crea la lógica para registrar pagos manuales (efectivo, tarjeta, etc.) y actualizar el estado de las facturas. - Se añaden todos los permisos necesarios a la base de datos para segmentar el acceso a las nuevas funcionalidades. Frontend: - Se crea la página de Facturación, que permite al usuario seleccionar un período, generar la facturación, listar los resultados y generar el archivo de débito para el banco. - Se implementa la funcionalidad para subir y procesar el archivo de respuesta del banco, actualizando la UI en consecuencia. - Se añade la página completa para el ABM de Promociones. - Se integra un modal en la gestión de suscripciones para asignar y desasignar promociones a un cliente. - Se añade la opción "Enviar Email" en el menú de acciones de las facturas, conectada al nuevo endpoint del backend. - Se completan y corrigen los componentes `PagoManualModal` y `FacturacionPage` para incluir la lógica de registro de pagos y solucionar errores de TypeScript.
243 lines
12 KiB
C#
243 lines
12 KiB
C#
using GestionIntegral.Api.Data;
|
|
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
|
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
|
using GestionIntegral.Api.Dtos.Suscripciones;
|
|
using GestionIntegral.Api.Models.Distribucion;
|
|
using GestionIntegral.Api.Models.Suscripciones;
|
|
using GestionIntegral.Api.Services.Comunicaciones;
|
|
using System.Data;
|
|
|
|
namespace GestionIntegral.Api.Services.Suscripciones
|
|
{
|
|
public class FacturacionService : IFacturacionService
|
|
{
|
|
private readonly ISuscripcionRepository _suscripcionRepository;
|
|
private readonly IFacturaRepository _facturaRepository;
|
|
private readonly IPrecioRepository _precioRepository;
|
|
private readonly IPromocionRepository _promocionRepository;
|
|
private readonly IRecargoZonaRepository _recargoZonaRepository; // Para futura implementación
|
|
private readonly ISuscriptorRepository _suscriptorRepository; // Para obtener zona del suscriptor
|
|
private readonly DbConnectionFactory _connectionFactory;
|
|
private readonly IEmailService _emailService;
|
|
private readonly ILogger<FacturacionService> _logger;
|
|
|
|
public FacturacionService(
|
|
ISuscripcionRepository suscripcionRepository,
|
|
IFacturaRepository facturaRepository,
|
|
IPrecioRepository precioRepository,
|
|
IPromocionRepository promocionRepository,
|
|
IRecargoZonaRepository recargoZonaRepository,
|
|
ISuscriptorRepository suscriptorRepository,
|
|
DbConnectionFactory connectionFactory,
|
|
IEmailService emailService,
|
|
ILogger<FacturacionService> logger)
|
|
{
|
|
_suscripcionRepository = suscripcionRepository;
|
|
_facturaRepository = facturaRepository;
|
|
_precioRepository = precioRepository;
|
|
_promocionRepository = promocionRepository;
|
|
_recargoZonaRepository = recargoZonaRepository;
|
|
_suscriptorRepository = suscriptorRepository;
|
|
_connectionFactory = connectionFactory;
|
|
_emailService = emailService;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<(bool Exito, string? Mensaje, int FacturasGeneradas)> GenerarFacturacionMensual(int anio, int mes, int idUsuario)
|
|
{
|
|
var periodo = $"{anio}-{mes:D2}";
|
|
_logger.LogInformation("Iniciando generación de facturación para el período {Periodo} por usuario {IdUsuario}", periodo, idUsuario);
|
|
|
|
using var connection = _connectionFactory.CreateConnection();
|
|
await (connection as System.Data.Common.DbConnection)!.OpenAsync();
|
|
using var transaction = connection.BeginTransaction();
|
|
|
|
try
|
|
{
|
|
var suscripcionesActivas = await _suscripcionRepository.GetAllActivasParaFacturacion(periodo, transaction);
|
|
if (!suscripcionesActivas.Any())
|
|
{
|
|
return (true, "No se encontraron suscripciones activas para facturar en el período especificado.", 0);
|
|
}
|
|
|
|
int facturasGeneradas = 0;
|
|
foreach (var suscripcion in suscripcionesActivas)
|
|
{
|
|
var facturaExistente = await _facturaRepository.GetBySuscripcionYPeriodoAsync(suscripcion.IdSuscripcion, periodo, transaction);
|
|
if (facturaExistente != null)
|
|
{
|
|
_logger.LogWarning("Ya existe una factura (ID: {IdFactura}) para la suscripción ID {IdSuscripcion} en el período {Periodo}. Se omite.", facturaExistente.IdFactura, suscripcion.IdSuscripcion, periodo);
|
|
continue;
|
|
}
|
|
|
|
// --- LÓGICA DE PROMOCIONES ---
|
|
var primerDiaMes = new DateTime(anio, mes, 1);
|
|
var promocionesAplicables = await _promocionRepository.GetPromocionesActivasParaSuscripcion(suscripcion.IdSuscripcion, primerDiaMes, transaction);
|
|
|
|
decimal importeBruto = await CalcularImporteParaSuscripcion(suscripcion, anio, mes, transaction);
|
|
decimal descuentoTotal = 0;
|
|
|
|
// Aplicar promociones de descuento
|
|
foreach (var promo in promocionesAplicables.Where(p => p.TipoPromocion == "Porcentaje"))
|
|
{
|
|
descuentoTotal += (importeBruto * promo.Valor) / 100;
|
|
}
|
|
foreach (var promo in promocionesAplicables.Where(p => p.TipoPromocion == "MontoFijo"))
|
|
{
|
|
descuentoTotal += promo.Valor;
|
|
}
|
|
// La bonificación de días se aplicaría idealmente dentro de CalcularImporteParaSuscripcion,
|
|
// pero por simplicidad, aquí solo manejamos descuentos sobre el total.
|
|
|
|
if (importeBruto <= 0)
|
|
{
|
|
_logger.LogInformation("Suscripción ID {IdSuscripcion} no tiene importe a facturar para el período {Periodo}. Se omite.", suscripcion.IdSuscripcion, periodo);
|
|
continue;
|
|
}
|
|
|
|
var importeFinal = importeBruto - descuentoTotal;
|
|
if (importeFinal < 0) importeFinal = 0; // El importe no puede ser negativo
|
|
|
|
var nuevaFactura = new Factura
|
|
{
|
|
IdSuscripcion = suscripcion.IdSuscripcion,
|
|
Periodo = periodo,
|
|
FechaEmision = DateTime.Now.Date,
|
|
FechaVencimiento = new DateTime(anio, mes, 10).AddMonths(1),
|
|
ImporteBruto = importeBruto,
|
|
DescuentoAplicado = descuentoTotal,
|
|
ImporteFinal = importeFinal,
|
|
Estado = "Pendiente de Facturar"
|
|
};
|
|
|
|
var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction);
|
|
if (facturaCreada == null) throw new DataException($"No se pudo crear el registro de factura para la suscripción ID {suscripcion.IdSuscripcion}");
|
|
|
|
facturasGeneradas++;
|
|
}
|
|
|
|
transaction.Commit();
|
|
_logger.LogInformation("Finalizada la generación de facturación para {Periodo}. Total generadas: {FacturasGeneradas}", periodo, facturasGeneradas);
|
|
return (true, $"Proceso completado. Se generaron {facturasGeneradas} facturas.", facturasGeneradas);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try { transaction.Rollback(); } catch { }
|
|
_logger.LogError(ex, "Error crítico durante la generación de facturación para el período {Periodo}", periodo);
|
|
return (false, "Error interno del servidor al generar la facturación.", 0);
|
|
}
|
|
}
|
|
|
|
private async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
|
{
|
|
decimal importeTotal = 0;
|
|
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
|
var fechaActual = new DateTime(anio, mes, 1);
|
|
|
|
while (fechaActual.Month == mes)
|
|
{
|
|
// La suscripción debe estar activa en este día
|
|
if (fechaActual.Date >= suscripcion.FechaInicio.Date &&
|
|
(suscripcion.FechaFin == null || fechaActual.Date <= suscripcion.FechaFin.Value.Date))
|
|
{
|
|
var diaSemanaChar = GetCharDiaSemana(fechaActual.DayOfWeek);
|
|
if (diasDeEntrega.Contains(diaSemanaChar))
|
|
{
|
|
var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(suscripcion.IdPublicacion, fechaActual, transaction);
|
|
if (precioActivo != null)
|
|
{
|
|
importeTotal += GetPrecioDelDia(precioActivo, fechaActual.DayOfWeek);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("No se encontró precio para la publicación ID {IdPublicacion} en la fecha {Fecha}", suscripcion.IdPublicacion, fechaActual.Date);
|
|
}
|
|
}
|
|
}
|
|
fechaActual = fechaActual.AddDays(1);
|
|
}
|
|
return importeTotal;
|
|
}
|
|
|
|
public async Task<IEnumerable<FacturaDto>> ObtenerFacturasPorPeriodo(int anio, int mes)
|
|
{
|
|
var periodo = $"{anio}-{mes:D2}";
|
|
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo);
|
|
|
|
return facturasData.Select(data => new FacturaDto
|
|
{
|
|
IdFactura = data.Factura.IdFactura,
|
|
IdSuscripcion = data.Factura.IdSuscripcion,
|
|
Periodo = data.Factura.Periodo,
|
|
FechaEmision = data.Factura.FechaEmision.ToString("yyyy-MM-dd"),
|
|
FechaVencimiento = data.Factura.FechaVencimiento.ToString("yyyy-MM-dd"),
|
|
ImporteFinal = data.Factura.ImporteFinal,
|
|
Estado = data.Factura.Estado,
|
|
NumeroFactura = data.Factura.NumeroFactura,
|
|
NombreSuscriptor = data.NombreSuscriptor,
|
|
NombrePublicacion = data.NombrePublicacion
|
|
});
|
|
}
|
|
|
|
public async Task<(bool Exito, string? Error)> EnviarFacturaPorEmail(int idFactura)
|
|
{
|
|
var factura = await _facturaRepository.GetByIdAsync(idFactura);
|
|
if (factura == null) return (false, "Factura no encontrada.");
|
|
if (string.IsNullOrEmpty(factura.NumeroFactura)) return (false, "La factura aún no tiene un número asignado por ARCA.");
|
|
|
|
var suscripcion = await _suscripcionRepository.GetByIdAsync(factura.IdSuscripcion);
|
|
if (suscripcion == null) return (false, "Suscripción asociada no encontrada.");
|
|
|
|
var suscriptor = await _suscriptorRepository.GetByIdAsync(suscripcion.IdSuscriptor);
|
|
if (suscriptor == null) return (false, "Suscriptor asociado no encontrado.");
|
|
if (string.IsNullOrEmpty(suscriptor.Email)) return (false, "El suscriptor no tiene una dirección de email configurada.");
|
|
|
|
try
|
|
{
|
|
var asunto = $"Tu factura del Diario El Día - Período {factura.Periodo}";
|
|
var cuerpo = $@"
|
|
<h1>Hola {suscriptor.NombreCompleto},</h1>
|
|
<p>Te adjuntamos los detalles de tu factura para el período {factura.Periodo}.</p>
|
|
<ul>
|
|
<li><strong>Número de Factura:</strong> {factura.NumeroFactura}</li>
|
|
<li><strong>Importe Total:</strong> ${factura.ImporteFinal:N2}</li>
|
|
<li><strong>Fecha de Vencimiento:</strong> {factura.FechaVencimiento:dd/MM/yyyy}</li>
|
|
</ul>
|
|
<p>Gracias por ser parte de nuestra comunidad de lectores.</p>
|
|
<p><em>Diario El Día</em></p>";
|
|
|
|
await _emailService.EnviarEmailAsync(suscriptor.Email, suscriptor.NombreCompleto, asunto, cuerpo);
|
|
return (true, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Falló el envío de email para la factura ID {IdFactura}", idFactura);
|
|
return (false, "Ocurrió un error al intentar enviar el email.");
|
|
}
|
|
}
|
|
|
|
private string GetCharDiaSemana(DayOfWeek dia) => dia switch
|
|
{
|
|
DayOfWeek.Sunday => "D",
|
|
DayOfWeek.Monday => "L",
|
|
DayOfWeek.Tuesday => "M",
|
|
DayOfWeek.Wednesday => "X",
|
|
DayOfWeek.Thursday => "J",
|
|
DayOfWeek.Friday => "V",
|
|
DayOfWeek.Saturday => "S",
|
|
_ => ""
|
|
};
|
|
|
|
private decimal GetPrecioDelDia(Precio precio, DayOfWeek dia) => dia switch
|
|
{
|
|
DayOfWeek.Sunday => precio.Domingo ?? 0,
|
|
DayOfWeek.Monday => precio.Lunes ?? 0,
|
|
DayOfWeek.Tuesday => precio.Martes ?? 0,
|
|
DayOfWeek.Wednesday => precio.Miercoles ?? 0,
|
|
DayOfWeek.Thursday => precio.Jueves ?? 0,
|
|
DayOfWeek.Friday => precio.Viernes ?? 0,
|
|
DayOfWeek.Saturday => precio.Sabado ?? 0,
|
|
_ => 0
|
|
};
|
|
}
|
|
} |