From 61453b65c9884d5fe791b12268949c55e6786b30 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Tue, 9 Sep 2025 18:51:48 -0300 Subject: [PATCH] fix: planos de pagamentos associados ao appSettings --- .../Controllers/StripeWebhookController.cs | 37 +-- src/BCards.Web/Models/PlanType.cs | 43 ++-- src/BCards.Web/Program.cs | 1 + .../Services/IPlanConfigurationService.cs | 57 +++++ src/BCards.Web/Services/PaymentService.cs | 74 +----- .../Services/PlanConfigurationService.cs | 228 ++++++++++++++++++ src/BCards.Web/appsettings.json | 4 +- 7 files changed, 336 insertions(+), 108 deletions(-) create mode 100644 src/BCards.Web/Services/IPlanConfigurationService.cs create mode 100644 src/BCards.Web/Services/PlanConfigurationService.cs diff --git a/src/BCards.Web/Controllers/StripeWebhookController.cs b/src/BCards.Web/Controllers/StripeWebhookController.cs index 5d0253a..0eba023 100644 --- a/src/BCards.Web/Controllers/StripeWebhookController.cs +++ b/src/BCards.Web/Controllers/StripeWebhookController.cs @@ -17,6 +17,7 @@ public class StripeWebhookController : ControllerBase private readonly ISubscriptionRepository _subscriptionRepository; private readonly IUserPageService _userPageService; private readonly IUserRepository _userRepository; + private readonly IPlanConfigurationService _planConfigurationService; private readonly string _webhookSecret; public StripeWebhookController( @@ -24,12 +25,14 @@ public class StripeWebhookController : ControllerBase ISubscriptionRepository subscriptionRepository, IUserPageService userPageService, IUserRepository userRepository, + IPlanConfigurationService planConfigurationService, IOptions stripeSettings) { _logger = logger; _subscriptionRepository = subscriptionRepository; _userPageService = userPageService; _userRepository = userRepository; + _planConfigurationService = planConfigurationService; _webhookSecret = stripeSettings.Value.WebhookSecret ?? ""; } @@ -253,7 +256,7 @@ public class StripeWebhookController : ControllerBase var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id; if (!string.IsNullOrEmpty(priceId)) { - subscription.PlanType = MapPriceIdToPlanType(priceId); + subscription.PlanType = _planConfigurationService.GetPlanNameFromPriceId(priceId); } await _subscriptionRepository.UpdateAsync(subscription); @@ -304,7 +307,7 @@ public class StripeWebhookController : ControllerBase var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id; if (!string.IsNullOrEmpty(priceId)) { - subscription.PlanType = MapPriceIdToPlanType(priceId); + subscription.PlanType = _planConfigurationService.GetPlanNameFromPriceId(priceId); } await _subscriptionRepository.UpdateAsync(subscription); @@ -346,27 +349,6 @@ public class StripeWebhookController : ControllerBase } } - private string MapPriceIdToPlanType(string priceId) - { - // Map Stripe price IDs to plan types - // This would be configured based on your actual Stripe price IDs - return priceId switch - { - "price_1RjUskBMIadsOxJVgLwlVo1y" => "Basic", - "price_1RjUv9BMIadsOxJVORqlM4E9" => "Professional", - "price_1RjUw0BMIadsOxJVmdouNV1g" => "Premium", - "price_basic_yearly_placeholder" => "BasicYearly", - "price_professional_yearly_placeholder" => "ProfessionalYearly", - "price_premium_yearly_placeholder" => "PremiumYearly", - var id when id.Contains("basic") && id.Contains("yearly") => "BasicYearly", - var id when id.Contains("professional") && id.Contains("yearly") => "ProfessionalYearly", - var id when id.Contains("premium") && id.Contains("yearly") => "PremiumYearly", - var id when id.Contains("basic") => "Basic", - var id when id.Contains("professional") => "Professional", - var id when id.Contains("premium") => "Premium", - _ => "Trial" - }; - } private string GetSubscriptionId(Event stripeEvent) { @@ -403,10 +385,8 @@ public class StripeWebhookController : ControllerBase try { _logger.LogInformation($"[DEBUG] [TID: {traceId}] - HandleSubscriptionCreatedForNewSubscription started for customer: {stripeSubscription.CustomerId}"); - - // Find user by Stripe Customer ID using a MongoDB query - // Since IUserRepository doesn't have GetByStripeCustomerIdAsync, we'll use MongoDB directly _logger.LogInformation($"[DEBUG] [TID: {traceId}] - Getting MongoDB database"); + var mongoDatabase = HttpContext.RequestServices.GetRequiredService(); var usersCollection = mongoDatabase.GetCollection("users"); @@ -423,7 +403,7 @@ public class StripeWebhookController : ControllerBase // Get plan type from price ID var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id; - var planType = !string.IsNullOrEmpty(priceId) ? MapPriceIdToPlanType(priceId) : "Trial"; + var planType = !string.IsNullOrEmpty(priceId) ? _planConfigurationService.GetPlanNameFromPriceId(priceId) : "Trial"; _logger.LogInformation($"[TID: {traceId}] - PriceId: {priceId}, PlanType: {planType}, User: {user.Id}"); @@ -446,7 +426,6 @@ public class StripeWebhookController : ControllerBase await _subscriptionRepository.CreateAsync(newSubscription); _logger.LogInformation($"[TID: {traceId}] - Created new subscription {newSubscription.Id} for user {user.Id}"); - // 🔥 CORREÇÃO: Update user's CurrentPlan _logger.LogInformation($"[TID: {traceId}] - Updating user {user.Id} CurrentPlan from '{user.CurrentPlan}' to '{planType}'"); user.CurrentPlan = planType; user.UpdatedAt = DateTime.UtcNow; @@ -455,7 +434,6 @@ public class StripeWebhookController : ControllerBase await usersCollection2.ReplaceOneAsync(u => u.Id == user.Id, user); _logger.LogInformation($"[TID: {traceId}] - User {user.Id} CurrentPlan updated to '{planType}'"); - // Activate user pages that were pending payment or expired var userPages = await _userPageService.GetUserPagesAsync(user.Id); foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.PendingPayment || @@ -473,7 +451,6 @@ public class StripeWebhookController : ControllerBase _logger.LogError($"[DEBUG] [TID: {traceId}] - User not found for Stripe customer ID: {stripeSubscription.CustomerId}"); _logger.LogInformation($"[DEBUG] [TID: {traceId}] - Will try to list some users to debug"); - // Debug: list some users to see what we have var allUsers = await usersCollection.Find(_ => true).Limit(5).ToListAsync(); foreach (var u in allUsers) { diff --git a/src/BCards.Web/Models/PlanType.cs b/src/BCards.Web/Models/PlanType.cs index 03b2438..99635f7 100644 --- a/src/BCards.Web/Models/PlanType.cs +++ b/src/BCards.Web/Models/PlanType.cs @@ -3,9 +3,10 @@ namespace BCards.Web.Models; public enum PlanType { Trial = 0, // Gratuito por 7 dias - Basic = 1, // R$ 9,90 - Professional = 2, // R$ 24,90 (Decoy) - Premium = 3 // R$ 29,90 + Basic = 1, + Professional = 2, + Premium = 4, + PremiumAffiliate = 5 } public static class PlanTypeExtensions @@ -18,22 +19,28 @@ public static class PlanTypeExtensions PlanType.Basic => "Básico", PlanType.Professional => "Profissional", PlanType.Premium => "Premium", + PlanType.PremiumAffiliate => "PremiumAffiliate", _ => "Desconhecido" }; } + // NOTA: Preços agora são configurados dinamicamente via IPlanConfigurationService + // Este método mantém valores fallback para compatibilidade public static decimal GetPrice(this PlanType planType) { return planType switch { PlanType.Trial => 0.00m, - PlanType.Basic => 9.90m, - PlanType.Professional => 24.90m, - PlanType.Premium => 29.90m, + PlanType.Basic => 5.90m, + PlanType.Professional => 12.90m, + PlanType.Premium => 19.90m, + PlanType.PremiumAffiliate => 29.90m, _ => 0.00m }; } + // NOTA: Limitações agora são configuradas dinamicamente via IPlanConfigurationService + // Este método mantém valores fallback para compatibilidade public static int GetMaxPages(this PlanType planType) { return planType switch @@ -42,10 +49,13 @@ public static class PlanTypeExtensions PlanType.Basic => 3, PlanType.Professional => 5, // DECOY - not attractive PlanType.Premium => 15, + PlanType.PremiumAffiliate => 15, _ => 1 }; } + // NOTA: Limitações agora são configuradas dinamicamente via IPlanConfigurationService + // Este método mantém valores fallback para compatibilidade public static int GetMaxLinksPerPage(this PlanType planType) { return planType switch @@ -54,6 +64,7 @@ public static class PlanTypeExtensions PlanType.Basic => 8, PlanType.Professional => 20, // DECOY - too expensive for the benefit PlanType.Premium => int.MaxValue, // Unlimited + PlanType.PremiumAffiliate => int.MaxValue, // Unlimited _ => 3 }; } @@ -71,6 +82,7 @@ public static class PlanTypeExtensions PlanType.Basic => true, PlanType.Professional => true, PlanType.Premium => true, + PlanType.PremiumAffiliate => true, _ => false }; } @@ -83,6 +95,7 @@ public static class PlanTypeExtensions PlanType.Basic => false, PlanType.Professional => true, PlanType.Premium => true, + PlanType.PremiumAffiliate => true, _ => false }; } @@ -96,10 +109,11 @@ public static class PlanTypeExtensions { return planType switch { - PlanType.Trial => 1, // 1 link de produto para trial - PlanType.Basic => 3, // 3 links de produto - PlanType.Professional => 8, // DECOY - mais caro para poucos benefícios - PlanType.Premium => int.MaxValue, // Ilimitado + PlanType.Trial => 0, // 1 link de produto para trial + PlanType.Basic => 0, // 3 links de produto + PlanType.Professional => 0, // DECOY - mais caro para poucos benefícios + PlanType.Premium => 0, // Ilimitado + PlanType.PremiumAffiliate => int.MaxValue, // Ilimitado _ => 0 }; } @@ -108,10 +122,11 @@ public static class PlanTypeExtensions { return planType switch { - PlanType.Trial => 2, // 2 extrações por dia no trial - PlanType.Basic => 5, // 5 extrações por dia - PlanType.Professional => 15, // 15 extrações por dia - PlanType.Premium => int.MaxValue, // Ilimitado + PlanType.Trial => 0, // 2 extrações por dia no trial + PlanType.Basic => 0, // 5 extrações por dia + PlanType.Professional => 0, // 15 extrações por dia + PlanType.Premium => 0, // Ilimitado + PlanType.PremiumAffiliate => int.MaxValue, // Ilimitado _ => 0 }; } diff --git a/src/BCards.Web/Program.cs b/src/BCards.Web/Program.cs index 6f8bf67..3f17430 100644 --- a/src/BCards.Web/Program.cs +++ b/src/BCards.Web/Program.cs @@ -401,6 +401,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/BCards.Web/Services/IPlanConfigurationService.cs b/src/BCards.Web/Services/IPlanConfigurationService.cs new file mode 100644 index 0000000..7632d52 --- /dev/null +++ b/src/BCards.Web/Services/IPlanConfigurationService.cs @@ -0,0 +1,57 @@ +using BCards.Web.Models; + +namespace BCards.Web.Services; + +public interface IPlanConfigurationService +{ + /// + /// Mapeia um PriceId do Stripe para o PlanType correspondente + /// + PlanType GetPlanTypeFromPriceId(string priceId); + + /// + /// Mapeia um PriceId do Stripe para o nome string do plano + /// + string GetPlanNameFromPriceId(string priceId); + + /// + /// Obtém as limitações de um plano baseado no PlanType + /// + PlanLimitations GetPlanLimitations(PlanType planType); + + /// + /// Obtém o PriceId de um plano (mensal por padrão) + /// + string GetPriceId(PlanType planType, bool yearly = false); + + /// + /// Obtém o preço de um plano + /// + decimal GetPlanPrice(PlanType planType, bool yearly = false); + + /// + /// Verifica se um plano é anual baseado no PriceId + /// + bool IsYearlyPlan(string priceId); + + /// + /// Obtém todas as configurações de um plano pelo nome da seção + /// + PlanConfiguration? GetPlanConfiguration(string planSectionName); +} + +public class PlanConfiguration +{ + public string Name { get; set; } = string.Empty; + public string PriceId { get; set; } = string.Empty; + public decimal Price { get; set; } + public int MaxPages { get; set; } + public int MaxLinks { get; set; } + public bool AllowPremiumThemes { get; set; } + public bool AllowProductLinks { get; set; } + public bool AllowAnalytics { get; set; } + public bool? SpecialModeration { get; set; } + public List Features { get; set; } = new(); + public string Interval { get; set; } = "month"; + public PlanType BasePlanType { get; set; } +} \ No newline at end of file diff --git a/src/BCards.Web/Services/PaymentService.cs b/src/BCards.Web/Services/PaymentService.cs index 79cc415..a9e8a18 100644 --- a/src/BCards.Web/Services/PaymentService.cs +++ b/src/BCards.Web/Services/PaymentService.cs @@ -15,17 +15,20 @@ public class PaymentService : IPaymentService private readonly IUserRepository _userRepository; private readonly ISubscriptionRepository _subscriptionRepository; private readonly IConfiguration _configuration; + private readonly IPlanConfigurationService _planConfigurationService; public PaymentService( IOptions stripeSettings, IUserRepository userRepository, ISubscriptionRepository subscriptionRepository, - IConfiguration configuration) + IConfiguration configuration, + IPlanConfigurationService planConfigurationService) { _stripeSettings = stripeSettings.Value; _userRepository = userRepository; _subscriptionRepository = subscriptionRepository; _configuration = configuration; + _planConfigurationService = planConfigurationService; StripeConfiguration.ApiKey = _stripeSettings.SecretKey; } @@ -201,70 +204,17 @@ public class PaymentService : IPaymentService public Task GetPlanLimitationsAsync(string planType) { - var limitations = planType.ToLower() switch + // Mapear string planType para PlanType enum + var planTypeEnum = planType.ToLower() switch { - "basic" => new PlanLimitations - { - MaxLinks = 8, - AllowCustomThemes = false, - AllowAnalytics = true, - AllowCustomDomain = true, // URL personalizada em todos os planos pagos - AllowMultipleDomains = false, - PrioritySupport = false, - AllowProductLinks = false, - MaxProductLinks = 0, - PlanType = "basic" - }, - "professional" => new PlanLimitations - { - MaxLinks = 20, - AllowCustomThemes = false, - AllowAnalytics = true, - AllowCustomDomain = true, - AllowMultipleDomains = false, - PrioritySupport = false, - AllowProductLinks = false, - MaxProductLinks = 0, - PlanType = "professional" - }, - "premium" => new PlanLimitations - { - MaxLinks = -1, // Unlimited - AllowCustomThemes = true, - AllowAnalytics = true, - AllowCustomDomain = true, - AllowMultipleDomains = true, - PrioritySupport = true, - AllowProductLinks = false, - MaxProductLinks = 0, - PlanType = "premium" - }, - "premiumaffiliate" => new PlanLimitations - { - MaxLinks = -1, // Unlimited - AllowCustomThemes = true, - AllowAnalytics = true, - AllowCustomDomain = true, - AllowMultipleDomains = true, - PrioritySupport = true, - AllowProductLinks = true, - MaxProductLinks = 10, - PlanType = "premiumaffiliate" - }, - _ => new PlanLimitations - { - MaxLinks = 3, - AllowCustomThemes = false, - AllowAnalytics = false, - AllowCustomDomain = false, - AllowMultipleDomains = false, - PrioritySupport = false, - AllowProductLinks = false, - MaxProductLinks = 0, - PlanType = "trial" - } + "basic" or "basicyearly" => PlanType.Basic, + "professional" or "professionalyearly" => PlanType.Professional, + "premium" or "premiumyearly" => PlanType.Premium, + "premiumaffiliate" or "premiumaffiliateyearly" => PlanType.PremiumAffiliate, + _ => PlanType.Trial }; + var limitations = _planConfigurationService.GetPlanLimitations(planTypeEnum); return Task.FromResult(limitations); } diff --git a/src/BCards.Web/Services/PlanConfigurationService.cs b/src/BCards.Web/Services/PlanConfigurationService.cs new file mode 100644 index 0000000..979e6fa --- /dev/null +++ b/src/BCards.Web/Services/PlanConfigurationService.cs @@ -0,0 +1,228 @@ +using BCards.Web.Models; + +namespace BCards.Web.Services; + +public class PlanConfigurationService : IPlanConfigurationService +{ + private readonly IConfiguration _configuration; + private readonly Dictionary _plans; + private readonly Dictionary _priceIdToPlanName; + private readonly Dictionary _priceIdToPlanType; + + public PlanConfigurationService(IConfiguration configuration) + { + _configuration = configuration; + _plans = LoadPlansFromConfiguration(); + _priceIdToPlanName = BuildPriceIdToPlanNameMap(); + _priceIdToPlanType = BuildPriceIdToPlanTypeMap(); + } + + public PlanType GetPlanTypeFromPriceId(string priceId) + { + if (string.IsNullOrEmpty(priceId)) + return PlanType.Trial; + + return _priceIdToPlanType.TryGetValue(priceId, out var planType) ? planType : PlanType.Trial; + } + + public string GetPlanNameFromPriceId(string priceId) + { + if (string.IsNullOrEmpty(priceId)) + return "Trial"; + + return _priceIdToPlanName.TryGetValue(priceId, out var planName) ? planName : "Trial"; + } + + public PlanLimitations GetPlanLimitations(PlanType planType) + { + return planType switch + { + PlanType.Trial => new PlanLimitations + { + MaxLinks = 3, + AllowCustomThemes = false, + AllowAnalytics = false, + AllowCustomDomain = false, + AllowMultipleDomains = false, + PrioritySupport = false, + AllowProductLinks = false, + MaxProductLinks = 0, + PlanType = "trial" + }, + PlanType.Basic => new PlanLimitations + { + MaxLinks = GetConfigValue(PlanType.Basic, "MaxLinks", 8), + AllowCustomThemes = GetConfigValue(PlanType.Basic, "AllowPremiumThemes", false), + AllowAnalytics = GetConfigValue(PlanType.Basic, "AllowAnalytics", true), + AllowCustomDomain = true, + AllowMultipleDomains = false, + PrioritySupport = false, + AllowProductLinks = GetConfigValue(PlanType.Basic, "AllowProductLinks", false), + MaxProductLinks = 0, + PlanType = "basic" + }, + PlanType.Professional => new PlanLimitations + { + MaxLinks = GetConfigValue(PlanType.Professional, "MaxLinks", 20), + AllowCustomThemes = GetConfigValue(PlanType.Professional, "AllowPremiumThemes", false), + AllowAnalytics = GetConfigValue(PlanType.Professional, "AllowAnalytics", true), + AllowCustomDomain = true, + AllowMultipleDomains = false, + PrioritySupport = false, + AllowProductLinks = GetConfigValue(PlanType.Professional, "AllowProductLinks", false), + MaxProductLinks = 0, + PlanType = "professional" + }, + PlanType.Premium => new PlanLimitations + { + MaxLinks = GetConfigValue(PlanType.Premium, "MaxLinks", -1), + AllowCustomThemes = GetConfigValue(PlanType.Premium, "AllowPremiumThemes", true), + AllowAnalytics = GetConfigValue(PlanType.Premium, "AllowAnalytics", true), + AllowCustomDomain = true, + AllowMultipleDomains = true, + PrioritySupport = true, + AllowProductLinks = GetConfigValue(PlanType.Premium, "AllowProductLinks", false), + MaxProductLinks = 0, + PlanType = "premium" + }, + PlanType.PremiumAffiliate => new PlanLimitations + { + MaxLinks = GetConfigValue(PlanType.PremiumAffiliate, "MaxLinks", -1), + AllowCustomThemes = GetConfigValue(PlanType.PremiumAffiliate, "AllowPremiumThemes", true), + AllowAnalytics = GetConfigValue(PlanType.PremiumAffiliate, "AllowAnalytics", true), + AllowCustomDomain = true, + AllowMultipleDomains = true, + PrioritySupport = true, + AllowProductLinks = GetConfigValue(PlanType.PremiumAffiliate, "AllowProductLinks", true), + MaxProductLinks = 10, + PlanType = "premiumaffiliate" + }, + _ => new PlanLimitations { PlanType = "trial" } + }; + } + + public string GetPriceId(PlanType planType, bool yearly = false) + { + var planName = planType switch + { + PlanType.Basic => yearly ? "BasicYearly" : "Basic", + PlanType.Professional => yearly ? "ProfessionalYearly" : "Professional", + PlanType.Premium => yearly ? "PremiumYearly" : "Premium", + PlanType.PremiumAffiliate => yearly ? "PremiumAffiliateYearly" : "PremiumAffiliate", + _ => "Trial" + }; + + return _plans.TryGetValue(planName, out var config) ? config.PriceId : string.Empty; + } + + public decimal GetPlanPrice(PlanType planType, bool yearly = false) + { + var planName = planType switch + { + PlanType.Basic => yearly ? "BasicYearly" : "Basic", + PlanType.Professional => yearly ? "ProfessionalYearly" : "Professional", + PlanType.Premium => yearly ? "PremiumYearly" : "Premium", + PlanType.PremiumAffiliate => yearly ? "PremiumAffiliateYearly" : "PremiumAffiliate", + _ => "Trial" + }; + + return _plans.TryGetValue(planName, out var config) ? config.Price : 0; + } + + public bool IsYearlyPlan(string priceId) + { + if (string.IsNullOrEmpty(priceId) || !_priceIdToPlanName.TryGetValue(priceId, out var planName)) + return false; + + return _plans.TryGetValue(planName, out var config) && config.Interval == "year"; + } + + public PlanConfiguration? GetPlanConfiguration(string planSectionName) + { + return _plans.TryGetValue(planSectionName, out var config) ? config : null; + } + + private Dictionary LoadPlansFromConfiguration() + { + var plans = new Dictionary(); + var plansSection = _configuration.GetSection("Plans"); + + foreach (var planSection in plansSection.GetChildren()) + { + var config = new PlanConfiguration(); + planSection.Bind(config); + + // Mapear o nome da seção para PlanType base + config.BasePlanType = planSection.Key switch + { + "Basic" or "BasicYearly" => PlanType.Basic, + "Professional" or "ProfessionalYearly" => PlanType.Professional, + "Premium" or "PremiumYearly" => PlanType.Premium, + "PremiumAffiliate" or "PremiumAffiliateYearly" => PlanType.PremiumAffiliate, + _ => PlanType.Trial + }; + + plans[planSection.Key] = config; + } + + return plans; + } + + private Dictionary BuildPriceIdToPlanNameMap() + { + var map = new Dictionary(); + + foreach (var kvp in _plans) + { + if (!string.IsNullOrEmpty(kvp.Value.PriceId)) + { + map[kvp.Value.PriceId] = kvp.Key; + } + } + + return map; + } + + private Dictionary BuildPriceIdToPlanTypeMap() + { + var map = new Dictionary(); + + foreach (var kvp in _plans) + { + if (!string.IsNullOrEmpty(kvp.Value.PriceId)) + { + map[kvp.Value.PriceId] = kvp.Value.BasePlanType; + } + } + + return map; + } + + private T GetConfigValue(PlanType planType, string propertyName, T defaultValue) + { + // Buscar primeira nas configurações mensais, depois anuais + var monthlyPlan = planType switch + { + PlanType.Basic => "Basic", + PlanType.Professional => "Professional", + PlanType.Premium => "Premium", + PlanType.PremiumAffiliate => "PremiumAffiliate", + _ => null + }; + + if (monthlyPlan != null && _plans.TryGetValue(monthlyPlan, out var config)) + { + var property = typeof(PlanConfiguration).GetProperty(propertyName); + if (property != null) + { + var value = property.GetValue(config); + if (value != null && value is T typedValue) + { + return typedValue; + } + } + } + + return defaultValue; + } +} \ No newline at end of file diff --git a/src/BCards.Web/appsettings.json b/src/BCards.Web/appsettings.json index 09345c3..e14273a 100644 --- a/src/BCards.Web/appsettings.json +++ b/src/BCards.Web/appsettings.json @@ -57,7 +57,7 @@ "Interval": "month" }, "PremiumAffiliate": { - "Name": "Premium + Afiliados", + "Name": "Premium+Afiliados", "PriceId": "price_1RycTaBMIadsOxJVeDLseXQq", "Price": 29.90, "MaxPages": 15, @@ -107,7 +107,7 @@ "Interval": "year" }, "PremiumAffiliateYearly": { - "Name": "Premium + Afiliados Anual", + "Name": "Premium+Afiliados Anual", "PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1", "Price": 299.00, "MaxPages": 15,