Sistema de Notificaciones y Baja One-Click

This commit is contained in:
2026-03-12 13:52:33 -03:00
parent f1a9bb9099
commit 96fca4d9c7
21 changed files with 1384 additions and 79 deletions

View File

@@ -38,6 +38,7 @@ public class AdExpirationService : BackgroundService
await PermanentDeleteOldDeletedAdsAsync();
await CleanupOldRefreshTokensAsync();
await CleanupAdViewLogsAsync();
await CleanupUnsubscribeTokensAsync();
// 3. Marketing y Retención
await ProcessWeeklyStatsAsync();
@@ -80,12 +81,36 @@ public class AdExpirationService : BackgroundService
}
}
private async Task CheckExpiredAdsAsync()
private async Task CleanupUnsubscribeTokensAsync()
{
using (var scope = _serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<AdExpirationService>>();
// Borramos tokens que ya expiraron o que ya fueron usados
var now = DateTime.UtcNow;
var deletedCount = await context.UnsubscribeTokens
.Where(t => t.ExpiresAt <= now || t.IsUsed)
.ExecuteDeleteAsync();
if (deletedCount > 0)
{
logger.LogInformation("Mantenimiento: Se eliminaron {Count} tokens de baja expirados o usados.", deletedCount);
}
}
}
private async Task CheckExpiredAdsAsync()
{
using (var scope = _serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var prefService = scope.ServiceProvider.GetRequiredService<INotificationPreferenceService>();
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var frontendUrl = config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
var cutoffDate = DateTime.UtcNow.AddDays(-30);
@@ -106,10 +131,17 @@ public class AdExpirationService : BackgroundService
{
ad.StatusID = (int)AdStatusEnum.Expired;
if (ad.User != null && !string.IsNullOrEmpty(ad.User.Email))
// Solo enviamos el correo si el usuario tiene habilitada la categoría "sistema"
if (ad.User != null && !string.IsNullOrEmpty(ad.User.Email)
&& await prefService.IsEnabledAsync(ad.User.UserID, NotificationCategory.Sistema))
{
var title = $"{ad.Brand?.Name} {ad.VersionName}";
await notifService.SendAdExpiredEmailAsync(ad.User.Email, ad.User.FirstName ?? "Usuario", title);
var title = $"{ad.Brand?.Name} {ad.VersionName}";
// Generamos el token de baja para la categoría "sistema"
var rawToken = await prefService.GetOrCreateUnsubscribeTokenAsync(ad.User.UserID, NotificationCategory.Sistema);
var unsubscribeUrl = $"{frontendUrl}/baja/procesar?token={Uri.EscapeDataString(rawToken)}";
await notifService.SendAdExpiredEmailAsync(
ad.User.Email, ad.User.FirstName ?? "Usuario", title, unsubscribeUrl);
}
context.AuditLogs.Add(new AuditLog
@@ -129,8 +161,11 @@ public class AdExpirationService : BackgroundService
{
using (var scope = _serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var prefService = scope.ServiceProvider.GetRequiredService<INotificationPreferenceService>();
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var frontendUrl = config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
var warningThreshold = DateTime.UtcNow.AddDays(-25);
@@ -150,12 +185,22 @@ public class AdExpirationService : BackgroundService
{
if (ad.User == null || string.IsNullOrEmpty(ad.User.Email)) continue;
var title = $"{ad.Brand?.Name} {ad.VersionName}";
var title = $"{ad.Brand?.Name} {ad.VersionName}";
var expDate = ad.PublishedAt!.Value.AddDays(30);
try
{
await notifService.SendExpirationWarningEmailAsync(ad.User.Email, ad.User.FirstName ?? "Usuario", title, expDate);
// Respetamos la preferencia de la categoría "sistema"
if (await prefService.IsEnabledAsync(ad.User.UserID, NotificationCategory.Sistema))
{
// Generamos el token de baja para la categoría "sistema"
var rawToken = await prefService.GetOrCreateUnsubscribeTokenAsync(ad.User.UserID, NotificationCategory.Sistema);
var unsubscribeUrl = $"{frontendUrl}/baja/procesar?token={Uri.EscapeDataString(rawToken)}";
await notifService.SendExpirationWarningEmailAsync(
ad.User.Email, ad.User.FirstName ?? "Usuario", title, expDate, unsubscribeUrl);
}
// La bandera se marca igual para no reintentar aunque el usuario no quiera el email
ad.ExpirationWarningSent = true;
}
catch { /* Log error pero continuar */ }
@@ -169,8 +214,11 @@ public class AdExpirationService : BackgroundService
{
using (var scope = _serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var prefService = scope.ServiceProvider.GetRequiredService<INotificationPreferenceService>();
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var frontendUrl = config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
var sevenDaysAgo = DateTime.UtcNow.AddDays(-7);
@@ -190,15 +238,28 @@ public class AdExpirationService : BackgroundService
{
if (ad.User == null || string.IsNullOrEmpty(ad.User.Email)) continue;
// Respetamos la preferencia de la categoría "rendimiento"
if (!await prefService.IsEnabledAsync(ad.User.UserID, NotificationCategory.Rendimiento))
{
// Actualizamos la fecha para no consultar este aviso en el próximo ciclo
ad.LastPerformanceEmailSentAt = DateTime.UtcNow;
continue;
}
var favCount = await context.Favorites.CountAsync(f => f.AdID == ad.AdID);
var title = $"{ad.Brand?.Name} {ad.VersionName}";
var title = $"{ad.Brand?.Name} {ad.VersionName}";
// Generamos el token de baja para la categoría "rendimiento"
var rawToken = await prefService.GetOrCreateUnsubscribeTokenAsync(ad.User.UserID, NotificationCategory.Rendimiento);
var unsubscribeUrl = $"{frontendUrl}/baja/procesar?token={Uri.EscapeDataString(rawToken)}";
await notifService.SendWeeklyPerformanceEmailAsync(
ad.User.Email,
ad.User.FirstName ?? "Usuario",
title,
ad.ViewsCounter,
favCount
favCount,
unsubscribeUrl
);
ad.LastPerformanceEmailSentAt = DateTime.UtcNow;
@@ -212,11 +273,11 @@ public class AdExpirationService : BackgroundService
{
using (var scope = _serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var frontendUrl = config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var prefService = scope.ServiceProvider.GetRequiredService<INotificationPreferenceService>();
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var frontendUrl = config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
var cutoff = DateTime.UtcNow.AddHours(-24);
@@ -236,10 +297,19 @@ public class AdExpirationService : BackgroundService
{
if (ad.User == null || string.IsNullOrEmpty(ad.User.Email)) continue;
var title = $"{ad.Brand?.Name} {ad.VersionName}";
var link = $"{frontendUrl}/publicar?edit={ad.AdID}";
// Respetamos la preferencia de la categoría "marketing" (carrito abandonado)
if (await prefService.IsEnabledAsync(ad.User.UserID, NotificationCategory.Marketing))
{
var title = $"{ad.Brand?.Name} {ad.VersionName}";
var link = $"{frontendUrl}/publicar?edit={ad.AdID}";
await notifService.SendPaymentReminderEmailAsync(ad.User.Email, ad.User.FirstName ?? "Usuario", title, link);
// Generamos el token de baja para la categoría "marketing"
var rawToken = await prefService.GetOrCreateUnsubscribeTokenAsync(ad.User.UserID, NotificationCategory.Marketing);
var unsubscribeUrl = $"{frontendUrl}/baja/procesar?token={Uri.EscapeDataString(rawToken)}";
await notifService.SendPaymentReminderEmailAsync(
ad.User.Email, ad.User.FirstName ?? "Usuario", title, link, unsubscribeUrl);
}
ad.PaymentReminderSentAt = DateTime.UtcNow;
}
@@ -252,10 +322,13 @@ public class AdExpirationService : BackgroundService
{
using (var scope = _serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
var notifService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var prefService = scope.ServiceProvider.GetRequiredService<INotificationPreferenceService>();
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var frontendUrl = config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
// Buscar usuarios que tengan mensajes no leídos viejos (> 4 horas)
// Buscar usuarios que tengan mensajes no leídos viejos (> 4 horas)
// y que no hayan sido notificados en las últimas 24 horas.
var messageThreshold = DateTime.UtcNow.AddHours(-4);
@@ -282,7 +355,16 @@ public class AdExpirationService : BackgroundService
// Contar total no leídos
var totalUnread = await context.ChatMessages.CountAsync(m => m.ReceiverID == userId && !m.IsRead);
await notifService.SendUnreadMessagesReminderEmailAsync(user.Email, user.FirstName ?? "Usuario", totalUnread);
// Respetamos la preferencia de la categoría "mensajes"
if (await prefService.IsEnabledAsync(userId, NotificationCategory.Mensajes))
{
// Generamos el token de baja para la categoría "mensajes"
var rawToken = await prefService.GetOrCreateUnsubscribeTokenAsync(userId, NotificationCategory.Mensajes);
var unsubscribeUrl = $"{frontendUrl}/baja/procesar?token={Uri.EscapeDataString(rawToken)}";
await notifService.SendUnreadMessagesReminderEmailAsync(
user.Email, user.FirstName ?? "Usuario", totalUnread, unsubscribeUrl);
}
user.LastUnreadMessageReminderSentAt = DateTime.UtcNow;
}