BCards/src/BCards.Web/Services/TrialExpirationService.cs
2025-11-03 00:04:41 -03:00

272 lines
10 KiB
C#

using BCards.Web.Models;
using BCards.Web.Repositories;
using BCards.Web.ViewModels;
using MongoDB.Driver;
namespace BCards.Web.Services;
public class TrialExpirationService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<TrialExpirationService> _logger;
private readonly TimeSpan _checkInterval = TimeSpan.FromHours(1); // Check every hour
public TrialExpirationService(
IServiceProvider serviceProvider,
ILogger<TrialExpirationService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("TrialExpirationService started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessTrialExpirationsAsync();
// Verificar cancelamento antes de fazer delay
if (stoppingToken.IsCancellationRequested)
break;
await Task.Delay(_checkInterval, stoppingToken);
}
catch (OperationCanceledException)
{
// Cancelamento normal - não é erro
_logger.LogInformation("TrialExpirationService is being cancelled");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing trial expirations");
// Verificar cancelamento antes de fazer delay de erro
if (stoppingToken.IsCancellationRequested)
break;
try
{
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); // Wait 5 minutes on error
}
catch (OperationCanceledException)
{
// Cancelamento durante delay de erro - também é normal
_logger.LogInformation("TrialExpirationService cancelled during error delay");
break;
}
}
}
_logger.LogInformation("TrialExpirationService stopped");
}
private async Task ProcessTrialExpirationsAsync()
{
try
{
using var scope = _serviceProvider.CreateScope();
var subscriptionRepository = scope.ServiceProvider.GetRequiredService<ISubscriptionRepository>();
var userPageRepository = scope.ServiceProvider.GetRequiredService<IUserPageRepository>();
var userRepository = scope.ServiceProvider.GetRequiredService<IUserRepository>();
_logger.LogInformation("Checking for expired trials...");
// Process trial expirations
var trialSubscriptions = await subscriptionRepository.GetTrialSubscriptionsAsync();
var now = DateTime.UtcNow;
_logger.LogInformation($"Found {trialSubscriptions.Count} trial subscriptions to process");
foreach (var subscription in trialSubscriptions)
{
try
{
var user = await userRepository.GetByIdAsync(subscription.UserId);
if (user == null)
{
_logger.LogWarning($"User not found for subscription {subscription.Id}");
continue;
}
var daysUntilExpiration = (subscription.CurrentPeriodEnd - now).TotalDays;
if (daysUntilExpiration <= 0)
{
// Trial expired - deactivate page
_logger.LogInformation($"Trial expired for user {user.Email}");
await HandleTrialExpiredAsync(user, subscription, userPageRepository);
}
else if (daysUntilExpiration <= 2 && !user.NotifiedOfExpiration)
{
// Trial expiring soon - send notification
_logger.LogInformation($"Trial expiring in {daysUntilExpiration:F1} days for user {user.Email}");
await SendExpirationWarningAsync(user, subscription, daysUntilExpiration);
// Mark as notified
user.NotifiedOfExpiration = true;
await userRepository.UpdateAsync(user);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing trial for subscription {subscription.Id}");
}
}
_logger.LogInformation("Finished checking trial expirations");
// Process permanent deletions (pages deleted for more than 30 days)
await ProcessPermanentDeletionsAsync(userPageRepository);
}
catch (Exception ex)
{
_logger.LogError(ex, "Critical error in ProcessTrialExpirationsAsync");
throw; // Re-throw para ser tratado pelo ExecuteAsync
}
}
private async Task HandleTrialExpiredAsync(
User user,
Subscription subscription,
IUserPageRepository userPageRepository)
{
// Mark user page as expired (logical deletion)
var userPage = await userPageRepository.GetByUserIdAsync(user.Id);
if (userPage != null)
{
userPage.Status = PageStatus.Expired;
userPage.DeletedAt = DateTime.UtcNow;
userPage.DeletionReason = "trial_expired";
userPage.UpdatedAt = DateTime.UtcNow;
await userPageRepository.UpdateAsync(userPage);
}
// Update subscription status
subscription.Status = "expired";
subscription.UpdatedAt = DateTime.UtcNow;
using var scope = _serviceProvider.CreateScope();
var subscriptionRepository = scope.ServiceProvider.GetRequiredService<ISubscriptionRepository>();
await subscriptionRepository.UpdateAsync(subscription);
// Send expiration email
await SendTrialExpiredEmailAsync(user);
_logger.LogInformation($"Deactivated trial page for user {user.Email}");
}
private async Task SendExpirationWarningAsync(
User user,
Subscription subscription,
double daysRemaining)
{
// TODO: Implement email service
// For now, just log
_logger.LogInformation($"Should send expiration warning to {user.Email} - {daysRemaining:F1} days remaining");
// Example email content:
var subject = "Seu trial do BCards expira em breve!";
var message = $@"
Olá {user.Name},
Seu trial gratuito do BCards expira em {Math.Ceiling(daysRemaining)} dia(s).
Para continuar usando sua página de links, escolha um de nossos planos:
• Básico - R$ 12,90/mês
• Profissional - R$ 25,90/mês
• Premium - R$ 29,90/mês
Acesse: {GetUpgradeUrl()}
Equipe BCards
";
// TODO: Send actual email when email service is implemented
await Task.CompletedTask;
}
private async Task SendTrialExpiredEmailAsync(User user)
{
// TODO: Implement email service
_logger.LogInformation($"Should send trial expired email to {user.Email}");
var subject = "Seu trial do BCards expirou";
var message = $@"
Olá {user.Name},
Seu trial gratuito do BCards expirou e sua página foi temporariamente desativada.
Para reativar sua página, escolha um de nossos planos:
• Básico - R$ 12,90/mês - 5 links, analytics básicos
• Profissional - R$ 25,90/mês - 15 links, todos os temas, analytics avançados
• Premium - R$ 29,90/mês - Links ilimitados, temas premium, analytics completos, upload de PDFs
Seus dados estão seguros e serão restaurados assim que você escolher um plano.
Acesse: {GetUpgradeUrl()}
Equipe BCards
";
// TODO: Send actual email when email service is implemented
await Task.CompletedTask;
}
private string GetUpgradeUrl()
{
// TODO: Get from configuration
return "https://bcards.com.br/pricing";
}
private async Task ProcessPermanentDeletionsAsync(IUserPageRepository userPageRepository)
{
try
{
_logger.LogInformation("Checking for pages to permanently delete...");
// Find pages that have been logically deleted for more than 30 days
var cutoffDate = DateTime.UtcNow.AddDays(-30);
// Get all expired pages older than 30 days
var filter = MongoDB.Driver.Builders<UserPage>.Filter.And(
MongoDB.Driver.Builders<UserPage>.Filter.Eq(p => p.Status, PageStatus.Expired),
MongoDB.Driver.Builders<UserPage>.Filter.Ne(p => p.DeletedAt, null),
MongoDB.Driver.Builders<UserPage>.Filter.Lt(p => p.DeletedAt, cutoffDate)
);
var pagesToDelete = await userPageRepository.GetManyAsync(filter);
_logger.LogInformation($"Found {pagesToDelete.Count} pages to permanently delete");
foreach (var page in pagesToDelete)
{
try
{
_logger.LogInformation($"Permanently deleting page {page.Id} ({page.DisplayName}) - deleted at {page.DeletedAt}");
await userPageRepository.DeleteAsync(page.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error permanently deleting page {page.Id}");
}
}
if (pagesToDelete.Count > 0)
{
_logger.LogInformation($"Permanently deleted {pagesToDelete.Count} pages");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing permanent deletions");
}
}
}