using BCards.Web.Configuration; using BCards.Web.Models; using BCards.Web.Repositories; using Microsoft.Extensions.Options; using Stripe; using Stripe.Checkout; using Stripe.BillingPortal; using System.Numerics; namespace BCards.Web.Services; public class PaymentService : IPaymentService { private readonly StripeSettings _stripeSettings; private readonly IUserRepository _userRepository; private readonly ISubscriptionRepository _subscriptionRepository; private readonly IConfiguration _configuration; private readonly IPlanConfigurationService _planConfigurationService; public PaymentService( IOptions stripeSettings, IUserRepository userRepository, ISubscriptionRepository subscriptionRepository, IConfiguration configuration, IPlanConfigurationService planConfigurationService) { _stripeSettings = stripeSettings.Value; _userRepository = userRepository; _subscriptionRepository = subscriptionRepository; _configuration = configuration; _planConfigurationService = planConfigurationService; StripeConfiguration.ApiKey = _stripeSettings.SecretKey; } public async Task CreateCheckoutSessionAsync(string userId, string planType, string returnUrl, string cancelUrl) { var user = await _userRepository.GetByIdAsync(userId); if (user == null) throw new InvalidOperationException("User not found"); var planConfig = _configuration.GetSection($"Plans:{planType}"); var priceId = planConfig["PriceId"]; if (string.IsNullOrEmpty(priceId)) throw new InvalidOperationException($"Price ID not found for plan: {planType}"); var customer = await CreateOrGetCustomerAsync(userId, user.Email, user.Name); var options = new Stripe.Checkout.SessionCreateOptions { PaymentMethodTypes = new List { "card" }, Mode = "subscription", Customer = customer.Id, LineItems = new List { new() { Price = priceId, Quantity = 1 } }, SuccessUrl = returnUrl, CancelUrl = cancelUrl, Metadata = new Dictionary { { "user_id", userId }, { "plan_type", planType } } }; var service = new Stripe.Checkout.SessionService(); var session = await service.CreateAsync(options); return session.Url; } public async Task CreateOrGetCustomerAsync(string userId, string email, string name) { var user = await _userRepository.GetByIdAsync(userId); if (!string.IsNullOrEmpty(user?.StripeCustomerId)) { var customerService = new CustomerService(); try { return await customerService.GetAsync(user.StripeCustomerId); } catch (StripeException) { // Customer doesn't exist, create new one } } // Create new customer var options = new CustomerCreateOptions { Email = email, Name = name, Metadata = new Dictionary { { "user_id", userId } } }; var service = new CustomerService(); var customer = await service.CreateAsync(options); // Update user with customer ID if (user != null) { user.StripeCustomerId = customer.Id; await _userRepository.UpdateAsync(user); } return customer; } public async Task HandleWebhookAsync(string requestBody, string signature) { try { var stripeEvent = EventUtility.ConstructEvent(requestBody, signature, _stripeSettings.WebhookSecret, throwOnApiVersionMismatch: false); switch (stripeEvent.Type) { case "checkout.session.completed": var session = stripeEvent.Data.Object as Stripe.Checkout.Session; await HandleCheckoutSessionCompletedAsync(session!); break; case "invoice.finalized": var invoice = stripeEvent.Data.Object as Invoice; await HandleInvoicePaymentSucceededAsync(invoice!); break; case "customer.subscription.updated": case "customer.subscription.deleted": var subscription = stripeEvent.Data.Object as Stripe.Subscription; await HandleSubscriptionUpdatedAsync(subscription!); break; } return stripeEvent.Data.Object as Stripe.Subscription ?? new Stripe.Subscription(); } catch (StripeException ex) { throw new InvalidOperationException($"Webhook signature verification failed: {ex.Message}"); } } public async Task> GetPricesAsync() { var service = new PriceService(); var options = new PriceListOptions { Active = true, Type = "recurring" }; var prices = await service.ListAsync(options); return prices.Data; } public async Task CancelSubscriptionAsync(string subscriptionId) { var service = new SubscriptionService(); var options = new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }; var subscription = await service.UpdateAsync(subscriptionId, options); // Update local subscription var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); if (localSubscription != null) { localSubscription.CancelAtPeriodEnd = true; await _subscriptionRepository.UpdateAsync(localSubscription); } return subscription.CancelAtPeriodEnd; } public async Task UpdateSubscriptionAsync(string subscriptionId, string newPriceId) { var service = new SubscriptionService(); var subscription = await service.GetAsync(subscriptionId); var options = new SubscriptionUpdateOptions { Items = new List { new() { Id = subscription.Items.Data[0].Id, Price = newPriceId } } }; return await service.UpdateAsync(subscriptionId, options); } public Task GetPlanLimitationsAsync(string planType) { // Mapear string planType para PlanType enum var planTypeEnum = planType.ToLower() switch { "basic" or "basicyearly" => PlanType.Basic, "professional" or "professionalyearly" => PlanType.Professional, "premium" or "premiumyearly" => PlanType.Premium, "premiumaffiliate" or "premiumaffiliateyearly" => PlanType.PremiumAffiliate, _ => PlanType.Trial }; var limitations = _planConfigurationService.GetPlanLimitations(planTypeEnum); return Task.FromResult(limitations); } private async Task HandleCheckoutSessionCompletedAsync(Stripe.Checkout.Session session) { var userId = session.Metadata["user_id"]; var planType = session.Metadata["plan_type"]; 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 { UserId = userId, StripeSubscriptionId = session.SubscriptionId, PlanType = planType, Status = stripeSubscription.Status, CurrentPeriodStart = subItem.CurrentPeriodStart, CurrentPeriodEnd = subItem.CurrentPeriodEnd, MaxLinks = limitations.MaxLinks, AllowCustomThemes = limitations.AllowCustomThemes, AllowAnalytics = limitations.AllowAnalytics, AllowCustomDomain = limitations.AllowCustomDomain, AllowMultipleDomains = limitations.AllowMultipleDomains, PrioritySupport = limitations.PrioritySupport }; await _subscriptionRepository.CreateAsync(subscription); // Update user var user = await _userRepository.GetByIdAsync(userId); if (user != null) { user.CurrentPlan = planType; user.SubscriptionStatus = "active"; await _userRepository.UpdateAsync(user); } } private async Task HandleInvoicePaymentSucceededAsync(Invoice invoice) { var subscriptionId = GetSubscriptionId(invoice); var subscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); if (subscription != null) { subscription.Status = "active"; await _subscriptionRepository.UpdateAsync(subscription); } } private async Task HandleSubscriptionUpdatedAsync(Stripe.Subscription stripeSubscription) { 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; await _subscriptionRepository.UpdateAsync(subscription); // Update user status var user = await _userRepository.GetByIdAsync(subscription.UserId); if (user != null) { user.SubscriptionStatus = stripeSubscription.Status; if (stripeSubscription.Status != "active") { // Quando assinatura não está ativa, usuário volta para o plano trial (gratuito) user.CurrentPlan = "trial"; } await _userRepository.UpdateAsync(user); } } } public async Task GetSubscriptionDetailsAsync(string userId) { var user = await _userRepository.GetByIdAsync(userId); if (user == null || string.IsNullOrEmpty(user.StripeCustomerId)) return null; var subscription = await _subscriptionRepository.GetByUserIdAsync(userId); if (subscription == null || string.IsNullOrEmpty(subscription.StripeSubscriptionId)) return null; try { var service = new SubscriptionService(); return await service.GetAsync(subscription.StripeSubscriptionId); } catch (StripeException) { return null; } } public async Task> GetPaymentHistoryAsync(string userId) { var user = await _userRepository.GetByIdAsync(userId); if (user == null || string.IsNullOrEmpty(user.StripeCustomerId)) return new List(); try { var service = new InvoiceService(); var options = new InvoiceListOptions { Customer = user.StripeCustomerId, Limit = 50, // Últimas 50 faturas Status = "paid" }; var invoices = await service.ListAsync(options); return invoices.Data; } catch (StripeException) { return new List(); } } public async Task CreatePortalSessionAsync(string customerId, string returnUrl) { try { var options = new Stripe.BillingPortal.SessionCreateOptions { Customer = customerId, ReturnUrl = returnUrl }; var service = new Stripe.BillingPortal.SessionService(); var session = await service.CreateAsync(options); return session.Url; } catch (StripeException ex) { throw new InvalidOperationException($"Erro ao criar sessão do portal: {ex.Message}"); } } // Métodos de cancelamento com diferentes políticas public async Task CancelSubscriptionImmediatelyAsync(string subscriptionId, bool refund = false) { try { var service = new SubscriptionService(); if (refund) { // Para reembolso completo, apenas cancela - reembolso deve ser feito manualmente via Stripe Dashboard await service.CancelAsync(subscriptionId); } else { await service.CancelAsync(subscriptionId); } // Atualizar subscription local var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); if (localSubscription != null) { localSubscription.Status = "cancelled"; localSubscription.UpdatedAt = DateTime.UtcNow; await _subscriptionRepository.UpdateAsync(localSubscription); } return true; } catch (StripeException) { return false; } } public async Task CancelSubscriptionAtPeriodEndAsync(string subscriptionId) { try { var service = new SubscriptionService(); var options = new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }; await service.UpdateAsync(subscriptionId, options); // Atualizar subscription local var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); if (localSubscription != null) { localSubscription.CancelAtPeriodEnd = true; localSubscription.UpdatedAt = DateTime.UtcNow; await _subscriptionRepository.UpdateAsync(localSubscription); } return true; } catch (StripeException) { return false; } } public async Task ReactivateSubscriptionAsync(string subscriptionId) { try { var service = new SubscriptionService(); var options = new SubscriptionUpdateOptions { CancelAtPeriodEnd = false // Reativar removendo o agendamento de cancelamento }; await service.UpdateAsync(subscriptionId, options); // Atualizar subscription local var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId); if (localSubscription != null) { localSubscription.CancelAtPeriodEnd = false; localSubscription.UpdatedAt = DateTime.UtcNow; await _subscriptionRepository.UpdateAsync(localSubscription); } return true; } catch (StripeException) { return false; } } public async Task CreatePartialRefundAsync(string subscriptionId, decimal refundAmount) { // NOTA: Para planos de assinatura, reembolsos devem ser processados manualmente via Stripe Dashboard // Este método retorna null para indicar que o reembolso precisa ser feito manualmente await Task.CompletedTask; throw new InvalidOperationException("Reembolsos parciais para assinaturas devem ser processados manualmente via Stripe Dashboard ou Portal de Cobrança"); } public async Task<(bool CanRefundFull, bool CanRefundPartial, decimal RefundAmount)> CalculateRefundAsync(string subscriptionId) { try { var service = new SubscriptionService(); var subscription = await service.GetAsync(subscriptionId); // Obter datas via SubscriptionItem var subscriptionItemService = new SubscriptionItemService(); var subItem = await subscriptionItemService.GetAsync(subscription.Items.Data[0].Id); var daysSinceStart = (DateTime.UtcNow - subItem.CurrentPeriodStart).TotalDays; var totalDays = (subItem.CurrentPeriodEnd - subItem.CurrentPeriodStart).TotalDays; var daysRemaining = totalDays - daysSinceStart; // Direito de arrependimento (7 dias) bool canRefundFull = daysSinceStart <= 7; // Reembolso proporcional para planos anuais bool canRefundPartial = false; decimal refundAmount = 0; if (!canRefundFull && daysRemaining > 0) { // Verificar se é plano anual (pela duração do período) bool isYearlyPlan = totalDays > 300; // Aproximadamente 1 ano if (isYearlyPlan) { canRefundPartial = true; // Buscar valor pago na última fatura var invoiceService = new InvoiceService(); var invoices = await invoiceService.ListAsync(new InvoiceListOptions { Subscription = subscriptionId, Status = "paid", Limit = 1 }); if (invoices.Data.Any()) { var latestInvoice = invoices.Data.First(); var totalPaid = (decimal)latestInvoice.AmountPaid / 100; // Converter de centavos // Calcular proporção do tempo restante var proportionRemaining = daysRemaining / totalDays; refundAmount = totalPaid * (decimal)proportionRemaining; refundAmount = Math.Round(refundAmount, 2); } } } return (canRefundFull, canRefundPartial, refundAmount); } catch (StripeException) { return (false, false, 0); } } private async Task CreateFullRefundAsync(string? chargeId) { // NOTA: Para planos de assinatura, reembolsos devem ser processados manualmente await Task.CompletedTask; return null; } 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; } }