272 lines
10 KiB
C#
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");
|
|
}
|
|
}
|
|
}
|