358 lines
16 KiB
C#
358 lines
16 KiB
C#
|
|
using Stripe;
|
|
using Stripe.Checkout;
|
|
using QRRapidoApp.Models;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Collections.Generic;
|
|
using System;
|
|
|
|
namespace QRRapidoApp.Services
|
|
{
|
|
public class StripeService
|
|
{
|
|
private readonly IConfiguration _config;
|
|
private readonly IUserService _userService;
|
|
private readonly ILogger<StripeService> _logger;
|
|
|
|
public StripeService(IConfiguration config, IUserService userService, ILogger<StripeService> logger)
|
|
{
|
|
_config = config;
|
|
_userService = userService;
|
|
_logger = logger;
|
|
StripeConfiguration.ApiKey = _config["Stripe:SecretKey"];
|
|
}
|
|
|
|
public async Task<string> CreateCheckoutSessionAsync(string userId, string priceId, string lang = "pt-BR")
|
|
{
|
|
var user = await _userService.GetUserAsync(userId);
|
|
if (user == null)
|
|
{
|
|
throw new Exception("User not found");
|
|
}
|
|
|
|
var customerId = user.StripeCustomerId;
|
|
var customerService = new CustomerService();
|
|
|
|
// Verify if customer exists in Stripe, create new if not
|
|
if (!string.IsNullOrEmpty(customerId))
|
|
{
|
|
try
|
|
{
|
|
// Try to retrieve the customer to verify it exists
|
|
await customerService.GetAsync(customerId);
|
|
_logger.LogInformation($"Using existing Stripe customer {customerId} for user {userId}");
|
|
}
|
|
catch (StripeException ex) when (ex.StripeError?.Code == "resource_missing")
|
|
{
|
|
_logger.LogWarning($"Stripe customer {customerId} not found, creating new one for user {userId}");
|
|
customerId = null; // Force creation of new customer
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(customerId))
|
|
{
|
|
var customerOptions = new CustomerCreateOptions
|
|
{
|
|
Email = user.Email,
|
|
Name = user.Name,
|
|
Metadata = new Dictionary<string, string> { { "app_user_id", user.Id } }
|
|
};
|
|
var customer = await customerService.CreateAsync(customerOptions);
|
|
customerId = customer.Id;
|
|
await _userService.UpdateUserStripeCustomerIdAsync(userId, customerId);
|
|
_logger.LogInformation($"Created new Stripe customer {customerId} for user {userId}");
|
|
}
|
|
|
|
var options = new SessionCreateOptions
|
|
{
|
|
PaymentMethodTypes = new List<string> { "card" },
|
|
Mode = "subscription",
|
|
LineItems = new List<SessionLineItemOptions>
|
|
{
|
|
new SessionLineItemOptions { Price = priceId, Quantity = 1 }
|
|
},
|
|
Customer = customerId,
|
|
ClientReferenceId = userId,
|
|
SuccessUrl = $"{_config["App:BaseUrl"]}/Pagamento/Sucesso",
|
|
CancelUrl = $"{_config["App:BaseUrl"]}/{lang}/Pagamento/SelecaoPlano",
|
|
AllowPromotionCodes = true,
|
|
Metadata = new Dictionary<string, string> { { "user_id", userId } }
|
|
};
|
|
|
|
var service = new SessionService();
|
|
var session = await service.CreateAsync(options);
|
|
_logger.LogInformation($"Created Stripe checkout session {session.Id} for user {userId}");
|
|
return session.Url;
|
|
}
|
|
|
|
public async Task HandleWebhookAsync(string json, string signature)
|
|
{
|
|
var webhookSecret = _config["Stripe:WebhookSecret"];
|
|
var stripeEvent = EventUtility.ConstructEvent(json, signature, webhookSecret);
|
|
|
|
_logger.LogInformation($"Processing Stripe webhook: {stripeEvent.Type}");
|
|
|
|
switch (stripeEvent.Type)
|
|
{
|
|
case "checkout.session.completed":
|
|
var session = stripeEvent.Data.Object as Session;
|
|
if (session?.SubscriptionId != null)
|
|
{
|
|
var subscriptionService = new SubscriptionService();
|
|
var subscription = await subscriptionService.GetAsync(session.SubscriptionId);
|
|
await ProcessSubscriptionActivation(session.ClientReferenceId, subscription);
|
|
}
|
|
break;
|
|
|
|
case "invoice.finalized":
|
|
var invoice = stripeEvent.Data.Object as 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;
|
|
}
|
|
|
|
if (subscriptionId != null)
|
|
{
|
|
var subscriptionService = new SubscriptionService();
|
|
var subscription = await subscriptionService.GetAsync(subscriptionId);
|
|
var user = await _userService.GetUserByStripeCustomerIdAsync(subscription.CustomerId);
|
|
if (user != null)
|
|
{
|
|
await ProcessSubscriptionActivation(user.Id, subscription);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "customer.subscription.deleted":
|
|
var deletedSubscription = stripeEvent.Data.Object as Subscription;
|
|
if (deletedSubscription != null)
|
|
{
|
|
await _userService.DeactivatePremiumStatus(deletedSubscription.Id);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
_logger.LogWarning($"Unhandled Stripe webhook event type: {stripeEvent.Type}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private async Task ProcessSubscriptionActivation(string userId, Subscription subscription)
|
|
{
|
|
var service = new SubscriptionItemService();
|
|
var subItem = service.Get(subscription.Items.Data[0].Id);
|
|
if (string.IsNullOrEmpty(userId) || subscription == null)
|
|
{
|
|
_logger.LogWarning("Could not process subscription activation due to missing userId or subscription data.");
|
|
return;
|
|
}
|
|
|
|
var user = await _userService.GetUserAsync(userId);
|
|
if (user == null)
|
|
{
|
|
_logger.LogWarning($"User not found for premium activation: {userId}");
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(user.StripeCustomerId))
|
|
{
|
|
await _userService.UpdateUserStripeCustomerIdAsync(user.Id, subscription.CustomerId);
|
|
}
|
|
|
|
await _userService.ActivatePremiumStatus(userId, subscription.Id, subItem.CurrentPeriodEnd);
|
|
|
|
_logger.LogInformation($"Successfully processed premium activation/renewal for user {userId}.");
|
|
}
|
|
|
|
public async Task<string> GetSubscriptionStatusAsync(string? subscriptionId)
|
|
{
|
|
if (string.IsNullOrEmpty(subscriptionId)) return "None";
|
|
try
|
|
{
|
|
var service = new SubscriptionService();
|
|
var subscription = await service.GetAsync(subscriptionId);
|
|
return subscription.Status;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, $"Error getting subscription status for {subscriptionId}");
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
public async Task<bool> CancelSubscriptionAsync(string subscriptionId)
|
|
{
|
|
try
|
|
{
|
|
var service = new SubscriptionService();
|
|
await service.CancelAsync(subscriptionId, new SubscriptionCancelOptions());
|
|
_logger.LogInformation($"Canceled subscription {subscriptionId} via API.");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, $"Error canceling subscription {subscriptionId}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica se a assinatura está dentro do período de 7 dias para reembolso (CDC)
|
|
/// </summary>
|
|
public bool IsEligibleForRefund(DateTime? subscriptionStartedAt)
|
|
{
|
|
if (!subscriptionStartedAt.HasValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var daysSinceSubscription = (DateTime.UtcNow - subscriptionStartedAt.Value).TotalDays;
|
|
return daysSinceSubscription <= 7;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancela assinatura E processa reembolso total (CDC - 7 dias)
|
|
/// </summary>
|
|
public async Task<(bool success, string message)> CancelAndRefundSubscriptionAsync(string userId)
|
|
{
|
|
try
|
|
{
|
|
var user = await _userService.GetUserAsync(userId);
|
|
if (user == null)
|
|
{
|
|
return (false, "Usuário não encontrado");
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(user.StripeSubscriptionId))
|
|
{
|
|
return (false, "Nenhuma assinatura ativa encontrada");
|
|
}
|
|
|
|
// Verifica elegibilidade para reembolso
|
|
if (!IsEligibleForRefund(user.SubscriptionStartedAt))
|
|
{
|
|
var daysSince = user.SubscriptionStartedAt.HasValue
|
|
? (DateTime.UtcNow - user.SubscriptionStartedAt.Value).TotalDays
|
|
: 0;
|
|
return (false, $"Período de reembolso de 7 dias expirado (assinatura criada há {Math.Round(daysSince, 1)} dias). Você ainda pode cancelar a renovação.");
|
|
}
|
|
|
|
// Busca a assinatura no Stripe
|
|
var subscriptionService = new SubscriptionService();
|
|
var subscription = await subscriptionService.GetAsync(user.StripeSubscriptionId);
|
|
|
|
if (subscription == null)
|
|
{
|
|
return (false, "Assinatura não encontrada no Stripe");
|
|
}
|
|
|
|
// Cancela a assinatura primeiro
|
|
await subscriptionService.CancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
|
|
|
// Busca o último pagamento (invoice) desta assinatura para reembolsar
|
|
var invoiceService = new InvoiceService();
|
|
var invoiceListOptions = new InvoiceListOptions
|
|
{
|
|
Subscription = subscription.Id,
|
|
Limit = 1,
|
|
Status = "paid"
|
|
};
|
|
var invoices = await invoiceService.ListAsync(invoiceListOptions);
|
|
var latestInvoice = invoices.Data.FirstOrDefault();
|
|
|
|
if (latestInvoice == null || latestInvoice.AmountPaid <= 0)
|
|
{
|
|
// Mesmo sem invoice, cancela e desativa
|
|
await _userService.DeactivatePremiumStatus(subscription.Id);
|
|
return (true, "Assinatura cancelada com sucesso. Nenhum pagamento para reembolsar foi encontrado.");
|
|
}
|
|
|
|
// Processa o reembolso - Stripe reembolsa automaticamente o último pagamento
|
|
var refundService = new RefundService();
|
|
var refundOptions = new RefundCreateOptions
|
|
{
|
|
Amount = latestInvoice.AmountPaid, // Reembolso total
|
|
Reason = RefundReasons.RequestedByCustomer,
|
|
Metadata = new Dictionary<string, string>
|
|
{
|
|
{ "user_id", userId },
|
|
{ "subscription_id", subscription.Id },
|
|
{ "invoice_id", latestInvoice.Id },
|
|
{ "refund_reason", "CDC 7 dias - Direito de arrependimento" }
|
|
}
|
|
};
|
|
|
|
// Stripe automaticamente encontra o charge/payment_intent correto através do subscription_id no metadata
|
|
// Alternativamente, podemos buscar o último charge da subscription
|
|
try
|
|
{
|
|
// Tenta reembolsar usando a subscription (Stripe encontra o charge automaticamente)
|
|
var chargeService = new ChargeService();
|
|
var chargeOptions = new ChargeListOptions
|
|
{
|
|
Limit = 1,
|
|
Customer = subscription.CustomerId
|
|
};
|
|
var charges = await chargeService.ListAsync(chargeOptions);
|
|
var lastCharge = charges.Data.FirstOrDefault();
|
|
|
|
if (lastCharge != null)
|
|
{
|
|
refundOptions.Charge = lastCharge.Id;
|
|
var refund = await refundService.CreateAsync(refundOptions);
|
|
|
|
if (refund.Status == "succeeded" || refund.Status == "pending")
|
|
{
|
|
// Desativa o premium imediatamente no caso de reembolso
|
|
await _userService.DeactivatePremiumStatus(subscription.Id);
|
|
|
|
_logger.LogInformation($"Successfully refunded and canceled subscription {subscription.Id} for user {userId}. Refund ID: {refund.Id}");
|
|
|
|
return (true, $"Reembolso processado com sucesso! Você receberá R$ {(latestInvoice.AmountPaid / 100.0):F2} de volta em 5-10 dias úteis.");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning($"Refund failed with status {refund.Status} for subscription {subscription.Id}");
|
|
await _userService.DeactivatePremiumStatus(subscription.Id);
|
|
return (false, "Falha ao processar reembolso, mas assinatura foi cancelada. Entre em contato com o suporte.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await _userService.DeactivatePremiumStatus(subscription.Id);
|
|
return (false, "Assinatura cancelada, mas nenhuma cobrança encontrada para reembolsar. Entre em contato com o suporte.");
|
|
}
|
|
}
|
|
catch (StripeException refundEx)
|
|
{
|
|
_logger.LogError(refundEx, $"Error creating refund for subscription {subscription.Id}");
|
|
await _userService.DeactivatePremiumStatus(subscription.Id);
|
|
return (false, $"Assinatura cancelada, mas erro ao processar reembolso: {refundEx.Message}. Entre em contato com o suporte.");
|
|
}
|
|
}
|
|
catch (StripeException ex)
|
|
{
|
|
_logger.LogError(ex, $"Stripe error during refund for user {userId}: {ex.Message}");
|
|
return (false, $"Erro ao processar reembolso: {ex.StripeError?.Message ?? ex.Message}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, $"Error processing refund for user {userId}");
|
|
return (false, "Erro inesperado ao processar reembolso. Tente novamente mais tarde.");
|
|
}
|
|
}
|
|
}
|
|
}
|