All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 3s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m29s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 2m25s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
279 lines
10 KiB
C#
279 lines
10 KiB
C#
using BCards.Web.Models;
|
|
using BCards.Web.Repositories;
|
|
using BCards.Web.Services;
|
|
using BCards.Web.ViewModels;
|
|
using BCards.Web.Utils;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using System.Globalization;
|
|
|
|
namespace BCards.Web.Controllers;
|
|
|
|
[Authorize]
|
|
public class PaymentController : Controller
|
|
{
|
|
private readonly IPaymentService _paymentService;
|
|
private readonly IAuthService _authService;
|
|
private readonly IUserRepository _userService;
|
|
private readonly ISubscriptionRepository _subscriptionRepository;
|
|
private readonly IConfiguration _configuration;
|
|
|
|
public PaymentController(IPaymentService paymentService, IAuthService authService, IUserRepository userService, ISubscriptionRepository subscriptionRepository, IConfiguration configuration)
|
|
{
|
|
_paymentService = paymentService;
|
|
_authService = authService;
|
|
_userService = userService;
|
|
_subscriptionRepository = subscriptionRepository;
|
|
_configuration = configuration;
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> CreateCheckoutSession(string planType)
|
|
{
|
|
var user = await _authService.GetCurrentUserAsync(User);
|
|
if (user == null)
|
|
return RedirectToAction("Login", "Auth");
|
|
|
|
var successUrl = Url.Action("Success", "Payment", null, Request.Scheme);
|
|
var cancelUrl = Url.Action("Cancel", "Payment", null, Request.Scheme);
|
|
|
|
TempData[$"PlanType|{user.Id}"] = planType;
|
|
|
|
try
|
|
{
|
|
var checkoutUrl = await _paymentService.CreateCheckoutSessionAsync(
|
|
user.Id,
|
|
planType,
|
|
successUrl!,
|
|
cancelUrl!);
|
|
|
|
return Redirect(checkoutUrl);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TempData["Error"] = $"Erro ao processar pagamento: {ex.Message}";
|
|
return RedirectToAction("Pricing", "Home");
|
|
}
|
|
}
|
|
|
|
public async Task<IActionResult> Success()
|
|
{
|
|
try
|
|
{
|
|
var user = await _authService.GetCurrentUserAsync(User);
|
|
var planType = TempData[$"PlanType|{user.Id}"].ToString();
|
|
|
|
TempData["Success"] = $"Assinatura {planType} ativada com sucesso!";
|
|
return RedirectToAction("Dashboard", "Admin");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TempData["Error"] = $"Erro ao processar pagamento: {ex.Message}";
|
|
return RedirectToAction("Dashboard", "Admin");
|
|
}
|
|
}
|
|
|
|
public IActionResult Cancel()
|
|
{
|
|
TempData["Info"] = "Pagamento cancelado. Você pode tentar novamente quando quiser.";
|
|
return RedirectToAction("Pricing", "Home");
|
|
}
|
|
|
|
[HttpPost]
|
|
[Route("webhook/stripe")]
|
|
[AllowAnonymous]
|
|
public async Task<IActionResult> StripeWebhook()
|
|
{
|
|
var signature = Request.Headers["Stripe-Signature"].FirstOrDefault();
|
|
if (string.IsNullOrEmpty(signature))
|
|
return BadRequest();
|
|
|
|
string requestBody;
|
|
using (var reader = new StreamReader(Request.Body))
|
|
{
|
|
requestBody = await reader.ReadToEndAsync();
|
|
}
|
|
|
|
try
|
|
{
|
|
await _paymentService.HandleWebhookAsync(requestBody, signature);
|
|
return Ok();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest($"Webhook error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<IActionResult> ManageSubscription()
|
|
{
|
|
var user = await _authService.GetCurrentUserAsync(User);
|
|
if (user == null)
|
|
return RedirectToAction("Login", "Auth");
|
|
|
|
try
|
|
{
|
|
// Parse do plano atual (mesmo que o Dashboard)
|
|
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial;
|
|
var currentPlanString = userPlanType.ToString().ToLower();
|
|
var subscription = await _subscriptionRepository.GetByUserIdAsync(user.Id);
|
|
var viewModel = new ManageSubscriptionViewModel
|
|
{
|
|
User = user,
|
|
StripeSubscription = await _paymentService.GetSubscriptionDetailsAsync(user.Id),
|
|
PaymentHistory = await _paymentService.GetPaymentHistoryAsync(user.Id),
|
|
AvailablePlans = GetAvailablePlans(currentPlanString),
|
|
CurrentPeriodEnd = (DateTime?) subscription.CurrentPeriodEnd
|
|
};
|
|
|
|
// Pegar assinatura local se existir
|
|
if (!string.IsNullOrEmpty(user.StripeCustomerId))
|
|
{
|
|
// Aqui você poderia buscar a subscription local se necessário
|
|
// viewModel.LocalSubscription = await _subscriptionRepository.GetByUserIdAsync(user.Id);
|
|
}
|
|
|
|
return View(viewModel);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Parse do plano atual também no catch
|
|
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial;
|
|
var currentPlanString = userPlanType.ToString().ToLower();
|
|
|
|
var errorViewModel = new ManageSubscriptionViewModel
|
|
{
|
|
User = user,
|
|
ErrorMessage = $"Erro ao carregar dados da assinatura: {ex.Message}",
|
|
AvailablePlans = GetAvailablePlans(currentPlanString)
|
|
};
|
|
|
|
return View(errorViewModel);
|
|
}
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> CancelSubscription(string subscriptionId)
|
|
{
|
|
try
|
|
{
|
|
await _paymentService.CancelSubscriptionAsync(subscriptionId);
|
|
TempData["Success"] = "Sua assinatura será cancelada no final do período atual.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TempData["Error"] = $"Erro ao cancelar assinatura: {ex.Message}";
|
|
}
|
|
|
|
return RedirectToAction("ManageSubscription");
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> ChangePlan(string newPlanType)
|
|
{
|
|
var user = await _authService.GetCurrentUserAsync(User);
|
|
if (user == null)
|
|
return RedirectToAction("Login", "Auth");
|
|
|
|
try
|
|
{
|
|
// Para mudanças de plano, vamos usar o Stripe Checkout
|
|
var returnUrl = Url.Action("ManageSubscription", "Payment", null, Request.Scheme);
|
|
var cancelUrl = Url.Action("ManageSubscription", "Payment", null, Request.Scheme);
|
|
|
|
var checkoutUrl = await _paymentService.CreateCheckoutSessionAsync(
|
|
user.Id,
|
|
newPlanType,
|
|
returnUrl!,
|
|
cancelUrl!);
|
|
|
|
return Redirect(checkoutUrl);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TempData["Error"] = $"Erro ao alterar plano: {ex.Message}";
|
|
return RedirectToAction("ManageSubscription");
|
|
}
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> OpenStripePortal()
|
|
{
|
|
var user = await _authService.GetCurrentUserAsync(User);
|
|
if (user == null || string.IsNullOrEmpty(user.StripeCustomerId))
|
|
{
|
|
TempData["Error"] = "Erro: dados de assinatura não encontrados.";
|
|
return RedirectToAction("ManageSubscription");
|
|
}
|
|
|
|
try
|
|
{
|
|
var returnUrl = Url.Action("ManageSubscription", "Payment", null, Request.Scheme);
|
|
var portalUrl = await _paymentService.CreatePortalSessionAsync(user.StripeCustomerId, returnUrl!);
|
|
|
|
return Redirect(portalUrl);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TempData["Error"] = $"Erro ao abrir portal de pagamento: {ex.Message}";
|
|
return RedirectToAction("ManageSubscription");
|
|
}
|
|
}
|
|
|
|
private List<AvailablePlanViewModel> GetAvailablePlans(string currentPlan)
|
|
{
|
|
var plansConfig = _configuration.GetSection("Plans");
|
|
var plans = new List<AvailablePlanViewModel>();
|
|
|
|
// Adicionar planos mensais apenas (excluir Trial e planos anuais)
|
|
var monthlyPlans = new[] { "Basic", "Professional", "Premium", "PremiumAffiliate" };
|
|
|
|
foreach (var planKey in monthlyPlans)
|
|
{
|
|
var planSection = plansConfig.GetSection(planKey);
|
|
if (planSection.Exists())
|
|
{
|
|
plans.Add(new AvailablePlanViewModel
|
|
{
|
|
PlanType = planKey.ToLower(),
|
|
DisplayName = planSection["Name"] ?? planKey,
|
|
Price = decimal.Parse(planSection["Price"] ?? "0", new CultureInfo("en-US")),
|
|
PriceId = planSection["PriceId"] ?? "",
|
|
MaxLinks = int.Parse(planSection["MaxLinks"] ?? "0"),
|
|
AllowAnalytics = bool.Parse(planSection["AllowAnalytics"] ?? "false"),
|
|
AllowCustomDomain = true, // URL personalizada em todos os planos pagos
|
|
AllowCustomThemes = bool.Parse(planSection["AllowPremiumThemes"] ?? "false"),
|
|
AllowProductLinks = bool.Parse(planSection["AllowProductLinks"] ?? "false"),
|
|
Features = planSection.GetSection("Features").Get<List<string>>() ?? new List<string>(),
|
|
IsCurrentPlan = currentPlan.Equals(planKey, StringComparison.OrdinalIgnoreCase)
|
|
});
|
|
}
|
|
}
|
|
|
|
// Marcar upgrades e filtrar downgrades
|
|
var currentPlanIndex = plans.FindIndex(p => p.IsCurrentPlan);
|
|
|
|
// Se usuário está no Trial (não encontrou plano atual), todos são upgrades
|
|
if (currentPlanIndex == -1 && (currentPlan == "trial" || currentPlan == "free"))
|
|
{
|
|
foreach (var plan in plans)
|
|
{
|
|
plan.IsUpgrade = true;
|
|
}
|
|
return plans; // Mostrar todos os planos pagos como upgrade
|
|
}
|
|
|
|
// Para planos pagos, marcar apenas upgrades superiores
|
|
for (int i = 0; i < plans.Count; i++)
|
|
{
|
|
if (i > currentPlanIndex)
|
|
plans[i].IsUpgrade = true;
|
|
else if (i < currentPlanIndex)
|
|
plans[i].IsDowngrade = true;
|
|
}
|
|
|
|
// Retornar apenas plano atual e upgrades (Stripe não gerencia downgrades automaticamente)
|
|
return plans.Where(p => p.IsCurrentPlan || p.IsUpgrade).ToList();
|
|
}
|
|
} |