feat: ajustes de callback do stripe e atualização do stripe.net

This commit is contained in:
Ricardo Carneiro 2025-08-16 23:13:00 -03:00
parent c6129a1c63
commit 5fc7eb5ad3
12 changed files with 133 additions and 42 deletions

View File

@ -23,7 +23,7 @@
<PackageReference Include="PuppeteerSharp" Version="13.0.2" />
<PackageReference Include="MongoDB.Driver" Version="2.25.0" />
<PackageReference Include="Testcontainers.MongoDb" Version="3.6.0" />
<PackageReference Include="Stripe.net" Version="44.7.0" />
<PackageReference Include="Stripe.net" Version="48.4.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />

View File

@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.25.0" />
<PackageReference Include="Stripe.net" Version="44.7.0" />
<PackageReference Include="Stripe.net" Version="48.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />

View File

@ -278,7 +278,7 @@ public class AdminController : Controller
null,
previewUrl);
TempData["Success"] = "Página atualizada e enviada para moderação!";
TempData["Success"] = "Página atualizada! Teste e envie para moderação.";
}
return RedirectToAction("Dashboard");

View File

@ -54,21 +54,13 @@ public class PaymentController : Controller
}
public async Task<IActionResult> Success()
{
try
{
var user = await _authService.GetCurrentUserAsync(User);
var planType = TempData[$"PlanType|{user.Id}"].ToString();
try
{
if (!string.IsNullOrEmpty(planType) && Enum.TryParse<PlanType>(planType, out var plan))
{
user.CurrentPlan = plan.ToString();
user.SubscriptionStatus = "active";
await _userService.UpdateAsync(user); // ou o método equivalente
TempData["Success"] = $"Assinatura {planType} ativada com sucesso!";
}
return RedirectToAction("Dashboard", "Admin");
}
catch (Exception ex)

View File

@ -59,19 +59,19 @@ public class StripeWebhookController : ControllerBase
switch (stripeEvent.Type)
{
case Events.InvoicePaymentSucceeded:
case "invoice.payment_succeeded":
await HandlePaymentSucceeded(stripeEvent);
break;
case Events.InvoicePaymentFailed:
case "invoice.payment_failed":
await HandlePaymentFailed(stripeEvent);
break;
case Events.CustomerSubscriptionDeleted:
case "customer.subscription.deleted":
await HandleSubscriptionDeleted(stripeEvent);
break;
case Events.CustomerSubscriptionUpdated:
case "customer.subscription.updated":
await HandleSubscriptionUpdated(stripeEvent);
break;
@ -100,7 +100,8 @@ public class StripeWebhookController : ControllerBase
{
_logger.LogInformation($"Payment succeeded for customer: {invoice.CustomerId}");
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(invoice.SubscriptionId);
var subscriptionId = GetSubscriptionId(stripeEvent);
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
if (subscription != null)
{
subscription.Status = "active";
@ -127,7 +128,8 @@ public class StripeWebhookController : ControllerBase
{
_logger.LogInformation($"Payment failed for customer: {invoice.CustomerId}");
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(invoice.SubscriptionId);
var subscriptionId = GetSubscriptionId(stripeEvent);
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
if (subscription != null)
{
subscription.Status = "past_due";
@ -184,9 +186,12 @@ public class StripeWebhookController : ControllerBase
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id);
if (subscription != null)
{
var service = new SubscriptionItemService();
var subItem = service.Get(stripeSubscription.Items.Data[0].Id);
subscription.Status = stripeSubscription.Status;
subscription.CurrentPeriodStart = stripeSubscription.CurrentPeriodStart;
subscription.CurrentPeriodEnd = stripeSubscription.CurrentPeriodEnd;
subscription.CurrentPeriodStart = subItem.CurrentPeriodStart;
subscription.CurrentPeriodEnd = subItem.CurrentPeriodEnd;
subscription.CancelAtPeriodEnd = stripeSubscription.CancelAtPeriodEnd;
subscription.UpdatedAt = DateTime.UtcNow;
@ -216,4 +221,33 @@ public class StripeWebhookController : ControllerBase
_ => "trial"
};
}
private string GetSubscriptionId(Event stripeEvent)
{
if (stripeEvent.Data.Object is Invoice invoice)
{
var subscriptionLineItem = invoice.Lines?.Data
.FirstOrDefault(line =>
!string.IsNullOrEmpty(line.SubscriptionId) ||
line.Subscription != null
);
string subscriptionId = null;
if (subscriptionLineItem != null)
{
// Tenta obter o ID da assinatura de duas formas diferentes
subscriptionId = subscriptionLineItem.SubscriptionId
?? subscriptionLineItem.Subscription?.Id;
}
return subscriptionId;
}
else if (stripeEvent.Data.Object is Subscription stripeSubscription)
{
return stripeSubscription.Id;
}
return null;
}
}

View File

@ -5,6 +5,7 @@ using Microsoft.Extensions.Options;
using Stripe;
using Stripe.Checkout;
using Stripe.BillingPortal;
using System.Numerics;
namespace BCards.Web.Services;
@ -115,22 +116,22 @@ public class PaymentService : IPaymentService
{
try
{
var stripeEvent = EventUtility.ConstructEvent(requestBody, signature, _stripeSettings.WebhookSecret);
var stripeEvent = EventUtility.ConstructEvent(requestBody, signature, _stripeSettings.WebhookSecret, throwOnApiVersionMismatch: false);
switch (stripeEvent.Type)
{
case Events.CheckoutSessionCompleted:
case "checkout.session.completed":
var session = stripeEvent.Data.Object as Stripe.Checkout.Session;
await HandleCheckoutSessionCompletedAsync(session!);
break;
case Events.InvoicePaymentSucceeded:
case "invoice.finalized":
var invoice = stripeEvent.Data.Object as Invoice;
await HandleInvoicePaymentSucceededAsync(invoice!);
break;
case Events.CustomerSubscriptionUpdated:
case Events.CustomerSubscriptionDeleted:
case "customer.subscription.updated":
case "customer.subscription.deleted":
var subscription = stripeEvent.Data.Object as Stripe.Subscription;
await HandleSubscriptionUpdatedAsync(subscription!);
break;
@ -255,6 +256,9 @@ public class PaymentService : IPaymentService
var subscriptionService = new SubscriptionService();
var stripeSubscription = await subscriptionService.GetAsync(session.SubscriptionId);
var service = new SubscriptionItemService();
var subItem = service.Get(stripeSubscription.Items.Data[0].Id);
var limitations = await GetPlanLimitationsAsync(planType);
var subscription = new Models.Subscription
@ -263,8 +267,8 @@ public class PaymentService : IPaymentService
StripeSubscriptionId = session.SubscriptionId,
PlanType = planType,
Status = stripeSubscription.Status,
CurrentPeriodStart = stripeSubscription.CurrentPeriodStart,
CurrentPeriodEnd = stripeSubscription.CurrentPeriodEnd,
CurrentPeriodStart = subItem.CurrentPeriodStart,
CurrentPeriodEnd = subItem.CurrentPeriodEnd,
MaxLinks = limitations.MaxLinks,
AllowCustomThemes = limitations.AllowCustomThemes,
AllowAnalytics = limitations.AllowAnalytics,
@ -287,7 +291,8 @@ public class PaymentService : IPaymentService
private async Task HandleInvoicePaymentSucceededAsync(Invoice invoice)
{
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(invoice.SubscriptionId);
var subscriptionId = GetSubscriptionId(invoice);
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
if (subscription != null)
{
subscription.Status = "active";
@ -300,9 +305,12 @@ public class PaymentService : IPaymentService
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id);
if (subscription != null)
{
var service = new SubscriptionItemService();
var subItem = service.Get(stripeSubscription.Items.Data[0].Id);
subscription.Status = stripeSubscription.Status;
subscription.CurrentPeriodStart = stripeSubscription.CurrentPeriodStart;
subscription.CurrentPeriodEnd = stripeSubscription.CurrentPeriodEnd;
subscription.CurrentPeriodStart = subItem.CurrentPeriodStart;
subscription.CurrentPeriodEnd = subItem.CurrentPeriodEnd;
subscription.CancelAtPeriodEnd = stripeSubscription.CancelAtPeriodEnd;
await _subscriptionRepository.UpdateAsync(subscription);
@ -386,4 +394,29 @@ public class PaymentService : IPaymentService
throw new InvalidOperationException($"Erro ao criar sessão do portal: {ex.Message}");
}
}
private string GetSubscriptionId(Invoice? invoice)
{
if (invoice!=null)
{
var subscriptionLineItem = invoice.Lines?.Data
.FirstOrDefault(line =>
!string.IsNullOrEmpty(line.SubscriptionId) ||
line.Subscription != null
);
string subscriptionId = null;
if (subscriptionLineItem != null)
{
// Tenta obter o ID da assinatura de duas formas diferentes
subscriptionId = subscriptionLineItem.SubscriptionId
?? subscriptionLineItem.Subscription?.Id;
}
return subscriptionId;
}
return null;
}
}

View File

@ -126,5 +126,6 @@ public enum PageStatus
Inactive, // Pausada pelo usuário
PendingModeration = 4, // Aguardando moderação
Rejected = 5, // Rejeitada na moderação
Creating = 6 // Em desenvolvimento/criação
Creating = 6, // Em desenvolvimento/criação
Approved = 7 // Aprovada
}

View File

@ -19,7 +19,7 @@ public class ManageSubscriptionViewModel
public bool CanDowngrade => HasActiveSubscription && User.CurrentPlan != "basic";
public bool WillCancelAtPeriodEnd => StripeSubscription?.CancelAtPeriodEnd == true;
public DateTime? CurrentPeriodEnd => StripeSubscription?.CurrentPeriodEnd;
public DateTime? CurrentPeriodEnd => null;
public DateTime? NextBillingDate => !WillCancelAtPeriodEnd ? CurrentPeriodEnd : null;
public decimal? MonthlyAmount => StripeSubscription?.Items?.Data?.FirstOrDefault()?.Price?.UnitAmount / 100m;

View File

@ -111,7 +111,7 @@
onclick="openPreview('@pageItem.Id')"
data-page-category="@pageItem.Category"
data-page-slug="@pageItem.Slug">
<i class="fas fa-eye me-1"></i>Preview
<i class="fas fa-eye me-1"></i>Testar
</button>
}
@ -212,7 +212,7 @@
<i class="fas fa-exclamation-triangle me-3"></i>
<div>
<strong>Página em criação!</strong>
Você pode editar e fazer preview quantas vezes quiser. <br />
Você pode editar e testar quantas vezes quiser. <br />
Ao terminar, clique em <i class="fas fa-ellipsis-v"></i> para enviar a página <b><span id="pageNameDisplay"></span></b> para moderação!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@ -268,7 +268,7 @@
{
<!-- Limite atingido -->
<div class="col-12">
<div class="alert alert-warning d-flex align-items-center">
<div class="alert alert-warning d-flex align-items-center alert-permanent">
<i class="fas fa-exclamation-triangle me-3"></i>
<div>
<strong>Limite atingido!</strong>

View File

@ -340,7 +340,7 @@
var instagram = Model.Links.Where(x => x.Icon.Contains("instagram")).FirstOrDefault();
var facebookUrl = facebook !=null ? facebook.Url : "";
var twitterUrl = twitter !=null ? twitter.Url : "";
var whatsappUrl = whatsapp !=null ? whatsapp.Url : "";
var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","") : "";
var instagramUrl = instagram !=null ? instagram.Url : "";
}
<!-- Passo 4: Redes Sociais (Opcional) -->
@ -1423,6 +1423,12 @@
userInput.on('input', function() {
let value = $(this).val().trim();
// Se o usuário colou uma URL completa, extrair apenas a parte do usuário
if (value.startsWith(prefix)) {
value = value.replace(prefix, '');
$(this).val(value);
}
if (isWhatsApp) {
// WhatsApp: apenas números
value = value.replace(/\D/g, '');

View File

@ -6,6 +6,31 @@
}
},
"DetailedErrors": true,
"Stripe": {
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
"SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO",
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543"
},
"Plans": {
"Basic": {
"PriceId": "price_1RjUskBMIadsOxJVgLwlVo1y",
"Price": 9.90,
"MaxLinks": 5,
"Features": [ "basic_themes", "simple_analytics" ]
},
"Professional": {
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
"Price": 24.90,
"MaxLinks": 15,
"Features": [ "all_themes", "advanced_analytics", "custom_domain" ]
},
"Premium": {
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
"Price": 29.90,
"MaxLinks": -1,
"Features": [ "custom_themes", "full_analytics", "multiple_domains", "priority_support" ]
}
},
"MongoDb": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BCardsDB_Dev"