Compare commits

..

2 Commits

Author SHA1 Message Date
Ricardo Carneiro
0f6ae71997 fix: create plan
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 2s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m28s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m19s
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
2025-09-07 21:39:36 -03:00
Ricardo Carneiro
79c254905a fix: create plan 2025-09-07 21:39:32 -03:00
2 changed files with 381 additions and 316 deletions

View File

@ -24,7 +24,8 @@
"Bash(dotnet nuget locals:*)", "Bash(dotnet nuget locals:*)",
"Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)", "Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)",
"Bash(sed:*)", "Bash(sed:*)",
"Bash(./clean-build.sh:*)" "Bash(./clean-build.sh:*)",
"Bash(git add:*)"
] ]
}, },
"enableAllProjectMcpServers": false "enableAllProjectMcpServers": false

View File

@ -1,316 +1,380 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Stripe; using Stripe;
using BCards.Web.Services; using BCards.Web.Services;
using BCards.Web.Repositories; using BCards.Web.Repositories;
using BCards.Web.Configuration; using BCards.Web.Configuration;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace BCards.Web.Controllers; namespace BCards.Web.Controllers;
[ApiController] [ApiController]
[Route("api/stripe")] [Route("api/stripe")]
public class StripeWebhookController : ControllerBase public class StripeWebhookController : ControllerBase
{ {
private readonly ILogger<StripeWebhookController> _logger; private readonly ILogger<StripeWebhookController> _logger;
private readonly ISubscriptionRepository _subscriptionRepository; private readonly ISubscriptionRepository _subscriptionRepository;
private readonly IUserPageService _userPageService; private readonly IUserPageService _userPageService;
private readonly string _webhookSecret; private readonly string _webhookSecret;
public StripeWebhookController( public StripeWebhookController(
ILogger<StripeWebhookController> logger, ILogger<StripeWebhookController> logger,
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IUserPageService userPageService, IUserPageService userPageService,
IOptions<StripeSettings> stripeSettings) IOptions<StripeSettings> stripeSettings)
{ {
_logger = logger; _logger = logger;
_subscriptionRepository = subscriptionRepository; _subscriptionRepository = subscriptionRepository;
_userPageService = userPageService; _userPageService = userPageService;
_webhookSecret = stripeSettings.Value.WebhookSecret ?? ""; _webhookSecret = stripeSettings.Value.WebhookSecret ?? "";
} }
[HttpPost("webhook")] [HttpPost("webhook")]
public async Task<IActionResult> HandleWebhook() public async Task<IActionResult> HandleWebhook()
{ {
try try
{ {
var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
_logger.LogInformation($"Recebido:{json}"); _logger.LogInformation($"Recebido:{json}");
if (string.IsNullOrEmpty(_webhookSecret)) if (string.IsNullOrEmpty(_webhookSecret))
{ {
_logger.LogWarning("Webhook secret not configured"); _logger.LogWarning("Webhook secret not configured");
return BadRequest("Webhook secret not configured"); return BadRequest("Webhook secret not configured");
} }
_logger.LogWarning($"Recebido:{json}"); _logger.LogWarning($"Recebido:{json}");
var stripeSignature = Request.Headers["Stripe-Signature"].FirstOrDefault(); var stripeSignature = Request.Headers["Stripe-Signature"].FirstOrDefault();
if (string.IsNullOrEmpty(stripeSignature)) if (string.IsNullOrEmpty(stripeSignature))
{ {
_logger.LogWarning("Missing Stripe signature"); _logger.LogWarning("Missing Stripe signature");
return BadRequest("Missing Stripe signature"); return BadRequest("Missing Stripe signature");
} }
_logger.LogInformation($"Contruir evento Stripe: {json}"); _logger.LogInformation($"Contruir evento Stripe: {json}");
var stripeEvent = EventUtility.ConstructEvent( var stripeEvent = EventUtility.ConstructEvent(
json, json,
stripeSignature, stripeSignature,
_webhookSecret, _webhookSecret,
throwOnApiVersionMismatch: false throwOnApiVersionMismatch: false
); );
_logger.LogInformation($"Processing Stripe webhook: {stripeEvent.Type}"); _logger.LogInformation($"Processing Stripe webhook: {stripeEvent.Type}");
switch (stripeEvent.Type) switch (stripeEvent.Type)
{ {
case "invoice.payment_succeeded": case "invoice.payment_succeeded":
await HandlePaymentSucceeded(stripeEvent); await HandlePaymentSucceeded(stripeEvent);
break; break;
case "invoice.payment_failed": case "invoice.payment_failed":
await HandlePaymentFailed(stripeEvent); await HandlePaymentFailed(stripeEvent);
break; break;
case "customer.subscription.deleted": case "customer.subscription.deleted":
await HandleSubscriptionDeleted(stripeEvent); await HandleSubscriptionDeleted(stripeEvent);
break; break;
case "customer.subscription.updated": case "customer.subscription.updated":
await HandleSubscriptionUpdated(stripeEvent); await HandleSubscriptionUpdated(stripeEvent);
break; break;
default: case "customer.subscription.created":
_logger.LogInformation($"Unhandled webhook event type: {stripeEvent.Type}"); await HandleSubscriptionCreated(stripeEvent);
break; break;
}
default:
await Task.Delay(2000); // 2 segundos _logger.LogInformation($"Unhandled webhook event type: {stripeEvent.Type}");
return Ok(); break;
} }
catch (StripeException ex)
{ await Task.Delay(2000); // 2 segundos
await Task.Delay(2000); // 2 segundos return Ok();
_logger.LogError(ex, "Stripe webhook error"); }
return BadRequest($"Stripe error: {ex.Message}"); catch (StripeException ex)
} {
catch (Exception ex) await Task.Delay(2000); // 2 segundos
{ _logger.LogError(ex, "Stripe webhook error");
await Task.Delay(2000); // 2 segundos return BadRequest($"Stripe error: {ex.Message}");
_logger.LogError(ex, "Webhook processing error"); }
return StatusCode(500, "Internal server error"); catch (Exception ex)
} {
} await Task.Delay(2000); // 2 segundos
_logger.LogError(ex, "Webhook processing error");
private async Task HandlePaymentSucceeded(Event stripeEvent) return StatusCode(500, "Internal server error");
{ }
try }
{
if (stripeEvent.Data.Object is Invoice invoice) private async Task HandlePaymentSucceeded(Event stripeEvent)
{ {
_logger.LogInformation($"Payment succeeded for customer: {invoice.CustomerId}"); try
{
var subscriptionId = GetSubscriptionId(stripeEvent); if (stripeEvent.Data.Object is Invoice invoice)
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); {
if (subscription != null) _logger.LogInformation($"Payment succeeded for customer: {invoice.CustomerId}");
{
subscription.Status = "active"; var subscriptionId = GetSubscriptionId(stripeEvent);
subscription.UpdatedAt = DateTime.UtcNow; var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
await _subscriptionRepository.UpdateAsync(subscription); if (subscription != null)
{
// Reactivate user pages subscription.Status = "active";
var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); subscription.UpdatedAt = DateTime.UtcNow;
foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.PendingPayment)) await _subscriptionRepository.UpdateAsync(subscription);
{
page.Status = ViewModels.PageStatus.Active; // Reactivate user pages
page.UpdatedAt = DateTime.UtcNow; var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId);
await _userPageService.UpdatePageAsync(page); foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.PendingPayment))
} {
page.Status = ViewModels.PageStatus.Active;
_logger.LogInformation($"Reactivated {userPages.Count} pages for user {subscription.UserId}"); page.UpdatedAt = DateTime.UtcNow;
} await _userPageService.UpdatePageAsync(page);
} }
else
{ _logger.LogInformation($"Reactivated {userPages.Count} pages for user {subscription.UserId}");
_logger.LogWarning($"Unexpected event type on HandlePaymentSucceeded: {stripeEvent.Type}"); }
} }
} else
catch (Exception ex) {
{ _logger.LogWarning($"Unexpected event type on HandlePaymentSucceeded: {stripeEvent.Type}");
_logger.LogError(ex, "Error handling payment succeeded event"); }
throw new Exception("Error handling payment succeeded event", ex); }
} catch (Exception ex)
} {
_logger.LogError(ex, "Error handling payment succeeded event");
private async Task HandlePaymentFailed(Event stripeEvent) throw new Exception("Error handling payment succeeded event", ex);
{ }
try }
{
if (stripeEvent.Data.Object is Invoice invoice) private async Task HandlePaymentFailed(Event stripeEvent)
{ {
_logger.LogInformation($"Payment failed for customer: {invoice.CustomerId}"); try
{
var subscriptionId = GetSubscriptionId(stripeEvent); if (stripeEvent.Data.Object is Invoice invoice)
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); {
if (subscription != null) _logger.LogInformation($"Payment failed for customer: {invoice.CustomerId}");
{
subscription.Status = "past_due"; var subscriptionId = GetSubscriptionId(stripeEvent);
subscription.UpdatedAt = DateTime.UtcNow; var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
await _subscriptionRepository.UpdateAsync(subscription); if (subscription != null)
{
// Set pages to pending payment subscription.Status = "past_due";
var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); subscription.UpdatedAt = DateTime.UtcNow;
foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.Active)) await _subscriptionRepository.UpdateAsync(subscription);
{
page.Status = ViewModels.PageStatus.PendingPayment; // Set pages to pending payment
page.UpdatedAt = DateTime.UtcNow; var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId);
await _userPageService.UpdatePageAsync(page); foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.Active))
} {
page.Status = ViewModels.PageStatus.PendingPayment;
_logger.LogInformation($"Set {userPages.Count} pages to pending payment for user {subscription.UserId}"); page.UpdatedAt = DateTime.UtcNow;
} await _userPageService.UpdatePageAsync(page);
} }
else
{ _logger.LogInformation($"Set {userPages.Count} pages to pending payment for user {subscription.UserId}");
_logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); }
} }
} else
catch (Exception ex) {
{ _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}");
_logger.LogError(ex, "Error handling payment failed event"); }
throw new Exception("Error handling payment failed event", ex); }
} catch (Exception ex)
} {
_logger.LogError(ex, "Error handling payment failed event");
private async Task HandleSubscriptionDeleted(Event stripeEvent) throw new Exception("Error handling payment failed event", ex);
{ }
try }
{
if (stripeEvent.Data.Object is Subscription stripeSubscription) private async Task HandleSubscriptionDeleted(Event stripeEvent)
{ {
_logger.LogInformation($"Subscription cancelled: {stripeSubscription.Id}"); try
{
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id); if (stripeEvent.Data.Object is Subscription stripeSubscription)
if (subscription != null) {
{ _logger.LogInformation($"Subscription cancelled: {stripeSubscription.Id}");
subscription.Status = "cancelled";
subscription.UpdatedAt = DateTime.UtcNow; var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id);
await _subscriptionRepository.UpdateAsync(subscription); if (subscription != null)
{
// Downgrade to trial or deactivate pages subscription.Status = "cancelled";
var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); subscription.UpdatedAt = DateTime.UtcNow;
foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.Active)) await _subscriptionRepository.UpdateAsync(subscription);
{
page.Status = ViewModels.PageStatus.Expired; // Downgrade to trial or deactivate pages
page.UpdatedAt = DateTime.UtcNow; var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId);
await _userPageService.UpdatePageAsync(page); foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.Active))
} {
page.Status = ViewModels.PageStatus.Expired;
_logger.LogInformation($"Deactivated {userPages.Count} pages for cancelled subscription {subscription.UserId}"); page.UpdatedAt = DateTime.UtcNow;
} await _userPageService.UpdatePageAsync(page);
} }
else
{ _logger.LogInformation($"Deactivated {userPages.Count} pages for cancelled subscription {subscription.UserId}");
_logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); }
} }
} else
catch (Exception ex) {
{ _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}");
_logger.LogError(ex, "Error handling subscription deleted failed event"); }
throw new Exception("Error handling subscription deleted failed event", ex); }
} catch (Exception ex)
} {
_logger.LogError(ex, "Error handling subscription deleted failed event");
private async Task HandleSubscriptionUpdated(Event stripeEvent) throw new Exception("Error handling subscription deleted failed event", ex);
{ }
try }
{
if (stripeEvent.Data.Object is Subscription stripeSubscription) private async Task HandleSubscriptionUpdated(Event stripeEvent)
{ {
_logger.LogInformation($"Subscription updated: {stripeSubscription.Id}"); try
{
var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id); if (stripeEvent.Data.Object is Subscription stripeSubscription)
if (subscription != null) {
{ _logger.LogInformation($"Subscription updated: {stripeSubscription.Id}");
var service = new SubscriptionItemService();
var subItem = service.Get(stripeSubscription.Items.Data[0].Id); var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id);
if (subscription != null)
subscription.Status = stripeSubscription.Status; {
subscription.CurrentPeriodStart = subItem.CurrentPeriodStart; var service = new SubscriptionItemService();
subscription.CurrentPeriodEnd = subItem.CurrentPeriodEnd; var subItem = service.Get(stripeSubscription.Items.Data[0].Id);
subscription.CancelAtPeriodEnd = stripeSubscription.CancelAtPeriodEnd;
subscription.UpdatedAt = DateTime.UtcNow; subscription.Status = stripeSubscription.Status;
subscription.CurrentPeriodStart = subItem.CurrentPeriodStart;
// Update plan type based on Stripe price ID subscription.CurrentPeriodEnd = subItem.CurrentPeriodEnd;
var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id; subscription.CancelAtPeriodEnd = stripeSubscription.CancelAtPeriodEnd;
if (!string.IsNullOrEmpty(priceId)) subscription.UpdatedAt = DateTime.UtcNow;
{
subscription.PlanType = MapPriceIdToPlanType(priceId); // Update plan type based on Stripe price ID
} var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id;
if (!string.IsNullOrEmpty(priceId))
await _subscriptionRepository.UpdateAsync(subscription); {
subscription.PlanType = MapPriceIdToPlanType(priceId);
_logger.LogInformation($"Updated subscription for user {subscription.UserId}"); }
}
} await _subscriptionRepository.UpdateAsync(subscription);
else
{ _logger.LogInformation($"Updated subscription for user {subscription.UserId}");
_logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); }
} }
} else
catch (Exception ex) {
{ _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}");
_logger.LogError(ex, "Error handling subscription updated failed event"); }
throw new Exception("Error handling subscription updated failed event", ex); }
} catch (Exception ex)
} {
_logger.LogError(ex, "Error handling subscription updated failed event");
private string MapPriceIdToPlanType(string priceId) throw new Exception("Error handling subscription updated failed event", ex);
{ }
// Map Stripe price IDs to plan types }
// This would be configured based on your actual Stripe price IDs
return priceId switch private async Task HandleSubscriptionCreated(Event stripeEvent)
{ {
"price_1RjUskBMIadsOxJVgLwlVo1y" => "Basic", try
"price_1RjUv9BMIadsOxJVORqlM4E9" => "Professional", {
"price_1RjUw0BMIadsOxJVmdouNV1g" => "Premium", if (stripeEvent.Data.Object is Subscription stripeSubscription)
"price_basic_yearly_placeholder" => "BasicYearly", {
"price_professional_yearly_placeholder" => "ProfessionalYearly", _logger.LogInformation($"Subscription created: {stripeSubscription.Id} for customer: {stripeSubscription.CustomerId}");
"price_premium_yearly_placeholder" => "PremiumYearly",
var id when id.Contains("basic") && id.Contains("yearly") => "BasicYearly", // Get subscription record from our database
var id when id.Contains("professional") && id.Contains("yearly") => "ProfessionalYearly", var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id);
var id when id.Contains("premium") && id.Contains("yearly") => "PremiumYearly", if (subscription != null)
var id when id.Contains("basic") => "Basic", {
var id when id.Contains("professional") => "Professional", var service = new SubscriptionItemService();
var id when id.Contains("premium") => "Premium", var subItem = service.Get(stripeSubscription.Items.Data[0].Id);
_ => "Trial"
}; // Update subscription status to active
} subscription.Status = "active";
subscription.UpdatedAt = DateTime.UtcNow;
private string GetSubscriptionId(Event stripeEvent) subscription.CurrentPeriodStart = subItem.CurrentPeriodStart;
{ subscription.CurrentPeriodEnd = subItem.CurrentPeriodEnd;
if (stripeEvent.Data.Object is Invoice invoice)
{ // Update plan type based on Stripe price ID
var subscriptionLineItem = invoice.Lines?.Data var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id;
.FirstOrDefault(line => if (!string.IsNullOrEmpty(priceId))
!string.IsNullOrEmpty(line.SubscriptionId) || {
line.Subscription != null subscription.PlanType = MapPriceIdToPlanType(priceId);
); }
string subscriptionId = null; await _subscriptionRepository.UpdateAsync(subscription);
if (subscriptionLineItem != null) // Activate user pages that were pending payment or trial
{ var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId);
// Tenta obter o ID da assinatura de duas formas diferentes foreach (var page in userPages.Where(p =>
subscriptionId = subscriptionLineItem.SubscriptionId p.Status == ViewModels.PageStatus.PendingPayment ||
?? subscriptionLineItem.Subscription?.Id; p.Status == ViewModels.PageStatus.Expired))
} {
page.Status = ViewModels.PageStatus.Active;
return subscriptionId; page.UpdatedAt = DateTime.UtcNow;
} await _userPageService.UpdatePageAsync(page);
else if (stripeEvent.Data.Object is Subscription stripeSubscription) }
{
return stripeSubscription.Id; _logger.LogInformation($"Activated subscription and {userPages.Count()} pages for user {subscription.UserId}");
} }
else
return null; {
} _logger.LogWarning($"Subscription not found in database: {stripeSubscription.Id}");
}
}
else
{
_logger.LogWarning($"Unexpected event type on HandleSubscriptionCreated: {stripeEvent.Type}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling subscription created event");
throw new Exception("Error handling subscription created event", ex);
}
}
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)
{
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;
}
} }