From 5d70ba797afb5c7f8f9551ea598005a50d816bc8 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sun, 5 Oct 2025 21:57:50 -0300 Subject: [PATCH] feat: ajustes para downgrade --- BCards.sln | 5 + .../BCards.IntegrationTests.csproj | 1 + src/BCards.Web/BCards.Web.csproj | 33 +-- src/BCards.Web/Controllers/AdminController.cs | 89 +++++++ src/BCards.Web/Controllers/AuthController.cs | 7 +- .../Controllers/TestToolsController.cs | 170 +++++++++++++ src/BCards.Web/Program.cs | 10 + src/BCards.Web/Services/DowngradeService.cs | 225 ++++++++++++++++++ src/BCards.Web/Services/IDowngradeService.cs | 32 +++ src/BCards.Web/TestSupport/TestAuthHandler.cs | 2 + .../ViewModels/ManagePageViewModel.cs | 42 +++- src/BCards.Web/Views/Admin/ManagePage.cshtml | 1 + 12 files changed, 600 insertions(+), 17 deletions(-) create mode 100644 src/BCards.Web/Controllers/TestToolsController.cs create mode 100644 src/BCards.Web/Services/DowngradeService.cs create mode 100644 src/BCards.Web/Services/IDowngradeService.cs diff --git a/BCards.sln b/BCards.sln index bec54d6..af5c4e7 100644 --- a/BCards.sln +++ b/BCards.sln @@ -28,16 +28,21 @@ Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + Testing|Any CPU = Testing|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2E8F4B5C-9B3A-4F8E-8C7D-1A2B3C4D5E6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E8F4B5C-9B3A-4F8E-8C7D-1A2B3C4D5E6F}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E8F4B5C-9B3A-4F8E-8C7D-1A2B3C4D5E6F}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E8F4B5C-9B3A-4F8E-8C7D-1A2B3C4D5E6F}.Release|Any CPU.Build.0 = Release|Any CPU + {2E8F4B5C-9B3A-4F8E-8C7D-1A2B3C4D5E6F}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {2E8F4B5C-9B3A-4F8E-8C7D-1A2B3C4D5E6F}.Testing|Any CPU.Build.0 = Testing|Any CPU {8F9E4C7D-2A3B-4E5F-9C8D-1B2A3E4F5C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F9E4C7D-2A3B-4E5F-9C8D-1B2A3E4F5C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F9E4C7D-2A3B-4E5F-9C8D-1B2A3E4F5C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F9E4C7D-2A3B-4E5F-9C8D-1B2A3E4F5C6D}.Release|Any CPU.Build.0 = Release|Any CPU + {8F9E4C7D-2A3B-4E5F-9C8D-1B2A3E4F5C6D}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {8F9E4C7D-2A3B-4E5F-9C8D-1B2A3E4F5C6D}.Testing|Any CPU.Build.0 = Testing|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/BCards.IntegrationTests/BCards.IntegrationTests.csproj b/src/BCards.IntegrationTests/BCards.IntegrationTests.csproj index 52be975..706e425 100644 --- a/src/BCards.IntegrationTests/BCards.IntegrationTests.csproj +++ b/src/BCards.IntegrationTests/BCards.IntegrationTests.csproj @@ -6,6 +6,7 @@ enable false true + Debug;Release;Testing diff --git a/src/BCards.Web/BCards.Web.csproj b/src/BCards.Web/BCards.Web.csproj index 802d00d..67d0eeb 100644 --- a/src/BCards.Web/BCards.Web.csproj +++ b/src/BCards.Web/BCards.Web.csproj @@ -5,7 +5,8 @@ enable enable false - linux-x64;linux-arm64 + linux-x64;linux-arm64 + Debug;Release;Testing @@ -19,21 +20,25 @@ - - - - - - - - - - - - + + + + + + + + + + + + - + + $(DefineConstants);TESTING + + + diff --git a/src/BCards.Web/Controllers/AdminController.cs b/src/BCards.Web/Controllers/AdminController.cs index c03bd18..1889d66 100644 --- a/src/BCards.Web/Controllers/AdminController.cs +++ b/src/BCards.Web/Controllers/AdminController.cs @@ -24,6 +24,7 @@ public class AdminController : Controller private readonly ILivePageService _livePageService; private readonly IImageStorageService _imageStorage; private readonly IPaymentService _paymentService; + private readonly IDowngradeService _downgradeService; private readonly ILogger _logger; public AdminController( @@ -36,6 +37,7 @@ public class AdminController : Controller ILivePageService livePageService, IImageStorageService imageStorage, IPaymentService paymentService, + IDowngradeService downgradeService, ILogger logger) { _authService = authService; @@ -47,6 +49,7 @@ public class AdminController : Controller _livePageService = livePageService; _imageStorage = imageStorage; _paymentService = paymentService; + _downgradeService = downgradeService; _logger = logger; } @@ -299,6 +302,19 @@ public class AdminController : Controller if (model.IsNewPage) { + // CRITICAL: Check if user can create new page (validate MaxPages limit) + var existingPages = await _userPageService.GetUserPagesAsync(user.Id); + var userPlanType = Enum.TryParse(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial; + var maxPages = userPlanType.GetMaxPages(); + + if (existingPages.Count >= maxPages) + { + TempData["Error"] = $"Você já atingiu o limite de {maxPages} página(s) do seu plano atual. Faça upgrade para criar mais páginas."; + model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); + model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); + return View(model); + } + // Generate slug if not provided if (string.IsNullOrEmpty(model.Slug)) { @@ -1126,6 +1142,18 @@ public class AdminController : Controller } } + // Método auxiliar para critérios de downgrade (usado na API) + private DowngradeCriteria GetDowngradeCriteria(PlanType plan) + { + return new DowngradeCriteria + { + MaxPages = plan.GetMaxPages(), + MaxLinksPerPage = plan.GetMaxLinksPerPage(), + SelectionCriteria = "Páginas mais antigas têm prioridade", + LinksCriteria = "Páginas com muitos links são automaticamente suspensas" + }; + } + private void CleanSocialMediaFields(ManagePageViewModel model) { // Tratar espaço em branco como campo vazio para redes sociais @@ -1142,6 +1170,67 @@ public class AdminController : Controller model.TwitterUrl = string.Empty; } + // Endpoint para validar impacto de downgrade + [HttpPost] + [Route("ValidateDowngrade")] + public async Task ValidateDowngrade(string targetPlan) + { + try + { + var user = await _authService.GetCurrentUserAsync(User); + if (user == null) + return Json(new { error = "Usuário não encontrado" }); + + if (!Enum.TryParse(targetPlan, true, out var newPlan)) + return Json(new { error = "Plano inválido" }); + + var analysis = await _downgradeService.AnalyzeDowngradeImpact(user.Id, newPlan); + + // CASO CRÍTICO: Nenhuma página atende os critérios + if (analysis.IsCritical) + { + return Json(new + { + canDowngrade = false, + critical = true, + title = "⚠️ Downgrade não recomendado", + message = "Nenhuma de suas páginas atende aos limites do novo plano. Todas seriam suspensas.", + details = analysis.Issues, + suggestion = "Considere editar suas páginas para reduzir o número de links antes do downgrade.", + criteria = GetDowngradeCriteria(newPlan) + }); + } + + // CASO NORMAL: Algumas páginas serão afetadas + return Json(new + { + canDowngrade = true, + critical = false, + title = "Confirmação de Downgrade", + summary = analysis.Summary, + eligiblePages = analysis.EligiblePages.Select(p => new + { + name = p.DisplayName, + linkCount = p.LinkCount, + reason = "✅ Dentro dos limites" + }), + suspendedPages = analysis.AffectedPages.Select(p => new + { + name = p.DisplayName, + linkCount = p.LinkCount, + reason = p.SuspensionReason + }), + criteria = GetDowngradeCriteria(newPlan) + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro ao validar downgrade para usuário {User}", User.Identity?.Name); + return Json(new { error = "Erro interno do servidor" }); + } + } + + // 🔥 OTIMIZAÇÃO: Endpoint para manter a sessão do usuário ativa [HttpPost] [Route("KeepAlive")] diff --git a/src/BCards.Web/Controllers/AuthController.cs b/src/BCards.Web/Controllers/AuthController.cs index 1b1307b..d1f57f7 100644 --- a/src/BCards.Web/Controllers/AuthController.cs +++ b/src/BCards.Web/Controllers/AuthController.cs @@ -6,7 +6,9 @@ using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; +#if TESTING using BCards.Web.TestSupport; +#endif namespace BCards.Web.Controllers; @@ -148,8 +150,10 @@ public class AuthController : Controller return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme); } +#if TESTING [HttpPost] [Route("LoginWithTest")] + public IActionResult LoginWithTest(string? returnUrl = null) { if (!_env.IsEnvironment("Testing")) @@ -170,6 +174,7 @@ public class AuthController : Controller var properties = new AuthenticationProperties { RedirectUri = redirectUrlString }; return Challenge(properties, TestAuthConstants.AuthenticationScheme); } +#endif [HttpGet] [Route("GoogleCallback")] @@ -251,4 +256,4 @@ public class AuthController : Controller return RedirectToAction("Dashboard", "Admin"); } -} \ No newline at end of file +} diff --git a/src/BCards.Web/Controllers/TestToolsController.cs b/src/BCards.Web/Controllers/TestToolsController.cs new file mode 100644 index 0000000..6c67a97 --- /dev/null +++ b/src/BCards.Web/Controllers/TestToolsController.cs @@ -0,0 +1,170 @@ +#if TESTING +using BCards.Web.Models; +using BCards.Web.Repositories; +using BCards.Web.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BCards.Web.Controllers; + +[Authorize] +[ApiController] +[Route("testing/tools")] +public class TestToolsController : ControllerBase +{ + private readonly IUserRepository _userRepository; + private readonly ISubscriptionRepository _subscriptionRepository; + private readonly IUserPageService _userPageService; + private readonly ILivePageService _livePageService; + private readonly IPlanConfigurationService _planConfigurationService; + private readonly IWebHostEnvironment _environment; + + public TestToolsController( + IUserRepository userRepository, + ISubscriptionRepository subscriptionRepository, + IUserPageService userPageService, + ILivePageService livePageService, + IPlanConfigurationService planConfigurationService, + IWebHostEnvironment environment) + { + _userRepository = userRepository; + _subscriptionRepository = subscriptionRepository; + _userPageService = userPageService; + _livePageService = livePageService; + _planConfigurationService = planConfigurationService; + _environment = environment; + } + + [HttpPost("plan")] + public async Task SetPlan([FromBody] SetPlanRequest request) + { + if (!_environment.IsEnvironment("Testing")) + { + return NotFound(); + } + + if (!Enum.TryParse(request.Plan, true, out var planType)) + { + return BadRequest(new { error = $"Plano desconhecido: {request.Plan}" }); + } + + var email = string.IsNullOrWhiteSpace(request.Email) + ? TestUserDefaults.Email + : request.Email!.Trim(); + + var user = await _userRepository.GetByEmailAsync(email); + if (user == null) + { + return NotFound(new { error = $"Usuário de teste não encontrado para o e-mail {email}" }); + } + + var planLimits = _planConfigurationService.GetPlanLimitations(planType); + var normalizedPlan = planLimits.PlanType ?? planType.ToString().ToLowerInvariant(); + + user.CurrentPlan = normalizedPlan; + user.SubscriptionStatus = "active"; + user.UpdatedAt = DateTime.UtcNow; + await _userRepository.UpdateAsync(user); + + var subscription = await _subscriptionRepository.GetByUserIdAsync(user.Id); + if (subscription == null) + { + subscription = new Subscription + { + UserId = user.Id, + StripeSubscriptionId = $"test-{Guid.NewGuid():N}", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + } + + subscription.PlanType = normalizedPlan; + subscription.Status = "active"; + subscription.MaxLinks = planLimits.MaxLinks; + subscription.AllowAnalytics = planLimits.AllowAnalytics; + subscription.AllowCustomThemes = planLimits.AllowCustomThemes; + subscription.AllowCustomDomain = planLimits.AllowCustomDomain; + subscription.AllowMultipleDomains = planLimits.AllowMultipleDomains; + subscription.PrioritySupport = planLimits.PrioritySupport; + subscription.CurrentPeriodStart = DateTime.UtcNow.Date; + subscription.CurrentPeriodEnd = DateTime.UtcNow.Date.AddMonths(1); + subscription.CancelAtPeriodEnd = false; + subscription.UpdatedAt = DateTime.UtcNow; + + if (string.IsNullOrEmpty(subscription.Id)) + { + await _subscriptionRepository.CreateAsync(subscription); + } + else + { + await _subscriptionRepository.UpdateAsync(subscription); + } + + var pages = await _userPageService.GetUserPagesAsync(user.Id); + var originalPagesCount = pages.Count; + var removed = 0; + if (request.ResetPages) + { + foreach (var page in pages) + { + await _livePageService.DeleteByOriginalPageIdAsync(page.Id); + await _userPageService.DeletePageAsync(page.Id); + removed++; + } + + pages = new List(); + } + else + { + foreach (var page in pages) + { + page.PlanLimitations = ClonePlanLimitations(planLimits); + page.UpdatedAt = DateTime.UtcNow; + await _userPageService.UpdatePageAsync(page); + } + } + + return Ok(new + { + email, + plan = normalizedPlan, + pagesAffected = request.ResetPages ? originalPagesCount : pages.Count, + pagesRemoved = removed + }); + } + + private static PlanLimitations ClonePlanLimitations(PlanLimitations source) + { + return new PlanLimitations + { + MaxLinks = source.MaxLinks, + AllowCustomThemes = source.AllowCustomThemes, + AllowAnalytics = source.AllowAnalytics, + AllowCustomDomain = source.AllowCustomDomain, + AllowMultipleDomains = source.AllowMultipleDomains, + PrioritySupport = source.PrioritySupport, + PlanType = source.PlanType, + MaxProductLinks = source.MaxProductLinks, + MaxOGExtractionsPerDay = source.MaxOGExtractionsPerDay, + AllowProductLinks = source.AllowProductLinks, + SpecialModeration = source.SpecialModeration, + OGExtractionsUsedToday = 0, + LastExtractionDate = null + }; + } + + private static class TestUserDefaults + { + public const string Email = "test.user@example.com"; + } + + public class SetPlanRequest + { + public string? Email { get; set; } + + public string Plan { get; set; } = "trial"; + + public bool ResetPages { get; set; } + } +} +#endif diff --git a/src/BCards.Web/Program.cs b/src/BCards.Web/Program.cs index 6c06e93..9ea0f6c 100644 --- a/src/BCards.Web/Program.cs +++ b/src/BCards.Web/Program.cs @@ -18,7 +18,9 @@ using Serilog; using Serilog.Events; using Microsoft.Extensions.Diagnostics.HealthChecks; using Serilog.Sinks.OpenSearch; +#if TESTING using BCards.Web.TestSupport; +#endif using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; @@ -421,6 +423,7 @@ authBuilder.AddGoogle(options => }; }); +#if TESTING // Conditionally set the DefaultChallengeScheme and register the Test scheme if (builder.Environment.IsEnvironment("Testing")) { @@ -437,6 +440,12 @@ else options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; }); } +#else +authBuilder.Services.Configure(options => +{ + options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; +}); +#endif // Localization builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); @@ -471,6 +480,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/BCards.Web/Services/DowngradeService.cs b/src/BCards.Web/Services/DowngradeService.cs new file mode 100644 index 0000000..8e8385f --- /dev/null +++ b/src/BCards.Web/Services/DowngradeService.cs @@ -0,0 +1,225 @@ +using BCards.Web.Models; +using BCards.Web.ViewModels; + +namespace BCards.Web.Services; + +public class DowngradeService : IDowngradeService +{ + private readonly IUserPageService _userPageService; + private readonly ILogger _logger; + + // Ordem de prioridade dos planos (menor = melhor) + private static readonly Dictionary PlanPriority = new() + { + { PlanType.Trial, 0 }, + { PlanType.Basic, 1 }, + { PlanType.Professional, 2 }, + { PlanType.Premium, 3 }, + { PlanType.PremiumAffiliate, 4 } + }; + + public DowngradeService(IUserPageService userPageService, ILogger logger) + { + _userPageService = userPageService; + _logger = logger; + } + + public async Task AnalyzeDowngradeImpact(string userId, PlanType newPlan) + { + var pages = await _userPageService.GetUserPagesAsync(userId); + var activePagesOnly = pages.Where(p => p.Status == PageStatus.Active).ToList(); + + var newMaxPages = newPlan.GetMaxPages(); + var newMaxLinks = newPlan.GetMaxLinksPerPage(); + + var eligiblePages = activePagesOnly + .Where(p => newMaxLinks == -1 || (p.Links?.Count ?? 0) <= newMaxLinks) + .OrderBy(p => p.CreatedAt) // Mais antigas primeiro + .Take(newMaxPages) + .Select(p => new PageInfo + { + Id = p.Id, + DisplayName = p.DisplayName, + LinkCount = p.Links?.Count ?? 0, + CreatedAt = p.CreatedAt + }) + .ToList(); + + var affectedPages = activePagesOnly + .Where(p => !eligiblePages.Any(ep => ep.Id == p.Id)) + .Select(p => new PageInfo + { + Id = p.Id, + DisplayName = p.DisplayName, + LinkCount = p.Links?.Count ?? 0, + CreatedAt = p.CreatedAt, + SuspensionReason = GetSuspensionReason(p, newPlan) + }) + .ToList(); + + var issues = new List(); + + var pagesWithTooManyLinks = activePagesOnly + .Where(p => newMaxLinks != -1 && (p.Links?.Count ?? 0) > newMaxLinks) + .Count(); + + if (pagesWithTooManyLinks > 0) + issues.Add($"{pagesWithTooManyLinks} páginas têm muitos links"); + + if (activePagesOnly.Count > newMaxPages) + issues.Add($"Total de páginas ({activePagesOnly.Count}) excede limite ({newMaxPages})"); + + return new DowngradeAnalysis + { + EligiblePages = eligiblePages, + AffectedPages = affectedPages, + Issues = issues + }; + } + + public async Task ProcessAutomaticDowngrade(string userId, PlanType newPlan) + { + try + { + var analysis = await AnalyzeDowngradeImpact(userId, newPlan); + + // PROTEÇÃO: Se nenhuma página for elegível, cancelar downgrade + if (analysis.IsCritical) + { + throw new InvalidOperationException( + "Downgrade cancelado: nenhuma página atende aos critérios do novo plano. " + + "Edite suas páginas para reduzir links ou escolha um plano superior." + ); + } + + // Processar normalmente - suspender páginas afetadas + foreach (var pageInfo in analysis.AffectedPages) + { + var page = await _userPageService.GetPageByIdAsync(pageInfo.Id); + if (page != null) + { + page.Status = PageStatus.SuspendedByPlanLimit; + var detailedReason = GetDetailedSuspensionReason(page, newPlan); + await _userPageService.UpdatePageAsync(page); + + _logger.LogInformation( + "Página {PageName} (ID: {PageId}) suspensa por downgrade para {PlanType}. Motivo: {Reason}", + page.DisplayName, page.Id, newPlan, detailedReason + ); + } + } + + var result = new DowngradeResult + { + Success = true, + KeptActive = analysis.EligiblePages.Count, + Suspended = analysis.AffectedPages.Count, + Message = $"Downgrade concluído. {analysis.EligiblePages.Count} páginas ativas, {analysis.AffectedPages.Count} suspensas.", + Details = analysis.AffectedPages.Select(p => $"{p.DisplayName}: {p.SuspensionReason}").ToList() + }; + + _logger.LogInformation( + "Downgrade automático concluído para usuário {UserId}: {KeptActive} mantidas, {Suspended} suspensas", + userId, result.KeptActive, result.Suspended + ); + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro no processamento automático de downgrade para usuário {UserId}", userId); + return new DowngradeResult + { + Success = false, + Message = $"Erro no processamento: {ex.Message}", + Details = new List { ex.Message } + }; + } + } + + public async Task ProcessAutomaticUpgrade(string userId, PlanType newPlan) + { + try + { + var pages = await _userPageService.GetUserPagesAsync(userId); + var suspendedPages = pages.Where(p => p.Status == PageStatus.SuspendedByPlanLimit).ToList(); + + var newMaxPages = newPlan.GetMaxPages(); + var newMaxLinks = newPlan.GetMaxLinksPerPage(); + + var reactivated = 0; + + // Reativar páginas que agora cabem no novo plano + foreach (var page in suspendedPages.OrderBy(p => p.CreatedAt).Take(newMaxPages)) + { + // Verificar se a página atende aos critérios de links + if (newMaxLinks == -1 || (page.Links?.Count ?? 0) <= newMaxLinks) + { + page.Status = PageStatus.Active; + await _userPageService.UpdatePageAsync(page); + reactivated++; + + _logger.LogInformation( + "Página {PageName} (ID: {PageId}) reativada por upgrade para {PlanType}", + page.DisplayName, page.Id, newPlan + ); + } + } + + return new DowngradeResult + { + Success = true, + KeptActive = reactivated, + Suspended = 0, + Message = $"Upgrade concluído. {reactivated} páginas reativadas.", + Details = suspendedPages.Take(reactivated).Select(p => $"{p.DisplayName}: Reativada").ToList() + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro no processamento automático de upgrade para usuário {UserId}", userId); + return new DowngradeResult + { + Success = false, + Message = $"Erro no processamento: {ex.Message}", + Details = new List { ex.Message } + }; + } + } + + public bool IsDowngrade(PlanType oldPlan, PlanType newPlan) + { + return PlanPriority.TryGetValue(oldPlan, out var oldPriority) && + PlanPriority.TryGetValue(newPlan, out var newPriority) && + newPriority < oldPriority; + } + + public bool IsUpgrade(PlanType oldPlan, PlanType newPlan) + { + return PlanPriority.TryGetValue(oldPlan, out var oldPriority) && + PlanPriority.TryGetValue(newPlan, out var newPriority) && + newPriority > oldPriority; + } + + private string GetSuspensionReason(UserPage page, PlanType newPlan) + { + var maxLinks = newPlan.GetMaxLinksPerPage(); + var currentLinks = page.Links?.Count ?? 0; + + if (maxLinks != -1 && currentLinks > maxLinks) + return $"❌ Tem {currentLinks} links (limite: {maxLinks})"; + + return "❌ Excesso de páginas (critério: mais recentes são suspensas)"; + } + + private string GetDetailedSuspensionReason(UserPage page, PlanType newPlan) + { + var maxLinks = newPlan.GetMaxLinksPerPage(); + var currentLinks = page.Links?.Count ?? 0; + + if (maxLinks != -1 && currentLinks > maxLinks) + return $"Página suspensa: possui {currentLinks} links, mas o plano {newPlan.GetDisplayName()} permite apenas {maxLinks} links por página."; + + return $"Página suspensa: plano {newPlan.GetDisplayName()} permite apenas {newPlan.GetMaxPages()} páginas ativas. Páginas mais recentes foram suspensas automaticamente."; + } +} \ No newline at end of file diff --git a/src/BCards.Web/Services/IDowngradeService.cs b/src/BCards.Web/Services/IDowngradeService.cs new file mode 100644 index 0000000..dc5ee35 --- /dev/null +++ b/src/BCards.Web/Services/IDowngradeService.cs @@ -0,0 +1,32 @@ +using BCards.Web.Models; +using BCards.Web.ViewModels; + +namespace BCards.Web.Services; + +public interface IDowngradeService +{ + /// + /// Analisa o impacto de um downgrade para um usuário + /// + Task AnalyzeDowngradeImpact(string userId, PlanType newPlan); + + /// + /// Processa automaticamente um downgrade, suspendendo páginas necessárias + /// + Task ProcessAutomaticDowngrade(string userId, PlanType newPlan); + + /// + /// Reativa páginas suspensas por limite quando há upgrade + /// + Task ProcessAutomaticUpgrade(string userId, PlanType newPlan); + + /// + /// Detecta se uma mudança de plano é um downgrade + /// + bool IsDowngrade(PlanType oldPlan, PlanType newPlan); + + /// + /// Detecta se uma mudança de plano é um upgrade + /// + bool IsUpgrade(PlanType oldPlan, PlanType newPlan); +} \ No newline at end of file diff --git a/src/BCards.Web/TestSupport/TestAuthHandler.cs b/src/BCards.Web/TestSupport/TestAuthHandler.cs index 18975f6..ee4adde 100644 --- a/src/BCards.Web/TestSupport/TestAuthHandler.cs +++ b/src/BCards.Web/TestSupport/TestAuthHandler.cs @@ -1,4 +1,5 @@ +#if TESTING using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using System.Security.Claims; @@ -82,3 +83,4 @@ namespace BCards.Web.TestSupport } } } +#endif diff --git a/src/BCards.Web/ViewModels/ManagePageViewModel.cs b/src/BCards.Web/ViewModels/ManagePageViewModel.cs index 25c00ac..1cb29c0 100644 --- a/src/BCards.Web/ViewModels/ManagePageViewModel.cs +++ b/src/BCards.Web/ViewModels/ManagePageViewModel.cs @@ -134,10 +134,48 @@ public enum PageStatus { Active, // Funcionando normalmente Expired, // Trial vencido -> 301 redirect - PendingPayment, // Pagamento atrasado -> aviso na página + PendingPayment, // Pagamento atrasado -> aviso na página Inactive, // Pausada pelo usuário PendingModeration = 4, // Aguardando moderação Rejected = 5, // Rejeitada na moderação Creating = 6, // Em desenvolvimento/criação - Approved = 7 // Aprovada + Approved = 7, // Aprovada + SuspendedByPlanLimit = 8 // Suspensa por limite de plano (downgrade) +} + +// Modelos para análise de downgrade +public class DowngradeAnalysis +{ + public List EligiblePages { get; set; } = new(); + public List AffectedPages { get; set; } = new(); + public List Issues { get; set; } = new(); + public bool IsCritical => EligiblePages.Count == 0; + public string Summary => $"{EligiblePages.Count} mantidas, {AffectedPages.Count} suspensas"; +} + +public class PageInfo +{ + public string Id { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public int LinkCount { get; set; } + public DateTime CreatedAt { get; set; } + public string SuspensionReason { get; set; } = string.Empty; +} + +public class DowngradeResult +{ + public bool Success { get; set; } + public int KeptActive { get; set; } + public int Suspended { get; set; } + public string Message { get; set; } = string.Empty; + public List Details { get; set; } = new(); +} + +public class DowngradeCriteria +{ + public int MaxPages { get; set; } + public int MaxLinksPerPage { get; set; } + public string MaxLinksDisplay => MaxLinksPerPage == -1 ? "Ilimitado" : MaxLinksPerPage.ToString(); + public string SelectionCriteria { get; set; } = "Páginas mais antigas têm prioridade"; + public string LinksCriteria { get; set; } = "Páginas com muitos links são automaticamente suspensas"; } \ No newline at end of file diff --git a/src/BCards.Web/Views/Admin/ManagePage.cshtml b/src/BCards.Web/Views/Admin/ManagePage.cshtml index 49af073..dade940 100644 --- a/src/BCards.Web/Views/Admin/ManagePage.cshtml +++ b/src/BCards.Web/Views/Admin/ManagePage.cshtml @@ -280,6 +280,7 @@ +