Feat: Implementa auditoría de envíos de email y mejora la UX
Se introduce un sistema completo de logging para todas las comunicaciones por correo electrónico y se realizan mejoras significativas en la experiencia del usuario, tanto en la retroalimentación del sistema como en la estética de los emails enviados al cliente. ### ✨ Nuevas Características - **Auditoría y Log de Envíos de Email:** - Se ha creado una nueva tabla `com_EmailLogs` en la base de datos para registrar cada intento de envío de correo. - El `EmailService` ahora centraliza toda la lógica de logging, registrando automáticamente la fecha, destinatario, asunto, estado (`Enviado` o `Fallido`), y mensajes de error detallados. - Se implementó un nuevo `EmailLogService` y `EmailLogRepository` para gestionar estos registros. - **Historial de Envíos en la Interfaz de Usuario:** - Se añade un nuevo ícono de "Historial" (<span style="color: #607d8b;">📧</span>) junto a cada factura en la página de "Consulta de Facturas". - Al hacer clic, se abre un modal que muestra una tabla detallada con todos los intentos de envío para esa factura, incluyendo el estado y el motivo del error (si lo hubo). - Esto proporciona una trazabilidad completa y una herramienta de diagnóstico para el usuario final. ### 🔄 Refactorización y Mejoras - **Mensajes de Éxito Dinámicos:** - Se ha mejorado la retroalimentación al enviar una factura por PDF. El sistema ahora muestra un mensaje de éxito específico, como "El email... se ha enviado correctamente a suscriptor@email.com", en lugar de un mensaje técnico genérico. - Se ajustó la cadena de llamadas (`Controller` -> `Service`) para que el email del destinatario esté disponible para la respuesta de la API. - **Diseño Unificado de Emails:** - Se ha rediseñado el template HTML para el "Aviso de Cuenta Mensual" para que coincida con la estética del email de "Envío de Factura PDF". - Ambos correos ahora presentan un diseño profesional y consistente, con cabecera, logo y pie de página, reforzando la imagen de marca. - **Manejo de Errores de Email Mejorado:** - El `EmailService` ahora captura excepciones específicas de la librería `MailKit` (ej. `SmtpCommandException`). - Esto permite registrar en el log errores mucho más precisos y útiles, como rechazos de destinatarios por parte del servidor (`User unknown`), fallos de autenticación, etc., que ahora son visibles en el `Tooltip` del historial.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
||||
using GestionIntegral.Api.Data.Repositories.Usuarios;
|
||||
using GestionIntegral.Api.Dtos.Comunicaciones;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
{
|
||||
public class EmailLogService : IEmailLogService
|
||||
{
|
||||
private readonly IEmailLogRepository _emailLogRepository;
|
||||
private readonly IUsuarioRepository _usuarioRepository;
|
||||
|
||||
public EmailLogService(IEmailLogRepository emailLogRepository, IUsuarioRepository usuarioRepository)
|
||||
{
|
||||
_emailLogRepository = emailLogRepository;
|
||||
_usuarioRepository = usuarioRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EmailLogDto>> ObtenerHistorialPorReferencia(string referenciaId)
|
||||
{
|
||||
var logs = await _emailLogRepository.GetByReferenceAsync(referenciaId);
|
||||
if (!logs.Any())
|
||||
{
|
||||
return Enumerable.Empty<EmailLogDto>();
|
||||
}
|
||||
|
||||
// Optimización N+1: Obtener todos los usuarios necesarios en una sola consulta
|
||||
var idsUsuarios = logs
|
||||
.Where(l => l.IdUsuarioDisparo.HasValue)
|
||||
.Select(l => l.IdUsuarioDisparo!.Value)
|
||||
.Distinct();
|
||||
|
||||
var usuariosDict = new Dictionary<int, string>();
|
||||
if (idsUsuarios.Any())
|
||||
{
|
||||
var usuarios = await _usuarioRepository.GetByIdsAsync(idsUsuarios);
|
||||
usuariosDict = usuarios.ToDictionary(u => u.Id, u => $"{u.Nombre} {u.Apellido}");
|
||||
}
|
||||
|
||||
// Mapear a DTO
|
||||
return logs.Select(log => new EmailLogDto
|
||||
{
|
||||
FechaEnvio = log.FechaEnvio,
|
||||
Estado = log.Estado,
|
||||
Asunto = log.Asunto,
|
||||
DestinatarioEmail = log.DestinatarioEmail,
|
||||
Error = log.Error,
|
||||
NombreUsuarioDisparo = log.IdUsuarioDisparo.HasValue
|
||||
? usuariosDict.GetValueOrDefault(log.IdUsuarioDisparo.Value, "Usuario Desconocido")
|
||||
: "Sistema"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
||||
using GestionIntegral.Api.Models.Comunicaciones;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
@@ -10,14 +11,22 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
{
|
||||
private readonly MailSettings _mailSettings;
|
||||
private readonly ILogger<EmailService> _logger;
|
||||
private readonly IEmailLogRepository _emailLogRepository;
|
||||
|
||||
public EmailService(IOptions<MailSettings> mailSettings, ILogger<EmailService> logger)
|
||||
public EmailService(
|
||||
IOptions<MailSettings> mailSettings,
|
||||
ILogger<EmailService> logger,
|
||||
IEmailLogRepository emailLogRepository) // Inyectar el nuevo repositorio
|
||||
{
|
||||
_mailSettings = mailSettings.Value;
|
||||
_logger = logger;
|
||||
_emailLogRepository = emailLogRepository;
|
||||
}
|
||||
|
||||
public async Task EnviarEmailAsync(string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml, byte[]? attachment = null, string? attachmentName = null)
|
||||
public async Task EnviarEmailAsync(
|
||||
string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml,
|
||||
byte[]? attachment = null, string? attachmentName = null,
|
||||
string? origen = null, string? referenciaId = null, int? idUsuarioDisparo = null)
|
||||
{
|
||||
var email = new MimeMessage();
|
||||
email.Sender = new MailboxAddress(_mailSettings.SenderName, _mailSettings.SenderEmail);
|
||||
@@ -32,26 +41,14 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
}
|
||||
email.Body = builder.ToMessageBody();
|
||||
|
||||
using var smtp = new SmtpClient();
|
||||
try
|
||||
{
|
||||
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
||||
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
||||
await smtp.SendAsync(email);
|
||||
_logger.LogInformation("Email enviado exitosamente a {Destinatario}", destinatarioEmail);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al enviar email a {Destinatario}", destinatarioEmail);
|
||||
throw; // Relanzar para que el servicio que lo llamó sepa que falló
|
||||
}
|
||||
finally
|
||||
{
|
||||
await smtp.DisconnectAsync(true);
|
||||
}
|
||||
// Llamar al método centralizado de envío y logging
|
||||
await SendAndLogEmailAsync(email, origen, referenciaId, idUsuarioDisparo);
|
||||
}
|
||||
|
||||
public async Task EnviarEmailConsolidadoAsync(string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml, List<(byte[] content, string name)> adjuntos)
|
||||
public async Task EnviarEmailConsolidadoAsync(
|
||||
string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml,
|
||||
List<(byte[] content, string name)> adjuntos,
|
||||
string? origen = null, string? referenciaId = null, int? idUsuarioDisparo = null)
|
||||
{
|
||||
var email = new MimeMessage();
|
||||
email.Sender = new MailboxAddress(_mailSettings.SenderName, _mailSettings.SenderEmail);
|
||||
@@ -60,7 +57,6 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
email.Subject = asunto;
|
||||
|
||||
var builder = new BodyBuilder { HtmlBody = cuerpoHtml };
|
||||
|
||||
if (adjuntos != null)
|
||||
{
|
||||
foreach (var adjunto in adjuntos)
|
||||
@@ -68,25 +64,80 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
builder.Attachments.Add(adjunto.name, adjunto.content, ContentType.Parse("application/pdf"));
|
||||
}
|
||||
}
|
||||
|
||||
email.Body = builder.ToMessageBody();
|
||||
|
||||
// Llamar al método centralizado de envío y logging
|
||||
await SendAndLogEmailAsync(email, origen, referenciaId, idUsuarioDisparo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Método privado que centraliza el envío de correo y el registro de logs.
|
||||
/// </summary>
|
||||
private async Task SendAndLogEmailAsync(MimeMessage emailMessage, string? origen, string? referenciaId, int? idUsuarioDisparo)
|
||||
{
|
||||
var destinatario = emailMessage.To.Mailboxes.FirstOrDefault()?.Address ?? "desconocido";
|
||||
|
||||
var log = new EmailLog
|
||||
{
|
||||
FechaEnvio = DateTime.Now,
|
||||
DestinatarioEmail = destinatario,
|
||||
Asunto = emailMessage.Subject,
|
||||
Origen = origen,
|
||||
ReferenciaId = referenciaId,
|
||||
IdUsuarioDisparo = idUsuarioDisparo
|
||||
};
|
||||
|
||||
using var smtp = new SmtpClient();
|
||||
try
|
||||
{
|
||||
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
||||
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
||||
await smtp.SendAsync(email);
|
||||
_logger.LogInformation("Email consolidado enviado exitosamente a {Destinatario}", destinatarioEmail);
|
||||
await smtp.SendAsync(emailMessage);
|
||||
|
||||
log.Estado = "Enviado";
|
||||
_logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||
}
|
||||
catch (Exception ex)
|
||||
// Capturamos excepciones específicas de MailKit para obtener errores más detallados.
|
||||
catch (SmtpCommandException scEx)
|
||||
{
|
||||
_logger.LogError(ex, "Error al enviar email consolidado a {Destinatario}", destinatarioEmail);
|
||||
// Este error ocurre cuando el servidor SMTP rechaza un comando.
|
||||
// Es el caso más común para direcciones de email inválidas que son rechazadas inmediatamente.
|
||||
_logger.LogError(scEx, "Error de comando SMTP al enviar a {Destinatario}. StatusCode: {StatusCode}", destinatario, scEx.StatusCode);
|
||||
log.Estado = "Fallido";
|
||||
log.Error = $"Error del servidor: ({scEx.StatusCode}) {scEx.Message}";
|
||||
throw;
|
||||
}
|
||||
catch (AuthenticationException authEx)
|
||||
{
|
||||
// Error específico de autenticación.
|
||||
_logger.LogError(authEx, "Error de autenticación con el servidor SMTP.");
|
||||
log.Estado = "Fallido";
|
||||
log.Error = "Error de autenticación. Revise las credenciales de correo.";
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) // Captura genérica para cualquier otro problema (conexión, etc.)
|
||||
{
|
||||
_logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||
log.Estado = "Fallido";
|
||||
log.Error = ex.Message;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await smtp.DisconnectAsync(true);
|
||||
if (smtp.IsConnected)
|
||||
{
|
||||
await smtp.DisconnectAsync(true);
|
||||
}
|
||||
|
||||
// Guardar el log en la base de datos, sin importar el resultado del envío
|
||||
try
|
||||
{
|
||||
await _emailLogRepository.CreateAsync(log);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
_logger.LogError(logEx, "FALLO CRÍTICO: No se pudo guardar el log del email para {Destinatario}", destinatario);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using GestionIntegral.Api.Dtos.Comunicaciones;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
{
|
||||
public interface IEmailLogService
|
||||
{
|
||||
Task<IEnumerable<EmailLogDto>> ObtenerHistorialPorReferencia(string referenciaId);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,50 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
{
|
||||
public interface IEmailService
|
||||
{
|
||||
Task EnviarEmailAsync(string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml, byte[]? attachment = null, string? attachmentName = null);
|
||||
Task EnviarEmailConsolidadoAsync(string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml, List<(byte[] content, string name)> adjuntos);
|
||||
/// <summary>
|
||||
/// Envía un correo electrónico a un único destinatario, con la posibilidad de adjuntar un archivo.
|
||||
/// Este método también registra automáticamente el resultado del envío en la base de datos.
|
||||
/// </summary>
|
||||
/// <param name="destinatarioEmail">La dirección de correo del destinatario.</param>
|
||||
/// <param name="destinatarioNombre">El nombre del destinatario.</param>
|
||||
/// <param name="asunto">El asunto del correo.</param>
|
||||
/// <param name="cuerpoHtml">El contenido del correo en formato HTML.</param>
|
||||
/// <param name="attachment">Los bytes del archivo a adjuntar (opcional).</param>
|
||||
/// <param name="attachmentName">El nombre del archivo adjunto (requerido si se provee attachment).</param>
|
||||
/// <param name="origen">Identificador del proceso que dispara el email (ej. "EnvioManualPDF"). Para logging.</param>
|
||||
/// <param name="referenciaId">ID de la entidad relacionada (ej. "Factura-59"). Para logging.</param>
|
||||
/// <param name="idUsuarioDisparo">ID del usuario que inició la acción (si aplica). Para logging.</param>
|
||||
Task EnviarEmailAsync(
|
||||
string destinatarioEmail,
|
||||
string destinatarioNombre,
|
||||
string asunto,
|
||||
string cuerpoHtml,
|
||||
byte[]? attachment = null,
|
||||
string? attachmentName = null,
|
||||
string? origen = null,
|
||||
string? referenciaId = null,
|
||||
int? idUsuarioDisparo = null);
|
||||
|
||||
/// <summary>
|
||||
/// Envía un correo electrónico a un único destinatario, con la posibilidad de adjuntar múltiples archivos.
|
||||
/// Este método también registra automáticamente el resultado del envío en la base de datos.
|
||||
/// </summary>
|
||||
/// <param name="destinatarioEmail">La dirección de correo del destinatario.</param>
|
||||
/// <param name="destinatarioNombre">El nombre del destinatario.</param>
|
||||
/// <param name="asunto">El asunto del correo.</param>
|
||||
/// <param name="cuerpoHtml">El contenido del correo en formato HTML.</param>
|
||||
/// <param name="adjuntos">Una lista de tuplas que contienen los bytes y el nombre de cada archivo a adjuntar.</param>
|
||||
/// <param name="origen">Identificador del proceso que dispara el email (ej. "FacturacionMensual"). Para logging.</param>
|
||||
/// <param name="referenciaId">ID de la entidad relacionada (ej. "Suscriptor-3"). Para logging.</param>
|
||||
/// <param name="idUsuarioDisparo">ID del usuario que inició la acción (si aplica). Para logging.</param>
|
||||
Task EnviarEmailConsolidadoAsync(
|
||||
string destinatarioEmail,
|
||||
string destinatarioNombre,
|
||||
string asunto,
|
||||
string cuerpoHtml,
|
||||
List<(byte[] content, string name)> adjuntos,
|
||||
string? origen = null,
|
||||
string? referenciaId = null,
|
||||
int? idUsuarioDisparo = null);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<FacturacionService> _logger;
|
||||
private readonly string _facturasPdfPath;
|
||||
private const string LogoUrl = "https://www.eldia.com/img/header/eldia.png";
|
||||
|
||||
public FacturacionService(
|
||||
ISuscripcionRepository suscripcionRepository,
|
||||
@@ -261,24 +262,18 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
return resumenes.ToList();
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> EnviarFacturaPdfPorEmail(int idFactura)
|
||||
public async Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario)
|
||||
{
|
||||
try
|
||||
{
|
||||
var factura = await _facturaRepository.GetByIdAsync(idFactura);
|
||||
if (factura == null) return (false, "Factura no encontrada.");
|
||||
if (string.IsNullOrEmpty(factura.NumeroFactura)) return (false, "La factura no tiene un número oficial asignado para generar el PDF.");
|
||||
if (factura.EstadoPago == "Anulada") return (false, "No se puede enviar email de una factura anulada.");
|
||||
if (factura == null) return (false, "Factura no encontrada.", null);
|
||||
if (string.IsNullOrEmpty(factura.NumeroFactura)) return (false, "La factura no tiene un número asignado.", null);
|
||||
if (factura.EstadoPago == "Anulada") return (false, "No se puede enviar email de una factura anulada.", null);
|
||||
|
||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(factura.IdSuscriptor);
|
||||
if (suscriptor == null || string.IsNullOrEmpty(suscriptor.Email)) return (false, "El suscriptor no es válido o no tiene email.");
|
||||
if (suscriptor == null || string.IsNullOrEmpty(suscriptor.Email)) return (false, "El suscriptor no es válido o no tiene email.", null);
|
||||
|
||||
var detalles = await _facturaDetalleRepository.GetDetallesPorFacturaIdAsync(factura.IdFactura);
|
||||
var primeraSuscripcionId = detalles.FirstOrDefault()?.IdSuscripcion ?? 0;
|
||||
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(primeraSuscripcionId);
|
||||
var empresa = await _empresaRepository.GetByIdAsync(publicacion?.IdEmpresa ?? 0);
|
||||
|
||||
// --- LÓGICA DE BÚSQUEDA Y ADJUNTO DE PDF ---
|
||||
byte[]? pdfAttachment = null;
|
||||
string? pdfFileName = null;
|
||||
var rutaCompleta = Path.Combine(_facturasPdfPath, $"{factura.NumeroFactura}.pdf");
|
||||
@@ -286,122 +281,190 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
if (File.Exists(rutaCompleta))
|
||||
{
|
||||
pdfAttachment = await File.ReadAllBytesAsync(rutaCompleta);
|
||||
pdfFileName = $"Factura_{empresa?.Nombre?.Replace(" ", "")}_{factura.NumeroFactura}.pdf";
|
||||
_logger.LogInformation("Adjuntando PDF encontrado en: {Ruta}", rutaCompleta);
|
||||
pdfFileName = $"Factura_{factura.NumeroFactura}.pdf";
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Se intentó enviar la factura {NumeroFactura} pero no se encontró el PDF en la ruta: {Ruta}", factura.NumeroFactura, rutaCompleta);
|
||||
return (false, "No se encontró el archivo PDF correspondiente en el servidor. Verifique que el archivo exista y el nombre coincida con el número de factura.");
|
||||
_logger.LogWarning("No se encontró el PDF para la factura {NumeroFactura}", factura.NumeroFactura);
|
||||
return (false, "No se encontró el archivo PDF correspondiente en el servidor.", null);
|
||||
}
|
||||
|
||||
string asunto = $"Tu Factura Oficial - Diario El Día - Período {factura.Periodo}";
|
||||
string cuerpoHtml = $"<div style='font-family: Arial, sans-serif;'><h2>Hola {suscriptor.NombreCompleto},</h2><p>Adjuntamos tu factura oficial número <strong>{factura.NumeroFactura}</strong> correspondiente al período <strong>{factura.Periodo}</strong>.</p><p>Gracias por ser parte de nuestra comunidad de lectores.</p><p><em>Diario El Día</em></p></div>";
|
||||
string asunto = $"Factura Electrónica - Período {factura.Periodo}";
|
||||
string cuerpoHtml = ConstruirCuerpoEmailFacturaPdf(suscriptor, factura);
|
||||
|
||||
// Pasamos los nuevos parámetros de contexto al EmailService.
|
||||
await _emailService.EnviarEmailAsync(
|
||||
destinatarioEmail: suscriptor.Email,
|
||||
destinatarioNombre: suscriptor.NombreCompleto,
|
||||
asunto: asunto,
|
||||
cuerpoHtml: cuerpoHtml,
|
||||
attachment: pdfAttachment,
|
||||
attachmentName: pdfFileName,
|
||||
origen: "EnvioManualPDF",
|
||||
referenciaId: $"Factura-{idFactura}",
|
||||
idUsuarioDisparo: idUsuario);
|
||||
|
||||
await _emailService.EnviarEmailAsync(suscriptor.Email, suscriptor.NombreCompleto, asunto, cuerpoHtml, pdfAttachment, pdfFileName);
|
||||
_logger.LogInformation("Email con factura PDF ID {IdFactura} enviado para Suscriptor ID {IdSuscriptor}", idFactura, suscriptor.IdSuscriptor);
|
||||
return (true, null);
|
||||
|
||||
return (true, null, suscriptor.Email);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Falló el envío de email con PDF para la factura ID {IdFactura}", idFactura);
|
||||
return (false, "Ocurrió un error al intentar enviar el email con la factura.");
|
||||
// El error ya será logueado por EmailService, pero lo relanzamos para que el controller lo maneje.
|
||||
// En este caso, simplemente devolvemos la tupla de error.
|
||||
return (false, "Ocurrió un error al intentar enviar el email con la factura.", null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> EnviarAvisoCuentaPorEmail(int anio, int mes, int idSuscriptor)
|
||||
{
|
||||
var periodo = $"{anio}-{mes:D2}";
|
||||
try
|
||||
{
|
||||
var facturasConEmpresa = await _facturaRepository.GetFacturasConEmpresaAsync(idSuscriptor, periodo);
|
||||
if (!facturasConEmpresa.Any()) return (false, "No se encontraron facturas para este suscriptor en el período.");
|
||||
|
||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(idSuscriptor);
|
||||
if (suscriptor == null || string.IsNullOrEmpty(suscriptor.Email)) return (false, "El suscriptor no es válido o no tiene email.");
|
||||
|
||||
var resumenHtml = new StringBuilder();
|
||||
var adjuntos = new List<(byte[] content, string name)>();
|
||||
|
||||
foreach (var item in facturasConEmpresa.Where(f => f.Factura.EstadoPago != "Anulada"))
|
||||
var periodo = $"{anio}-{mes:D2}";
|
||||
try
|
||||
{
|
||||
var factura = item.Factura;
|
||||
var nombreEmpresa = item.NombreEmpresa;
|
||||
var facturasConEmpresa = await _facturaRepository.GetFacturasConEmpresaAsync(idSuscriptor, periodo);
|
||||
if (!facturasConEmpresa.Any()) return (false, "No se encontraron facturas para este suscriptor en el período.");
|
||||
|
||||
var detalles = await _facturaDetalleRepository.GetDetallesPorFacturaIdAsync(factura.IdFactura);
|
||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(idSuscriptor);
|
||||
if (suscriptor == null || string.IsNullOrEmpty(suscriptor.Email)) return (false, "El suscriptor no es válido o no tiene email.");
|
||||
|
||||
resumenHtml.Append($"<h4 style='margin-top: 20px; margin-bottom: 10px; color: #34515e;'>Resumen para {nombreEmpresa}</h4>");
|
||||
resumenHtml.Append("<table style='width: 100%; border-collapse: collapse; font-size: 0.9em;'>");
|
||||
|
||||
foreach (var detalle in detalles)
|
||||
var resumenHtml = new StringBuilder();
|
||||
var adjuntos = new List<(byte[] content, string name)>();
|
||||
|
||||
foreach (var item in facturasConEmpresa.Where(f => f.Factura.EstadoPago != "Anulada"))
|
||||
{
|
||||
resumenHtml.Append($"<tr><td style='padding: 5px; border-bottom: 1px solid #eee;'>{detalle.Descripcion}</td><td style='padding: 5px; border-bottom: 1px solid #eee; text-align: right;'>${detalle.ImporteNeto:N2}</td></tr>");
|
||||
}
|
||||
|
||||
var ajustes = await _ajusteRepository.GetAjustesPorIdFacturaAsync(factura.IdFactura);
|
||||
if (ajustes.Any())
|
||||
{
|
||||
foreach (var ajuste in ajustes)
|
||||
var factura = item.Factura;
|
||||
var nombreEmpresa = item.NombreEmpresa;
|
||||
|
||||
var detalles = await _facturaDetalleRepository.GetDetallesPorFacturaIdAsync(factura.IdFactura);
|
||||
|
||||
resumenHtml.Append($"<h4 style='margin-top: 20px; margin-bottom: 10px; color: #34515e;'>Resumen para {nombreEmpresa}</h4>");
|
||||
resumenHtml.Append("<table style='width: 100%; border-collapse: collapse; font-size: 0.9em;'>");
|
||||
|
||||
foreach (var detalle in detalles)
|
||||
{
|
||||
bool esCredito = ajuste.TipoAjuste == "Credito";
|
||||
string colorMonto = esCredito ? "#5cb85c" : "#d9534f";
|
||||
string signo = esCredito ? "-" : "+";
|
||||
resumenHtml.Append($"<tr><td style='padding: 5px; border-bottom: 1px solid #eee; font-style: italic;'>Ajuste: {ajuste.Motivo}</td><td style='padding: 5px; border-bottom: 1px solid #eee; text-align: right; color: {colorMonto}; font-style: italic;'>{signo} ${ajuste.Monto:N2}</td></tr>");
|
||||
resumenHtml.Append($"<tr><td style='padding: 5px; border-bottom: 1px solid #eee;'>{detalle.Descripcion}</td><td style='padding: 5px; border-bottom: 1px solid #eee; text-align: right;'>${detalle.ImporteNeto:N2}</td></tr>");
|
||||
}
|
||||
|
||||
var ajustes = await _ajusteRepository.GetAjustesPorIdFacturaAsync(factura.IdFactura);
|
||||
if (ajustes.Any())
|
||||
{
|
||||
foreach (var ajuste in ajustes)
|
||||
{
|
||||
bool esCredito = ajuste.TipoAjuste == "Credito";
|
||||
string colorMonto = esCredito ? "#5cb85c" : "#d9534f";
|
||||
string signo = esCredito ? "-" : "+";
|
||||
resumenHtml.Append($"<tr><td style='padding: 5px; border-bottom: 1px solid #eee; font-style: italic;'>Ajuste: {ajuste.Motivo}</td><td style='padding: 5px; border-bottom: 1px solid #eee; text-align: right; color: {colorMonto}; font-style: italic;'>{signo} ${ajuste.Monto:N2}</td></tr>");
|
||||
}
|
||||
}
|
||||
|
||||
resumenHtml.Append($"<tr style='font-weight: bold;'><td style='padding: 5px;'>Subtotal</td><td style='padding: 5px; text-align: right;'>${factura.ImporteFinal:N2}</td></tr>");
|
||||
resumenHtml.Append("</table>");
|
||||
|
||||
if (!string.IsNullOrEmpty(factura.NumeroFactura))
|
||||
{
|
||||
var rutaCompleta = Path.Combine(_facturasPdfPath, $"{factura.NumeroFactura}.pdf");
|
||||
if (File.Exists(rutaCompleta))
|
||||
{
|
||||
byte[] pdfBytes = await File.ReadAllBytesAsync(rutaCompleta);
|
||||
string pdfFileName = $"Factura_{nombreEmpresa.Replace(" ", "")}_{factura.NumeroFactura}.pdf";
|
||||
adjuntos.Add((pdfBytes, pdfFileName));
|
||||
_logger.LogInformation("PDF adjuntado: {FileName}", pdfFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No se encontró el PDF para la factura {NumeroFactura} en la ruta: {Ruta}", factura.NumeroFactura, rutaCompleta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resumenHtml.Append($"<tr style='font-weight: bold;'><td style='padding: 5px;'>Subtotal</td><td style='padding: 5px; text-align: right;'>${factura.ImporteFinal:N2}</td></tr>");
|
||||
resumenHtml.Append("</table>");
|
||||
|
||||
if (!string.IsNullOrEmpty(factura.NumeroFactura))
|
||||
{
|
||||
var rutaCompleta = Path.Combine(_facturasPdfPath, $"{factura.NumeroFactura}.pdf");
|
||||
if (File.Exists(rutaCompleta))
|
||||
{
|
||||
byte[] pdfBytes = await File.ReadAllBytesAsync(rutaCompleta);
|
||||
string pdfFileName = $"Factura_{nombreEmpresa.Replace(" ", "")}_{factura.NumeroFactura}.pdf";
|
||||
adjuntos.Add((pdfBytes, pdfFileName));
|
||||
_logger.LogInformation("PDF adjuntado: {FileName}", pdfFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No se encontró el PDF para la factura {NumeroFactura} en la ruta: {Ruta}", factura.NumeroFactura, rutaCompleta);
|
||||
}
|
||||
}
|
||||
var totalGeneral = facturasConEmpresa.Where(f => f.Factura.EstadoPago != "Anulada").Sum(f => f.Factura.ImporteFinal);
|
||||
string asunto = $"Resumen de Cuenta - Período {periodo}";
|
||||
string cuerpoHtml = ConstruirCuerpoEmailConsolidado(suscriptor, periodo, resumenHtml.ToString(), totalGeneral);
|
||||
|
||||
// Añadir los parámetros de contexto aquí también
|
||||
await _emailService.EnviarEmailConsolidadoAsync(
|
||||
destinatarioEmail: suscriptor.Email,
|
||||
destinatarioNombre: suscriptor.NombreCompleto,
|
||||
asunto: asunto,
|
||||
cuerpoHtml: cuerpoHtml,
|
||||
adjuntos: adjuntos,
|
||||
origen: "FacturacionMensual",
|
||||
referenciaId: $"Suscriptor-{idSuscriptor}",
|
||||
idUsuarioDisparo: null); // Es null porque es un proceso automático del sistema
|
||||
|
||||
await _emailService.EnviarEmailConsolidadoAsync(suscriptor.Email, suscriptor.NombreCompleto, asunto, cuerpoHtml, adjuntos);
|
||||
_logger.LogInformation("Email consolidado para Suscriptor ID {IdSuscriptor} enviado para el período {Periodo}.", idSuscriptor, periodo);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Falló el envío de email consolidado para el suscriptor ID {IdSuscriptor} y período {Periodo}", idSuscriptor, periodo);
|
||||
return (false, "Ocurrió un error al intentar enviar el email consolidado.");
|
||||
}
|
||||
|
||||
var totalGeneral = facturasConEmpresa.Where(f => f.Factura.EstadoPago != "Anulada").Sum(f => f.Factura.ImporteFinal);
|
||||
string asunto = $"Resumen de Cuenta - Diario El Día - Período {periodo}";
|
||||
string cuerpoHtml = ConstruirCuerpoEmailConsolidado(suscriptor, periodo, resumenHtml.ToString(), totalGeneral);
|
||||
|
||||
await _emailService.EnviarEmailConsolidadoAsync(suscriptor.Email, suscriptor.NombreCompleto, asunto, cuerpoHtml, adjuntos);
|
||||
_logger.LogInformation("Email consolidado para Suscriptor ID {IdSuscriptor} enviado para el período {Periodo}.", idSuscriptor, periodo);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Falló el envío de email consolidado para el suscriptor ID {IdSuscriptor} y período {Periodo}", idSuscriptor, periodo);
|
||||
return (false, "Ocurrió un error al intentar enviar el email consolidado.");
|
||||
}
|
||||
}
|
||||
|
||||
private string ConstruirCuerpoEmailConsolidado(Suscriptor suscriptor, string periodo, string resumenHtml, decimal totalGeneral)
|
||||
{
|
||||
return $@"
|
||||
<div style='font-family: Arial, sans-serif; max-width: 600px; margin: auto; border: 1px solid #ddd; padding: 20px;'>
|
||||
<h3 style='color: #333;'>Hola {suscriptor.NombreCompleto},</h2>
|
||||
<p>Le enviamos el resumen de su cuenta para el período <strong>{periodo}</strong>.</p>
|
||||
{resumenHtml}
|
||||
<hr style='border: none; border-top: 1px solid #eee; margin: 20px 0;'/>
|
||||
<table style='width: 100%;'>
|
||||
<tr>
|
||||
<td style='font-size: 1.2em; font-weight: bold;'>TOTAL:</td>
|
||||
<td style='font-size: 1.4em; font-weight: bold; text-align: right; color: #34515e;'>${totalGeneral:N2}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style='margin-top: 25px;'>Si su pago es por débito automático, los importes se debitarán de su cuenta. Si utiliza otro medio de pago, por favor, regularice su situación.</p>
|
||||
<p>Gracias por ser parte de nuestra comunidad de lectores.</p>
|
||||
<p style='font-size: 0.9em; color: #777;'><em>Diario El Día</em></p>
|
||||
<div style='font-family: Arial, sans-serif; background-color: #f9f9f9; padding: 20px;'>
|
||||
<div style='max-width: 600px; margin: auto; background-color: #ffffff; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;'>
|
||||
<div style='background-color: #34515e; color: #ffffff; padding: 20px; text-align: center;'>
|
||||
<img src='{LogoUrl}' alt='El Día' style='max-width: 150px; margin-bottom: 10px;'>
|
||||
<h2>Resumen de su Cuenta</h2>
|
||||
</div>
|
||||
<div style='padding: 20px; color: #333;'>
|
||||
<h3 style='color: #34515e;'>Hola {suscriptor.NombreCompleto},</h3>
|
||||
<p>Le enviamos el resumen de su cuenta para el período <strong>{periodo}</strong>.</p>
|
||||
|
||||
<!-- Aquí se insertan las tablas de resumen generadas dinámicamente -->
|
||||
{resumenHtml}
|
||||
|
||||
<hr style='border: none; border-top: 1px solid #eee; margin: 20px 0;'/>
|
||||
<table style='width: 100%;'>
|
||||
<tr>
|
||||
<td style='font-size: 1.2em; font-weight: bold;'>TOTAL A ABONAR:</td>
|
||||
<td style='font-size: 1.4em; font-weight: bold; text-align: right; color: #34515e;'>${totalGeneral:N2}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style='margin-top: 25px;'>Si su pago es por débito automático, los importes se debitarán de su cuenta. Si utiliza otro medio de pago, por favor, regularice su situación.</p>
|
||||
<p>Gracias por ser parte de nuestra comunidad de lectores.</p>
|
||||
</div>
|
||||
<div style='background-color: #f2f2f2; padding: 15px; text-align: center; font-size: 0.8em; color: #777;'>
|
||||
<p>Este es un correo electrónico generado automáticamente. Por favor, no responda a este mensaje.</p>
|
||||
<p>© {DateTime.Now.Year} Diario El Día. Todos los derechos reservados.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>";
|
||||
}
|
||||
|
||||
private string ConstruirCuerpoEmailFacturaPdf(Suscriptor suscriptor, Factura factura)
|
||||
{
|
||||
return $@"
|
||||
<div style='font-family: Arial, sans-serif; background-color: #f9f9f9; padding: 20px;'>
|
||||
<div style='max-width: 600px; margin: auto; background-color: #ffffff; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;'>
|
||||
<div style='background-color: #34515e; color: #ffffff; padding: 20px; text-align: center;'>
|
||||
<img src='{LogoUrl}' alt='El Día' style='max-width: 150px; margin-bottom: 10px;'>
|
||||
<h2>Factura Electrónica Adjunta</h2>
|
||||
</div>
|
||||
<div style='padding: 20px; color: #333;'>
|
||||
<h3 style='color: #34515e;'>Hola {suscriptor.NombreCompleto},</h3>
|
||||
<p>Le enviamos adjunta su factura correspondiente al período <strong>{factura.Periodo}</strong>.</p>
|
||||
<h4 style='border-bottom: 2px solid #34515e; padding-bottom: 5px; margin-top: 30px;'>Resumen de la Factura</h4>
|
||||
<table style='width: 100%; border-collapse: collapse; margin-top: 15px;'>
|
||||
<tr style='border-bottom: 1px solid #eee;'><td style='padding: 8px; font-weight: bold;'>Número de Factura:</td><td style='padding: 8px; text-align: right;'>{factura.NumeroFactura}</td></tr>
|
||||
<tr style='border-bottom: 1px solid #eee;'><td style='padding: 8px; font-weight: bold;'>Período:</td><td style='padding: 8px; text-align: right;'>{factura.Periodo}</td></tr>
|
||||
<tr style='border-bottom: 1px solid #eee;'><td style='padding: 8px; font-weight: bold;'>Fecha de Envío:</td><td style='padding: 8px; text-align: right;'>{factura.FechaEmision:dd/MM/yyyy}</td></tr>
|
||||
<tr style='background-color: #f2f2f2;'><td style='padding: 12px; font-weight: bold; font-size: 1.1em;'>IMPORTE TOTAL:</td><td style='padding: 12px; text-align: right; font-weight: bold; font-size: 1.2em; color: #34515e;'>${factura.ImporteFinal:N2}</td></tr>
|
||||
</table>
|
||||
<p style='margin-top: 30px;'>Puede descargar y guardar el archivo PDF adjunto para sus registros.</p>
|
||||
<p>Gracias por ser parte de nuestra comunidad de lectores.</p>
|
||||
</div>
|
||||
<div style='background-color: #f2f2f2; padding: 15px; text-align: center; font-size: 0.8em; color: #777;'>
|
||||
<p>Este es un correo electrónico generado automáticamente. Por favor, no responda a este mensaje.</p>
|
||||
<p>© {DateTime.Now.Year} Diario El Día. Todos los derechos reservados.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>";
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
Task<(bool Exito, string? Mensaje, int FacturasGeneradas)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
||||
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
||||
Task<(bool Exito, string? Error)> EnviarAvisoCuentaPorEmail(int anio, int mes, int idSuscriptor);
|
||||
Task<(bool Exito, string? Error)> EnviarFacturaPdfPorEmail(int idFactura);
|
||||
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user