From 2edb4e119625fa6ed9dfa3935ec7a6a9bb77d54f Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sun, 19 Oct 2025 21:48:45 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20ajustar=20para=20configura=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20plano=20ficam=20s=C3=B3=20no=20mongondb.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 6 +- Controllers/AdminController.cs | 142 ++++++++++++ Controllers/PagamentoController.cs | 334 ++++++++++++++--------------- Properties/launchSettings.json | 2 +- Scripts/README-ADMIN-API.md | 272 +++++++++++++++++++++++ Scripts/plans.json | 134 ++++++++++++ Scripts/seed-mongodb-plans.js | 16 +- Scripts/update-plans.sh | 63 ++++++ Services/StripeService.cs | 20 +- Views/Shared/_AdSpace.cshtml | 11 +- appsettings.Development.json | 18 ++ appsettings.json | 19 +- 12 files changed, 834 insertions(+), 203 deletions(-) create mode 100644 Controllers/AdminController.cs create mode 100644 Scripts/README-ADMIN-API.md create mode 100644 Scripts/plans.json create mode 100644 Scripts/update-plans.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f610e2e..a0af5a0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -29,8 +29,10 @@ "Bash(nc:*)", "Bash(ssh:*)", "Read(//mnt/c/vscode/**)", - "Read(//mnt/c/**)" + "Read(//mnt/c/**)", + "Bash(chmod +x /mnt/c/vscode/qrrapido/Scripts/update-plans.sh)", + "Bash(netstat -tln)" ], "deny": [] } -} \ No newline at end of file +} diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs new file mode 100644 index 0000000..ecee3e1 --- /dev/null +++ b/Controllers/AdminController.cs @@ -0,0 +1,142 @@ +using Microsoft.AspNetCore.Mvc; +using QRRapidoApp.Data; +using QRRapidoApp.Models; +using MongoDB.Driver; + +namespace QRRapidoApp.Controllers +{ + /// + /// Admin controller - ONLY accessible from localhost for security + /// + [ApiController] + [Route("api/[controller]")] + public class AdminController : ControllerBase + { + private readonly MongoDbContext _context; + private readonly ILogger _logger; + + public AdminController(MongoDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + /// + /// Seed/Update MongoDB Plans collection + /// Only accessible from localhost (127.0.0.1 or ::1) + /// + [HttpPost("SeedPlans")] + public async Task SeedPlans([FromBody] List plans) + { + // SECURITY: Only allow from localhost + var remoteIp = HttpContext.Connection.RemoteIpAddress; + var isLocalhost = remoteIp != null && + (remoteIp.ToString() == "127.0.0.1" || + remoteIp.ToString() == "::1" || + remoteIp.ToString() == "localhost"); + + if (!isLocalhost) + { + _logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}"); + return Forbid("This endpoint is only accessible from localhost"); + } + + try + { + _logger.LogInformation($"SeedPlans called from localhost - Upserting {plans.Count} plans"); + + foreach (var plan in plans) + { + // Upsert based on interval (month/year) + var filter = Builders.Filter.Eq(p => p.Interval, plan.Interval); + var options = new ReplaceOptions { IsUpsert = true }; + + await _context.Plans.ReplaceOneAsync(filter, plan, options); + _logger.LogInformation($"Upserted plan: {plan.Interval}"); + } + + return Ok(new { + success = true, + message = $"{plans.Count} plans seeded successfully", + plans = plans.Select(p => new { + interval = p.Interval, + priceIds = p.PricesByCountry.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.StripePriceId + ) + }) + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error seeding plans"); + return StatusCode(500, new { success = false, error = ex.Message }); + } + } + + /// + /// Get all plans from MongoDB + /// Only accessible from localhost + /// + [HttpGet("Plans")] + public async Task GetPlans() + { + // SECURITY: Only allow from localhost + var remoteIp = HttpContext.Connection.RemoteIpAddress; + var isLocalhost = remoteIp != null && + (remoteIp.ToString() == "127.0.0.1" || + remoteIp.ToString() == "::1" || + remoteIp.ToString() == "localhost"); + + if (!isLocalhost) + { + _logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}"); + return Forbid("This endpoint is only accessible from localhost"); + } + + try + { + var plans = await _context.Plans.Find(_ => true).ToListAsync(); + return Ok(new { success = true, count = plans.Count, plans }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving plans"); + return StatusCode(500, new { success = false, error = ex.Message }); + } + } + + /// + /// Delete all plans from MongoDB + /// Only accessible from localhost + /// + [HttpDelete("Plans")] + public async Task DeleteAllPlans() + { + // SECURITY: Only allow from localhost + var remoteIp = HttpContext.Connection.RemoteIpAddress; + var isLocalhost = remoteIp != null && + (remoteIp.ToString() == "127.0.0.1" || + remoteIp.ToString() == "::1" || + remoteIp.ToString() == "localhost"); + + if (!isLocalhost) + { + _logger.LogWarning($"Unauthorized admin access attempt from {remoteIp}"); + return Forbid("This endpoint is only accessible from localhost"); + } + + try + { + var result = await _context.Plans.DeleteManyAsync(_ => true); + _logger.LogInformation($"Deleted {result.DeletedCount} plans"); + return Ok(new { success = true, deletedCount = result.DeletedCount }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting plans"); + return StatusCode(500, new { success = false, error = ex.Message }); + } + } + } +} diff --git a/Controllers/PagamentoController.cs b/Controllers/PagamentoController.cs index b88935d..9dc798e 100644 --- a/Controllers/PagamentoController.cs +++ b/Controllers/PagamentoController.cs @@ -1,172 +1,172 @@ - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using QRRapidoApp.Services; -using System.Security.Claims; -using System.Threading.Tasks; -using QRRapidoApp.Models.ViewModels; -using System.Linq; - -namespace QRRapidoApp.Controllers -{ - [Authorize] - public class PagamentoController : Controller - { - private readonly IPlanService _planService; - private readonly AdDisplayService _adDisplayService; - private readonly IUserService _userService; - private readonly StripeService _stripeService; - private readonly ILogger _logger; - private readonly List languages = new List { "pt-BR", "es-PY", "es" }; - - public PagamentoController(IPlanService planService, IUserService userService, StripeService stripeService, ILogger logger, AdDisplayService adDisplayService) - { - _planService = planService; - _userService = userService; - _stripeService = stripeService; - _logger = logger; - _adDisplayService = adDisplayService; - } - - [HttpGet] - public async Task SelecaoPlano() - { - var plans = await _planService.GetActivePlansAsync(); - var countryCode = GetUserCountryCodeComplete(); // Implement this method based on your needs - _adDisplayService.SetViewBagAds(ViewBag); - - var model = new SelecaoPlanoViewModel - { - Plans = plans, - CountryCode = countryCode - }; - - return View(model); - } - - [HttpPost] - public async Task CreateCheckout(string planId, string lang) - { - var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userId)) - { - return Json(new { success = false, error = "User not authenticated" }); - } - - var plan = await _planService.GetPlanByIdAsync(planId); - if (plan == null) - { - return Json(new { success = false, error = "Plan not found" }); - } - - var countryCode = GetUserCountryCode(); + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using QRRapidoApp.Services; +using System.Security.Claims; +using System.Threading.Tasks; +using QRRapidoApp.Models.ViewModels; +using System.Linq; + +namespace QRRapidoApp.Controllers +{ + [Authorize] + public class PagamentoController : Controller + { + private readonly IPlanService _planService; + private readonly AdDisplayService _adDisplayService; + private readonly IUserService _userService; + private readonly StripeService _stripeService; + private readonly ILogger _logger; + private readonly List languages = new List { "pt-BR", "es-PY", "es" }; + + public PagamentoController(IPlanService planService, IUserService userService, StripeService stripeService, ILogger logger, AdDisplayService adDisplayService) + { + _planService = planService; + _userService = userService; + _stripeService = stripeService; + _logger = logger; + _adDisplayService = adDisplayService; + } + + [HttpGet] + public async Task SelecaoPlano() + { + var plans = await _planService.GetActivePlansAsync(); + var countryCode = GetUserCountryCodeComplete(); // Implement this method based on your needs + _adDisplayService.SetViewBagAds(ViewBag); + + var model = new SelecaoPlanoViewModel + { + Plans = plans, + CountryCode = countryCode + }; + + return View(model); + } + + [HttpPost] + public async Task CreateCheckout(string planId, string lang) + { + var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userId)) + { + return Json(new { success = false, error = "User not authenticated" }); + } + + var plan = await _planService.GetPlanByIdAsync(planId); + if (plan == null) + { + return Json(new { success = false, error = "Plan not found" }); + } + + var countryCode = GetUserCountryCode(); if (countryCode != lang && languages.Contains(lang)) { countryCode = lang; - } - - var priceId = plan.PricesByCountry.ContainsKey(countryCode) - ? plan.PricesByCountry[countryCode].StripePriceId - : plan.StripePriceId; - - try - { - var checkoutUrl = await _stripeService.CreateCheckoutSessionAsync(userId, priceId, lang); - return Json(new { success = true, url = checkoutUrl }); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error creating checkout session for user {userId} and plan {planId}"); - return Json(new { success = false, error = ex.Message }); - } - } - - [HttpGet] - public IActionResult Sucesso() - { - _adDisplayService.SetViewBagAds(ViewBag); - ViewBag.SuccessMessage = "Pagamento concluído com sucesso! Bem-vindo ao Premium."; - return View(); - } - - [HttpGet] - public async Task Cancelar() - { - _adDisplayService.SetViewBagAds(ViewBag); - ViewBag.CancelMessage = "O pagamento foi cancelado. Você pode tentar novamente a qualquer momento."; - - var plans = await _planService.GetActivePlansAsync(); - var countryCode = GetUserCountryCode(); // Implement this method based on your needs - _adDisplayService.SetViewBagAds(ViewBag); - - var model = new SelecaoPlanoViewModel - { - Plans = plans, - CountryCode = countryCode - }; - - return View("SelecaoPlano", model); - } - - [HttpPost] - [AllowAnonymous] - public async Task StripeWebhook() - { - try - { - using var reader = new StreamReader(HttpContext.Request.Body); - var json = await reader.ReadToEndAsync(); - var signature = Request.Headers["Stripe-Signature"].FirstOrDefault(); - - if (string.IsNullOrEmpty(signature)) - { - return BadRequest("Missing Stripe signature"); - } - - await _stripeService.HandleWebhookAsync(json, signature); - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing Stripe webhook"); - return BadRequest(ex.Message); - } - } - - private string GetUserCountryCode() - { - // Check current culture from URL first + } + + var priceId = plan.PricesByCountry.ContainsKey(countryCode) + ? plan.PricesByCountry[countryCode].StripePriceId + : plan.StripePriceId; + + try + { + var checkoutUrl = await _stripeService.CreateCheckoutSessionAsync(userId, priceId, lang); + return Json(new { success = true, url = checkoutUrl }); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error creating checkout session for user {userId} and plan {planId}"); + return Json(new { success = false, error = ex.Message }); + } + } + + [HttpGet] + public IActionResult Sucesso() + { + _adDisplayService.SetViewBagAds(ViewBag); + ViewBag.SuccessMessage = "Pagamento concluído com sucesso! Bem-vindo ao Premium."; + return View(); + } + + [HttpGet] + public async Task Cancelar() + { + _adDisplayService.SetViewBagAds(ViewBag); + ViewBag.CancelMessage = "O pagamento foi cancelado. Você pode tentar novamente a qualquer momento."; + + var plans = await _planService.GetActivePlansAsync(); + var countryCode = GetUserCountryCode(); // Implement this method based on your needs + _adDisplayService.SetViewBagAds(ViewBag); + + var model = new SelecaoPlanoViewModel + { + Plans = plans, + CountryCode = countryCode + }; + + return View("SelecaoPlano", model); + } + + [HttpPost] + [AllowAnonymous] + public async Task StripeWebhook() + { + try + { + using var reader = new StreamReader(HttpContext.Request.Body); + var json = await reader.ReadToEndAsync(); + var signature = Request.Headers["Stripe-Signature"].FirstOrDefault(); + + if (string.IsNullOrEmpty(signature)) + { + return BadRequest("Missing Stripe signature"); + } + + await _stripeService.HandleWebhookAsync(json, signature); + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing Stripe webhook"); + return BadRequest(ex.Message); + } + } + + private string GetUserCountryCode() + { + // Check current culture from URL first var culture = HttpContext.Request.RouteValues["culture"]?.ToString() ?? - HttpContext.Features.Get()?.RequestCulture?.Culture?.Name; - - var countryMap = new Dictionary - { - { "pt-BR", "BR" }, - { "es-PY", "PY" }, - { "es", "PY" } - }; - - if (!string.IsNullOrEmpty(culture) && countryMap.ContainsKey(culture)) - { - return countryMap[culture]; - } - - // Fallback to Cloudflare header or default - return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR"; - } - private string GetUserCountryCodeComplete() - { - // Check current culture from URL first + HttpContext.Features.Get()?.RequestCulture?.Culture?.Name; + + var countryMap = new Dictionary + { + { "pt-BR", "BR" }, + { "es-PY", "PY" }, + { "es", "PY" } + }; + + if (!string.IsNullOrEmpty(culture) && countryMap.ContainsKey(culture)) + { + return countryMap[culture]; + } + + // Fallback to Cloudflare header or default + return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR"; + } + private string GetUserCountryCodeComplete() + { + // Check current culture from URL first var culture = HttpContext.Request.RouteValues["culture"]?.ToString() ?? - HttpContext.Features.Get()?.RequestCulture?.Culture?.Name; - - if (languages.Contains(culture)) - { - return culture; - } - - // Fallback to Cloudflare header or default - return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR"; - } - } -} + HttpContext.Features.Get()?.RequestCulture?.Culture?.Name; + + if (languages.Contains(culture)) + { + return culture; + } + + // Fallback to Cloudflare header or default + return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR"; + } + } +} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 1bc9734..20dd78a 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:52428;http://localhost:52429" + "applicationUrl": "https://0.0.0.0:52428;http://0.0.0.0:52429" } } } \ No newline at end of file diff --git a/Scripts/README-ADMIN-API.md b/Scripts/README-ADMIN-API.md new file mode 100644 index 0000000..c38c51a --- /dev/null +++ b/Scripts/README-ADMIN-API.md @@ -0,0 +1,272 @@ +# 🔧 Admin API - Gerenciamento de Planos + +Este diretório contém ferramentas para gerenciar os planos do Stripe no MongoDB via API local. + +## 🔒 Segurança + +**IMPORTANTE**: O endpoint `/api/Admin/*` **só funciona quando acessado de localhost** (127.0.0.1). Qualquer acesso externo retornará `403 Forbidden`. + +--- + +## 📋 Arquivos + +- **`plans.json`** - Configuração dos planos com Price IDs do Stripe +- **`update-plans.sh`** - Script bash para atualizar planos via curl +- **`seed-mongodb-plans.js`** - Script MongoDB direto (método alternativo) + +--- + +## 🚀 Método 1: API Endpoint (Recomendado) + +### Vantagens +- ✅ Não precisa de credenciais do MongoDB +- ✅ Validação automática +- ✅ Logs no sistema +- ✅ Funciona com app rodando + +### Como Usar + +#### 1️⃣ Edite o `plans.json` com seus Price IDs + +```json +{ + "pricesByCountry": { + "BR": { + "stripePriceId": "price_SEU_ID_AQUI" + } + } +} +``` + +#### 2️⃣ Inicie a aplicação + +```bash +dotnet run +# ou +dotnet watch run +``` + +#### 3️⃣ Execute o script de atualização + +```bash +# Linux/Mac/WSL +cd Scripts +./update-plans.sh + +# Especificar porta diferente +./update-plans.sh 5000 +``` + +#### 4️⃣ Ou use curl diretamente + +```bash +# Atualizar planos +curl -k -X POST \ + -H "Content-Type: application/json" \ + -d @Scripts/plans.json \ + https://localhost:52428/api/Admin/SeedPlans + +# Listar planos +curl -k https://localhost:52428/api/Admin/Plans + +# Deletar todos os planos +curl -k -X DELETE https://localhost:52428/api/Admin/Plans +``` + +--- + +## 🗄️ Método 2: Script MongoDB Direto + +### Vantagens +- ✅ Funciona sem a app rodando +- ✅ Acesso direto ao MongoDB + +### Desvantagens +- ❌ Precisa de credenciais do MongoDB +- ❌ Sem validação automática + +### Como Usar + +```bash +# Localhost +mongosh "mongodb://localhost:27017/QrRapido" < Scripts/seed-mongodb-plans.js + +# Produção +mongosh "mongodb://user:pass@host:27017/QrRapido?replicaSet=rs0&authSource=admin" < Scripts/seed-mongodb-plans.js +``` + +--- + +## 📝 Endpoints Disponíveis + +### POST `/api/Admin/SeedPlans` +Cria ou atualiza planos no MongoDB + +**Request Body:** +```json +[ + { + "interval": "month", + "stripePriceId": "price_...", + "pricesByCountry": { ... } + } +] +``` + +**Response:** +```json +{ + "success": true, + "message": "2 plans seeded successfully", + "plans": [ ... ] +} +``` + +--- + +### GET `/api/Admin/Plans` +Lista todos os planos do MongoDB + +**Response:** +```json +{ + "success": true, + "count": 2, + "plans": [ ... ] +} +``` + +--- + +### DELETE `/api/Admin/Plans` +Remove todos os planos do MongoDB + +**Response:** +```json +{ + "success": true, + "deletedCount": 2 +} +``` + +--- + +## 🛡️ Proteção de Segurança + +O código verifica o IP de origem: + +```csharp +var remoteIp = HttpContext.Connection.RemoteIpAddress; +var isLocalhost = remoteIp != null && + (remoteIp.ToString() == "127.0.0.1" || + remoteIp.ToString() == "::1" || + remoteIp.ToString() == "localhost"); + +if (!isLocalhost) { + return Forbid("This endpoint is only accessible from localhost"); +} +``` + +**Tentativas de acesso externo são logadas:** +``` +[Warning] Unauthorized admin access attempt from 192.168.1.100 +``` + +--- + +## 🧪 Testando + +```bash +# 1. Listar planos atuais +curl -k https://localhost:52428/api/Admin/Plans | jq '.' + +# 2. Deletar planos +curl -k -X DELETE https://localhost:52428/api/Admin/Plans + +# 3. Criar novos planos +curl -k -X POST \ + -H "Content-Type: application/json" \ + -d @Scripts/plans.json \ + https://localhost:52428/api/Admin/SeedPlans | jq '.' + +# 4. Verificar se foram criados +curl -k https://localhost:52428/api/Admin/Plans | jq '.plans[] | {interval, priceIds: .pricesByCountry}' +``` + +--- + +## ⚠️ Troubleshooting + +### Erro: "Connection refused" +- ✅ Certifique-se que a app está rodando: `dotnet run` + +### Erro: "Forbidden" mesmo em localhost +- ✅ Verifique se está usando `https://localhost` (não `http://`) +- ✅ Verifique se está usando `localhost` ou `127.0.0.1` (não o IP da máquina) + +### Erro: "SSL certificate problem" +- ✅ Use o flag `-k` no curl para aceitar certificados auto-assinados + +--- + +## 📊 Estrutura do plans.json + +```json +[ + { + "name": { + "pt-BR": "Nome em português", + "es-PY": "Nombre en español", + "en": "Name in english" + }, + "description": { ... }, + "features": { ... }, + "interval": "month" ou "year", + "stripePriceId": "price_default", + "pricesByCountry": { + "BR": { + "amount": 9.90, + "currency": "BRL", + "stripePriceId": "price_BR_ID" + }, + "PY": { + "amount": 35000, + "currency": "PYG", + "stripePriceId": "price_PY_ID" + } + }, + "isActive": true, + "displayOrder": 1, + "badge": { ... } // Opcional + } +] +``` + +--- + +## 🎯 Fluxo Recomendado + +1. **Desenvolvimento (localhost)**: + ```bash + # Editar plans.json com Price IDs de teste + ./update-plans.sh + ``` + +2. **Staging**: + ```bash + # SSH no servidor staging + ssh user@staging-server + cd /app/qrrapido + ./Scripts/update-plans.sh 5000 + ``` + +3. **Produção**: + ```bash + # Opção 1: Via API (se tiver acesso SSH) + ssh user@prod-server + cd /app/qrrapido + ./Scripts/update-plans.sh 5001 + + # Opção 2: Via MongoDB direto + mongosh "connection_string" < Scripts/seed-mongodb-plans.js + ``` diff --git a/Scripts/plans.json b/Scripts/plans.json new file mode 100644 index 0000000..345887a --- /dev/null +++ b/Scripts/plans.json @@ -0,0 +1,134 @@ +[ + { + "name": { + "pt-BR": "Premium Mensal", + "es-PY": "Premium Mensual", + "en": "Premium Monthly" + }, + "description": { + "pt-BR": "Acesso ilimitado a todos os recursos premium - Cobrança mensal", + "es-PY": "Acceso ilimitado a todas las funciones premium - Facturación mensual", + "en": "Unlimited access to all premium features - Monthly billing" + }, + "features": { + "pt-BR": [ + "QR Codes ilimitados", + "Sem anúncios", + "Customização avançada", + "Suporte para logos", + "Histórico e downloads", + "Suporte prioritário", + "Cancele a qualquer momento" + ], + "es-PY": [ + "Códigos QR ilimitados", + "Sin anuncios", + "Personalización avanzada", + "Soporte para logos", + "Historial y descargas", + "Soporte prioritario", + "Cancela cuando quieras" + ], + "en": [ + "Unlimited QR Codes", + "No ads", + "Advanced customization", + "Logo support", + "History and downloads", + "Priority support", + "Cancel anytime" + ] + }, + "interval": "month", + "stripePriceId": "price_1SJwebB6bFjHQirAloEqXWd6", + "pricesByCountry": { + "BR": { + "amount": 9.90, + "currency": "BRL", + "stripePriceId": "price_1SJwebB6bFjHQirAloEqXWd6" + }, + "PY": { + "amount": 35000, + "currency": "PYG", + "stripePriceId": "price_1SK4Y0B6bFjHQirAaxNHxILi" + }, + "US": { + "amount": 1.99, + "currency": "USD", + "stripePriceId": "price_XXXXX_monthly_us" + } + }, + "isActive": true, + "displayOrder": 1 + }, + { + "name": { + "pt-BR": "Premium Anual", + "es-PY": "Premium Anual", + "en": "Premium Yearly" + }, + "description": { + "pt-BR": "Acesso ilimitado a todos os recursos premium - Economia de 20% no plano anual!", + "es-PY": "Acceso ilimitado a todas las funciones premium - ¡Ahorra 20% con el plan anual!", + "en": "Unlimited access to all premium features - Save 20% with yearly billing!" + }, + "features": { + "pt-BR": [ + "QR Codes ilimitados", + "Sem anúncios", + "Customização avançada", + "Suporte para logos", + "Histórico e downloads", + "Suporte prioritário", + "💰 Economia de 20%", + "Cobrança anual única" + ], + "es-PY": [ + "Códigos QR ilimitados", + "Sin anuncios", + "Personalización avanzada", + "Soporte para logos", + "Historial y descargas", + "Soporte prioritario", + "💰 Ahorro del 20%", + "Facturación anual única" + ], + "en": [ + "Unlimited QR Codes", + "No ads", + "Advanced customization", + "Logo support", + "History and downloads", + "Priority support", + "💰 Save 20%", + "Billed annually" + ] + }, + "interval": "year", + "stripePriceId": "price_1SK4X7B6bFjHQirAdMtviPw4", + "pricesByCountry": { + "BR": { + "amount": 95.04, + "currency": "BRL", + "stripePriceId": "price_1SK4X7B6bFjHQirAdMtviPw4" + }, + "PY": { + "amount": 336000, + "currency": "PYG", + "stripePriceId": "price_1SK4Y0B6bFjHQirAaxNHxILi" + }, + "US": { + "amount": 19.10, + "currency": "USD", + "stripePriceId": "price_XXXXX_yearly_us" + } + }, + "isActive": true, + "displayOrder": 2, + "badge": { + "pt-BR": "MELHOR VALOR", + "es-PY": "MEJOR VALOR", + "en": "BEST VALUE" + } + } +] diff --git a/Scripts/seed-mongodb-plans.js b/Scripts/seed-mongodb-plans.js index 6f51bbf..ef44a10 100644 --- a/Scripts/seed-mongodb-plans.js +++ b/Scripts/seed-mongodb-plans.js @@ -51,22 +51,22 @@ db.Plans.insertOne({ ] }, interval: "month", - stripePriceId: "price_XXXXX_monthly_us", // Default price (USA) - UPDATE from appsettings.json > Stripe:Plans:Monthly:US + stripePriceId: "price_1SJwebB6bFjHQirAloEqXWd6", // Default price (BR) pricesByCountry: { "BR": { amount: 9.90, currency: "BRL", - stripePriceId: "price_XXXXX_monthly_br" // UPDATE from appsettings.json > Stripe:Plans:Monthly:BR + stripePriceId: "price_1SJwebB6bFjHQirAloEqXWd6" }, "PY": { amount: 35000, currency: "PYG", - stripePriceId: "price_XXXXX_monthly_py" // UPDATE from appsettings.json > Stripe:Plans:Monthly:PY + stripePriceId: "price_1SK4Y0B6bFjHQirAaxNHxILi" }, "US": { amount: 1.99, currency: "USD", - stripePriceId: "price_XXXXX_monthly_us" // UPDATE from appsettings.json > Stripe:Plans:Monthly:US + stripePriceId: "price_XXXXX_monthly_us" // TODO: Update with real Stripe Price ID } }, isActive: true, @@ -120,22 +120,22 @@ db.Plans.insertOne({ ] }, interval: "year", - stripePriceId: "price_XXXXX_yearly_us", // Default price (USA) - UPDATE from appsettings.json > Stripe:Plans:Yearly:US + stripePriceId: "price_1SK4X7B6bFjHQirAdMtviPw4", // Default price (BR) pricesByCountry: { "BR": { amount: 95.04, // 9.90 * 12 * 0.80 = Economia de 20% currency: "BRL", - stripePriceId: "price_XXXXX_yearly_br" // UPDATE from appsettings.json > Stripe:Plans:Yearly:BR + stripePriceId: "price_1SK4X7B6bFjHQirAdMtviPw4" }, "PY": { amount: 336000, // 35000 * 12 * 0.80 = Economia de 20% currency: "PYG", - stripePriceId: "price_XXXXX_yearly_py" // UPDATE from appsettings.json > Stripe:Plans:Yearly:PY + stripePriceId: "price_1SK4Y0B6bFjHQirAaxNHxILi" }, "US": { amount: 19.10, // 1.99 * 12 * 0.80 = Economia de 20% currency: "USD", - stripePriceId: "price_XXXXX_yearly_us" // UPDATE from appsettings.json > Stripe:Plans:Yearly:US + stripePriceId: "price_XXXXX_yearly_us" // TODO: Update with real Stripe Price ID } }, isActive: true, diff --git a/Scripts/update-plans.sh b/Scripts/update-plans.sh new file mode 100644 index 0000000..26544ce --- /dev/null +++ b/Scripts/update-plans.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Script to update MongoDB Plans via localhost-only API endpoint +# Usage: ./update-plans.sh [port] + +PORT=${1:-52428} +API_URL="https://localhost:$PORT/api/Admin/SeedPlans" +PLANS_FILE="$(dirname "$0")/plans.json" + +echo "🔧 QR Rapido - Update Plans Script" +echo "==================================" +echo "" +echo "📋 Plans file: $PLANS_FILE" +echo "🌐 API URL: $API_URL" +echo "" + +# Check if plans.json exists +if [ ! -f "$PLANS_FILE" ]; then + echo "❌ Error: plans.json not found at $PLANS_FILE" + echo "" + echo "Please create plans.json with your Stripe Price IDs" + exit 1 +fi + +# Check if the app is running +echo "🔍 Checking if app is running on port $PORT..." +if ! curl -k -s "https://localhost:$PORT/health" > /dev/null 2>&1; then + echo "❌ Error: App not responding on https://localhost:$PORT" + echo "" + echo "Please start the app first:" + echo " dotnet run" + exit 1 +fi + +echo "✅ App is running" +echo "" + +# Send request +echo "📤 Sending plans to API..." +response=$(curl -k -s -w "\n%{http_code}" \ + -X POST \ + -H "Content-Type: application/json" \ + -d @"$PLANS_FILE" \ + "$API_URL") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +echo "" + +if [ "$http_code" -eq 200 ]; then + echo "✅ Success! Plans updated in MongoDB" + echo "" + echo "$body" | jq '.' 2>/dev/null || echo "$body" +else + echo "❌ Error! HTTP Status: $http_code" + echo "" + echo "$body" + exit 1 +fi + +echo "" +echo "🎉 Done!" diff --git a/Services/StripeService.cs b/Services/StripeService.cs index 1f4dbb2..39a7adb 100644 --- a/Services/StripeService.cs +++ b/Services/StripeService.cs @@ -33,6 +33,24 @@ namespace QRRapidoApp.Services } var customerId = user.StripeCustomerId; + var customerService = new CustomerService(); + + // Verify if customer exists in Stripe, create new if not + if (!string.IsNullOrEmpty(customerId)) + { + try + { + // Try to retrieve the customer to verify it exists + await customerService.GetAsync(customerId); + _logger.LogInformation($"Using existing Stripe customer {customerId} for user {userId}"); + } + catch (StripeException ex) when (ex.StripeError?.Code == "resource_missing") + { + _logger.LogWarning($"Stripe customer {customerId} not found, creating new one for user {userId}"); + customerId = null; // Force creation of new customer + } + } + if (string.IsNullOrEmpty(customerId)) { var customerOptions = new CustomerCreateOptions @@ -41,10 +59,10 @@ namespace QRRapidoApp.Services Name = user.Name, Metadata = new Dictionary { { "app_user_id", user.Id } } }; - var customerService = new CustomerService(); var customer = await customerService.CreateAsync(customerOptions); customerId = customer.Id; await _userService.UpdateUserStripeCustomerIdAsync(userId, customerId); + _logger.LogInformation($"Created new Stripe customer {customerId} for user {userId}"); } var options = new SessionCreateOptions diff --git a/Views/Shared/_AdSpace.cshtml b/Views/Shared/_AdSpace.cshtml index d27bd46..3d9812a 100644 --- a/Views/Shared/_AdSpace.cshtml +++ b/Views/Shared/_AdSpace.cshtml @@ -145,15 +145,9 @@ else if (User.Identity.IsAuthenticated) { var isPremium = await AdService.HasValidPremiumSubscription(userId); - if (isPremium) - { -
- - @Localizer["PremiumUserNoAds"] -
- } - else + if (!isPremium) { + @* Only show upgrade notice for non-premium users *@
@Localizer["UpgradePremiumRemoveAds"] @@ -162,4 +156,5 @@ else if (User.Identity.IsAuthenticated)
} + @* Premium users: render nothing (100% invisible) *@ } diff --git a/appsettings.Development.json b/appsettings.Development.json index e667b05..290425f 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -38,6 +38,24 @@ "GrowthRateWarningMBPerHour": 200, "IncludeCollectionStats": true }, + "Stripe": { + "PublishableKey": "pk_test_51Rs42SB6bFjHQirAJ6kzbFCbBuAobyNbmlgpULFsInl8KRzlpclUoqOZICqvp2S51kquw3Bc04CPO9bNUEgDLDgd00XbAHT7Fh", + "SecretKey": "sk_test_51Rs42SB6bFjHQirANOUg8jgzPALbNdVWULSVRMycFRBTzE0QUGA6pkpoQaTVsCIoV3XGRgoJ7E3CA6Y67vWlM76q00QBoKW0aH", + "WebhookSecret": "whsec_667402ff1d753b181f626636d556975f2749b5fec4d1231d44f040b057fb3009", + "ProductId": "prod_TGTbombliOUYmQ", + "Plans": { + "Monthly": { + "BR": "price_1SJwebB6bFjHQirAloEqXWd6", + "PY": "price_1SK4Y0B6bFjHQirAaxNHxILi", + "US": "price_XXXXX_monthly_us" + }, + "Yearly": { + "BR": "price_1SK4X7B6bFjHQirAdMtviPw4", + "PY": "price_1SK4Y0B6bFjHQirAaxNHxILi", + "US": "price_XXXXX_yearly_us" + } + } + }, "HealthChecks": { "MongoDB": { "TimeoutSeconds": 10, diff --git a/appsettings.json b/appsettings.json index 4656ddd..08b245d 100644 --- a/appsettings.json +++ b/appsettings.json @@ -21,22 +21,9 @@ } }, "Stripe": { - "PublishableKey": "pk_test_51Rs42tBeR5IFYUsBooapyDwQTgh6CFuKbya5R3MVDTrdOUKmgiHQYipU0pgOdG5iKogH77RUYIKBJzbCt5BghUOY00xitV5KiN", - "SecretKey": "sk_test_51Rs42tBeR5IFYUsBtycRlJJcdwgoMbh8MfQIKIGelBPTQFwDcOn2iCCbw5uG6hnqlpgNAUuFgWRAUUMA8qkABKun00EIx4odDF", - "WebhookSecret": "whsec_2e828803ceb48e7865458b0cf332b68535fdff8753d26d69b1c88ea55cb0e482", - "ProductId": "prod_SnfQTxwE3i8r5L", - "Plans": { - "Monthly": { - "BR": "price_1Rs45OBeR5IFYUsBfsnOpOiv", - "PY": "price_XXXXX_monthly_py", - "US": "price_XXXXX_monthly_us" - }, - "Yearly": { - "BR": "price_1Rs4AyBeR5IFYUsB8kRSNUIM", - "PY": "price_XXXXX_yearly_py", - "US": "price_XXXXX_yearly_us" - } - } + "PublishableKey": "pk_test_51Rs42SB6bFjHQirAJ6kzbFCbBuAobyNbmlgpULFsInl8KRzlpclUoqOZICqvp2S51kquw3Bc04CPO9bNUEgDLDgd00XbAHT7Fh", + "SecretKey": "sk_test_51Rs42SB6bFjHQirANOUg8jgzPALbNdVWULSVRMycFRBTzE0QUGA6pkpoQaTVsCIoV3XGRgoJ7E3CA6Y67vWlM76q00QBoKW0aH", + "WebhookSecret": "whsec_667402ff1d753b181f626636d556975f2749b5fec4d1231d44f040b057fb3009" }, "AdSense": { "ClientId": "ca-pub-3475956393038764",