Sistema de Notificaciones y Baja One-Click
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user