From 79c254905a2ad2bda55d98ea7e00bdba8959a3a5 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sun, 7 Sep 2025 21:39:32 -0300 Subject: [PATCH] fix: create plan --- .../Controllers/StripeWebhookController.cs | 691 ++++++++++-------- 1 file changed, 376 insertions(+), 315 deletions(-) diff --git a/src/BCards.Web/Controllers/StripeWebhookController.cs b/src/BCards.Web/Controllers/StripeWebhookController.cs index 24addff..4d4ad26 100644 --- a/src/BCards.Web/Controllers/StripeWebhookController.cs +++ b/src/BCards.Web/Controllers/StripeWebhookController.cs @@ -1,316 +1,377 @@ -using Microsoft.AspNetCore.Mvc; -using Stripe; -using BCards.Web.Services; -using BCards.Web.Repositories; -using BCards.Web.Configuration; -using Microsoft.Extensions.Options; - -namespace BCards.Web.Controllers; - -[ApiController] -[Route("api/stripe")] -public class StripeWebhookController : ControllerBase -{ - private readonly ILogger _logger; - private readonly ISubscriptionRepository _subscriptionRepository; - private readonly IUserPageService _userPageService; - private readonly string _webhookSecret; - - public StripeWebhookController( - ILogger logger, - ISubscriptionRepository subscriptionRepository, - IUserPageService userPageService, - IOptions stripeSettings) - { - _logger = logger; - _subscriptionRepository = subscriptionRepository; - _userPageService = userPageService; - _webhookSecret = stripeSettings.Value.WebhookSecret ?? ""; - } - - [HttpPost("webhook")] - public async Task HandleWebhook() - { - try - { - var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); - _logger.LogInformation($"Recebido:{json}"); - - if (string.IsNullOrEmpty(_webhookSecret)) - { - _logger.LogWarning("Webhook secret not configured"); - return BadRequest("Webhook secret not configured"); - } - _logger.LogWarning($"Recebido:{json}"); - - var stripeSignature = Request.Headers["Stripe-Signature"].FirstOrDefault(); - if (string.IsNullOrEmpty(stripeSignature)) - { - _logger.LogWarning("Missing Stripe signature"); - return BadRequest("Missing Stripe signature"); - } - - _logger.LogInformation($"Contruir evento Stripe: {json}"); - var stripeEvent = EventUtility.ConstructEvent( - json, - stripeSignature, - _webhookSecret, - throwOnApiVersionMismatch: false - ); - - _logger.LogInformation($"Processing Stripe webhook: {stripeEvent.Type}"); - - switch (stripeEvent.Type) - { - case "invoice.payment_succeeded": - await HandlePaymentSucceeded(stripeEvent); - break; - - case "invoice.payment_failed": - await HandlePaymentFailed(stripeEvent); - break; - - case "customer.subscription.deleted": - await HandleSubscriptionDeleted(stripeEvent); - break; - - case "customer.subscription.updated": - await HandleSubscriptionUpdated(stripeEvent); - break; - - default: - _logger.LogInformation($"Unhandled webhook event type: {stripeEvent.Type}"); - break; - } - - await Task.Delay(2000); // 2 segundos - return Ok(); - } - catch (StripeException ex) - { - await Task.Delay(2000); // 2 segundos - _logger.LogError(ex, "Stripe webhook error"); - return BadRequest($"Stripe error: {ex.Message}"); - } - catch (Exception ex) - { - await Task.Delay(2000); // 2 segundos - _logger.LogError(ex, "Webhook processing error"); - return StatusCode(500, "Internal server error"); - } - } - - private async Task HandlePaymentSucceeded(Event stripeEvent) - { - try - { - if (stripeEvent.Data.Object is Invoice invoice) - { - _logger.LogInformation($"Payment succeeded for customer: {invoice.CustomerId}"); - - var subscriptionId = GetSubscriptionId(stripeEvent); - var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); - if (subscription != null) - { - subscription.Status = "active"; - subscription.UpdatedAt = DateTime.UtcNow; - await _subscriptionRepository.UpdateAsync(subscription); - - // Reactivate user pages - var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); - foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.PendingPayment)) - { - page.Status = ViewModels.PageStatus.Active; - page.UpdatedAt = DateTime.UtcNow; - await _userPageService.UpdatePageAsync(page); - } - - _logger.LogInformation($"Reactivated {userPages.Count} pages for user {subscription.UserId}"); - } - } - else - { - _logger.LogWarning($"Unexpected event type on HandlePaymentSucceeded: {stripeEvent.Type}"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error handling payment succeeded event"); - throw new Exception("Error handling payment succeeded event", ex); - } - } - - private async Task HandlePaymentFailed(Event stripeEvent) - { - try - { - if (stripeEvent.Data.Object is Invoice invoice) - { - _logger.LogInformation($"Payment failed for customer: {invoice.CustomerId}"); - - var subscriptionId = GetSubscriptionId(stripeEvent); - var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); - if (subscription != null) - { - subscription.Status = "past_due"; - subscription.UpdatedAt = DateTime.UtcNow; - await _subscriptionRepository.UpdateAsync(subscription); - - // Set pages to pending payment - var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); - foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.Active)) - { - page.Status = ViewModels.PageStatus.PendingPayment; - page.UpdatedAt = DateTime.UtcNow; - await _userPageService.UpdatePageAsync(page); - } - - _logger.LogInformation($"Set {userPages.Count} pages to pending payment for user {subscription.UserId}"); - } - } - else - { - _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error handling payment failed event"); - throw new Exception("Error handling payment failed event", ex); - } - } - - private async Task HandleSubscriptionDeleted(Event stripeEvent) - { - try - { - if (stripeEvent.Data.Object is Subscription stripeSubscription) - { - _logger.LogInformation($"Subscription cancelled: {stripeSubscription.Id}"); - - var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id); - if (subscription != null) - { - subscription.Status = "cancelled"; - subscription.UpdatedAt = DateTime.UtcNow; - await _subscriptionRepository.UpdateAsync(subscription); - - // Downgrade to trial or deactivate pages - var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); - foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.Active)) - { - page.Status = ViewModels.PageStatus.Expired; - page.UpdatedAt = DateTime.UtcNow; - await _userPageService.UpdatePageAsync(page); - } - - _logger.LogInformation($"Deactivated {userPages.Count} pages for cancelled subscription {subscription.UserId}"); - } - } - else - { - _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error handling subscription deleted failed event"); - throw new Exception("Error handling subscription deleted failed event", ex); - } - } - - private async Task HandleSubscriptionUpdated(Event stripeEvent) - { - try - { - if (stripeEvent.Data.Object is Subscription stripeSubscription) - { - _logger.LogInformation($"Subscription updated: {stripeSubscription.Id}"); - - 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 = subItem.CurrentPeriodStart; - subscription.CurrentPeriodEnd = subItem.CurrentPeriodEnd; - subscription.CancelAtPeriodEnd = stripeSubscription.CancelAtPeriodEnd; - subscription.UpdatedAt = DateTime.UtcNow; - - // Update plan type based on Stripe price ID - var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id; - if (!string.IsNullOrEmpty(priceId)) - { - subscription.PlanType = MapPriceIdToPlanType(priceId); - } - - await _subscriptionRepository.UpdateAsync(subscription); - - _logger.LogInformation($"Updated subscription for user {subscription.UserId}"); - } - } - else - { - _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error handling subscription updated failed event"); - throw new Exception("Error handling subscription updated failed 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; - } +using Microsoft.AspNetCore.Mvc; +using Stripe; +using BCards.Web.Services; +using BCards.Web.Repositories; +using BCards.Web.Configuration; +using Microsoft.Extensions.Options; + +namespace BCards.Web.Controllers; + +[ApiController] +[Route("api/stripe")] +public class StripeWebhookController : ControllerBase +{ + private readonly ILogger _logger; + private readonly ISubscriptionRepository _subscriptionRepository; + private readonly IUserPageService _userPageService; + private readonly string _webhookSecret; + + public StripeWebhookController( + ILogger logger, + ISubscriptionRepository subscriptionRepository, + IUserPageService userPageService, + IOptions stripeSettings) + { + _logger = logger; + _subscriptionRepository = subscriptionRepository; + _userPageService = userPageService; + _webhookSecret = stripeSettings.Value.WebhookSecret ?? ""; + } + + [HttpPost("webhook")] + public async Task HandleWebhook() + { + try + { + var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); + _logger.LogInformation($"Recebido:{json}"); + + if (string.IsNullOrEmpty(_webhookSecret)) + { + _logger.LogWarning("Webhook secret not configured"); + return BadRequest("Webhook secret not configured"); + } + _logger.LogWarning($"Recebido:{json}"); + + var stripeSignature = Request.Headers["Stripe-Signature"].FirstOrDefault(); + if (string.IsNullOrEmpty(stripeSignature)) + { + _logger.LogWarning("Missing Stripe signature"); + return BadRequest("Missing Stripe signature"); + } + + _logger.LogInformation($"Contruir evento Stripe: {json}"); + var stripeEvent = EventUtility.ConstructEvent( + json, + stripeSignature, + _webhookSecret, + throwOnApiVersionMismatch: false + ); + + _logger.LogInformation($"Processing Stripe webhook: {stripeEvent.Type}"); + + switch (stripeEvent.Type) + { + case "invoice.payment_succeeded": + await HandlePaymentSucceeded(stripeEvent); + break; + + case "invoice.payment_failed": + await HandlePaymentFailed(stripeEvent); + break; + + case "customer.subscription.deleted": + await HandleSubscriptionDeleted(stripeEvent); + break; + + case "customer.subscription.updated": + await HandleSubscriptionUpdated(stripeEvent); + break; + + case "customer.subscription.created": + await HandleSubscriptionCreated(stripeEvent); + break; + + default: + _logger.LogInformation($"Unhandled webhook event type: {stripeEvent.Type}"); + break; + } + + await Task.Delay(2000); // 2 segundos + return Ok(); + } + catch (StripeException ex) + { + await Task.Delay(2000); // 2 segundos + _logger.LogError(ex, "Stripe webhook error"); + return BadRequest($"Stripe error: {ex.Message}"); + } + catch (Exception ex) + { + await Task.Delay(2000); // 2 segundos + _logger.LogError(ex, "Webhook processing error"); + return StatusCode(500, "Internal server error"); + } + } + + private async Task HandlePaymentSucceeded(Event stripeEvent) + { + try + { + if (stripeEvent.Data.Object is Invoice invoice) + { + _logger.LogInformation($"Payment succeeded for customer: {invoice.CustomerId}"); + + var subscriptionId = GetSubscriptionId(stripeEvent); + var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); + if (subscription != null) + { + subscription.Status = "active"; + subscription.UpdatedAt = DateTime.UtcNow; + await _subscriptionRepository.UpdateAsync(subscription); + + // Reactivate user pages + var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); + foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.PendingPayment)) + { + page.Status = ViewModels.PageStatus.Active; + page.UpdatedAt = DateTime.UtcNow; + await _userPageService.UpdatePageAsync(page); + } + + _logger.LogInformation($"Reactivated {userPages.Count} pages for user {subscription.UserId}"); + } + } + else + { + _logger.LogWarning($"Unexpected event type on HandlePaymentSucceeded: {stripeEvent.Type}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling payment succeeded event"); + throw new Exception("Error handling payment succeeded event", ex); + } + } + + private async Task HandlePaymentFailed(Event stripeEvent) + { + try + { + if (stripeEvent.Data.Object is Invoice invoice) + { + _logger.LogInformation($"Payment failed for customer: {invoice.CustomerId}"); + + var subscriptionId = GetSubscriptionId(stripeEvent); + var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); + if (subscription != null) + { + subscription.Status = "past_due"; + subscription.UpdatedAt = DateTime.UtcNow; + await _subscriptionRepository.UpdateAsync(subscription); + + // Set pages to pending payment + var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); + foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.Active)) + { + page.Status = ViewModels.PageStatus.PendingPayment; + page.UpdatedAt = DateTime.UtcNow; + await _userPageService.UpdatePageAsync(page); + } + + _logger.LogInformation($"Set {userPages.Count} pages to pending payment for user {subscription.UserId}"); + } + } + else + { + _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling payment failed event"); + throw new Exception("Error handling payment failed event", ex); + } + } + + private async Task HandleSubscriptionDeleted(Event stripeEvent) + { + try + { + if (stripeEvent.Data.Object is Subscription stripeSubscription) + { + _logger.LogInformation($"Subscription cancelled: {stripeSubscription.Id}"); + + var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id); + if (subscription != null) + { + subscription.Status = "cancelled"; + subscription.UpdatedAt = DateTime.UtcNow; + await _subscriptionRepository.UpdateAsync(subscription); + + // Downgrade to trial or deactivate pages + var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); + foreach (var page in userPages.Where(p => p.Status == ViewModels.PageStatus.Active)) + { + page.Status = ViewModels.PageStatus.Expired; + page.UpdatedAt = DateTime.UtcNow; + await _userPageService.UpdatePageAsync(page); + } + + _logger.LogInformation($"Deactivated {userPages.Count} pages for cancelled subscription {subscription.UserId}"); + } + } + else + { + _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling subscription deleted failed event"); + throw new Exception("Error handling subscription deleted failed event", ex); + } + } + + private async Task HandleSubscriptionUpdated(Event stripeEvent) + { + try + { + if (stripeEvent.Data.Object is Subscription stripeSubscription) + { + _logger.LogInformation($"Subscription updated: {stripeSubscription.Id}"); + + 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 = subItem.CurrentPeriodStart; + subscription.CurrentPeriodEnd = subItem.CurrentPeriodEnd; + subscription.CancelAtPeriodEnd = stripeSubscription.CancelAtPeriodEnd; + subscription.UpdatedAt = DateTime.UtcNow; + + // Update plan type based on Stripe price ID + var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id; + if (!string.IsNullOrEmpty(priceId)) + { + subscription.PlanType = MapPriceIdToPlanType(priceId); + } + + await _subscriptionRepository.UpdateAsync(subscription); + + _logger.LogInformation($"Updated subscription for user {subscription.UserId}"); + } + } + else + { + _logger.LogWarning($"Unexpected event type on HandlePaymentFailed: {stripeEvent.Type}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling subscription updated failed event"); + throw new Exception("Error handling subscription updated failed event", ex); + } + } + + private async Task HandleSubscriptionCreated(Event stripeEvent) + { + try + { + if (stripeEvent.Data.Object is Subscription stripeSubscription) + { + _logger.LogInformation($"Subscription created: {stripeSubscription.Id} for customer: {stripeSubscription.CustomerId}"); + + // Get subscription record from our database + var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(stripeSubscription.Id); + if (subscription != null) + { + // Update subscription status to active + subscription.Status = "active"; + subscription.UpdatedAt = DateTime.UtcNow; + subscription.CurrentPeriodStart = stripeSubscription.CurrentPeriodStart; + subscription.CurrentPeriodEnd = stripeSubscription.CurrentPeriodEnd; + + // Update plan type based on Stripe price ID + var priceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id; + if (!string.IsNullOrEmpty(priceId)) + { + subscription.PlanType = MapPriceIdToPlanType(priceId); + } + + await _subscriptionRepository.UpdateAsync(subscription); + + // Activate user pages that were pending payment or trial + var userPages = await _userPageService.GetUserPagesAsync(subscription.UserId); + foreach (var page in userPages.Where(p => + p.Status == ViewModels.PageStatus.PendingPayment || + p.Status == ViewModels.PageStatus.Expired)) + { + page.Status = ViewModels.PageStatus.Active; + page.UpdatedAt = DateTime.UtcNow; + await _userPageService.UpdatePageAsync(page); + } + + _logger.LogInformation($"Activated subscription and {userPages.Count()} pages for user {subscription.UserId}"); + } + else + { + _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; + } } \ No newline at end of file