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 _logger; private readonly TimeSpan _checkInterval = TimeSpan.FromHours(1); // Check every hour public TrialExpirationService( IServiceProvider serviceProvider, ILogger 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(); var userPageRepository = scope.ServiceProvider.GetRequiredService(); var userRepository = scope.ServiceProvider.GetRequiredService(); _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(); 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.Filter.And( MongoDB.Driver.Builders.Filter.Eq(p => p.Status, PageStatus.Expired), MongoDB.Driver.Builders.Filter.Ne(p => p.DeletedAt, null), MongoDB.Driver.Builders.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"); } } }