Files
social-media/backend/src/Web/Features/Memberships/Infrastructure/StripeService.cs
2025-01-15 15:24:30 -05:00

429 lines
15 KiB
C#

using System.ComponentModel.DataAnnotations;
using Hutopy.Web.Features.Memberships.Data;
using Hutopy.Web.Features.Memberships.Events;
using Microsoft.Extensions.Options;
using Stripe;
using Stripe.Checkout;
using Subscription = Stripe.Subscription;
namespace Hutopy.Web.Features.Memberships.Infrastructure;
public class StripeOptions
{
[Required] public required string SecretKey { get; init; }
[Required] public required string WebhookSecret { get; init; }
[Required] [Range(0, 1)] public required decimal HutopyRate { get; init; }
}
public sealed class StripeService(
IOptions<StripeOptions> paymentOptions,
MembershipDbContext dbContext,
PushNotificationService notificationService)
{
public async Task<string> CreateProductAsync(
Guid creatorId,
Guid tierId,
string productName,
string currencyCode,
decimal amount)
{
StripeConfiguration.ApiKey = paymentOptions.Value.SecretKey;
// Create the product
var productService = new ProductService();
var product = await productService.CreateAsync(
new ProductCreateOptions
{
Name = productName,
Metadata = { { "creatorId", creatorId.ToString() }, { "tierId", tierId.ToString() } }
});
// Create the price for the product
var priceService = new PriceService();
await priceService.CreateAsync(
new PriceCreateOptions
{
Product = product.Id,
UnitAmountDecimal = amount * 100, // Convert amount to cents
Currency = currencyCode,
Recurring = new PriceRecurringOptions { Interval = "month" }
});
return product.Id;
}
public async Task<Session> CreateTipCheckoutSessionAsync(
Guid creatorId,
string creatorName,
decimal amount,
string currencyCode,
string message,
string creatorAccountId,
string successUrl,
string cancelUrl,
CancellationToken ct = default)
{
StripeConfiguration.ApiKey = paymentOptions.Value.SecretKey;
// Create Stripe customer for the user if not already created
var customerService = new CustomerService();
var customer = await customerService.CreateAsync(
new CustomerCreateOptions{},
cancellationToken: ct);
// Create paymentIntent for the user
var sessionService = new SessionService();
return await sessionService.CreateAsync(
new SessionCreateOptions
{
Customer = customer.Id,
PaymentMethodTypes = ["card"],
LineItems =
[
new SessionLineItemOptions
{
PriceData = new SessionLineItemPriceDataOptions
{
Currency = currencyCode,
UnitAmountDecimal = amount, // Amount in cents
ProductData = new SessionLineItemPriceDataProductDataOptions
{
Name = $"Tip for {creatorName}", // or any descriptive name for the tip
Metadata = new Dictionary<string, string> { { "creatorId", creatorId.ToString() } }
}
},
Quantity = 1
}
],
Mode = "payment",
PaymentIntentData = new SessionPaymentIntentDataOptions
{
ApplicationFeeAmount =
Convert.ToInt64(amount * 100 * paymentOptions.Value.HutopyRate), // Platform fee
TransferData = new SessionPaymentIntentDataTransferDataOptions
{
Destination = creatorAccountId // Creator's Stripe account ID
}
},
SuccessUrl = successUrl, // Redirect after successful payment
CancelUrl = cancelUrl, // Redirect after canceled payment
Metadata = new Dictionary<string, string>
{
{ "creatorId", creatorId.ToString() },
{ "creatorName", creatorName },
{ "message", message },
}
},
cancellationToken: ct);
}
public async Task<Session> CreateSubscriptionCheckoutSession(
Guid userId,
Guid creatorId,
string creatorName,
string creatorAccountId,
Guid tierId,
string priceId,
string successUrl,
string cancelUrl)
{
StripeConfiguration.ApiKey = paymentOptions.Value.SecretKey;
// Create Stripe customer for the user if not already created
var customerService = new CustomerService();
var customer = await customerService.CreateAsync(
new CustomerCreateOptions
{
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
});
// Create Checkout Session for the subscription
var sessionService = new SessionService();
return await sessionService.CreateAsync(
new SessionCreateOptions
{
Customer = customer.Id,
PaymentMethodTypes = ["card"],
LineItems =
[
new SessionLineItemOptions { Price = priceId, Quantity = 1 }
],
Mode = "subscription",
SubscriptionData = new SessionSubscriptionDataOptions
{
ApplicationFeePercent = paymentOptions.Value.HutopyRate,
TransferData = new SessionSubscriptionDataTransferDataOptions { Destination = creatorAccountId }
},
SuccessUrl = successUrl, // Redirect after successful payment
CancelUrl = cancelUrl, // Redirect after canceled payment
Metadata = new Dictionary<string, string>
{
{ "userId", userId.ToString() },
{ "creatorId", creatorId.ToString() },
{ "creatorName", creatorName },
{ "tierId", tierId.ToString() }
}
});
}
public async Task CancelSubscription(
Guid subscriptionId)
{
var subscriptionService = new SubscriptionService();
await subscriptionService.CancelAsync(subscriptionId.ToString());
}
public async Task HandleInvoicePaymentSucceeded(
Event stripeEvent,
CancellationToken ct = default)
{
// Ensure we have an invoice related to a Subscription
if (stripeEvent.Data.Object is not Invoice { Subscription: not null } invoice)
{
return;
}
var subscription = await dbContext
.Subscriptions
.FirstOrDefaultAsync(
subscription => subscription.StripeSubscriptionId == invoice.Subscription.Id,
cancellationToken: ct);
if (subscription == null)
{
return;
}
// Record the Transaction
var transaction = new Transaction
{
Id = Guid.NewGuid(),
CreatedAt = DateTimeOffset.UtcNow,
Amount = invoice.AmountPaid / 100m, // Convert amount from cents to dollars
Currency = invoice.Currency,
Type = "Subscription",
Timestamp = DateTime.UtcNow,
StripeInvoiceUrl = invoice.HostedInvoiceUrl
};
dbContext.Transactions.Add(transaction);
// Link the Transaction to the Subscription
subscription.Transactions.Add(transaction);
await dbContext.SaveChangesAsync(ct);
}
public async Task HandleInvoicePaymentFailed(
Event stripeEvent,
CancellationToken ct = default)
{
if (stripeEvent.Data.Object is not Invoice { Subscription: not null } invoice)
{
return;
}
var subscription = await dbContext
.Subscriptions
.SingleOrDefaultAsync(
subscription => subscription.StripeSubscriptionId == invoice.SubscriptionId,
cancellationToken: ct);
if (subscription != null)
{
subscription.EndDate = DateTimeOffset.UtcNow; // Mark as expired or failed
await dbContext.SaveChangesAsync(ct);
}
}
private async Task HandleTipPayment(
Session session,
CancellationToken ct)
{
// Record the Tip
var tip = new Tip
{
Id = Guid.NewGuid(),
CreatedAt = DateTimeOffset.UtcNow,
StripeSessionId = session.Id,
TipperId = Guid.Parse(session.Metadata["tipperId"]),
TipperName = session.Metadata["tipperName"],
CreatorId = Guid.Parse(session.Metadata["creatorId"]),
CreatorName = session.Metadata["creatorName"],
Amount = session.AmountTotal!.Value / 100m,
Currency = session.Currency,
Message = session.Metadata["message"]
};
dbContext.Tips.Add(tip);
// Record the Transaction
var transaction = new Transaction
{
Id = Guid.NewGuid(),
CreatedAt = DateTimeOffset.UtcNow,
Amount = tip.Amount,
Currency = tip.Currency,
Type = "Tip",
Timestamp = DateTime.UtcNow,
// TODO: __StripeInvoiceUrl = session.Invoice.HostedInvoiceUrl__ How come nor Invoice or InvoiceId are set.
};
dbContext.Transactions.Add(transaction);
// Link the Transaction to the Tip
tip.TransactionId = transaction.Id;
// Save the changes
await dbContext.SaveChangesAsync(ct);
// Notify the Creator
notificationService.NotifyCreator(
tip.CreatorId,
new TipPaid(
tip.CreatorId,
tip.CreatorName,
tip.Amount,
tip.Currency,
tip.Message)
);
}
private async Task HandleSubscriptionPayment(
Session session,
CancellationToken ct)
{
// Record the Subscription
var subscription = new Data.Subscription
{
Id = Guid.NewGuid(),
CreatedAt = DateTimeOffset.UtcNow,
UserId = Guid.Parse(session.Metadata["userId"]),
CreatorId = Guid.Parse(session.Metadata["creatorId"]),
TierId = Guid.Parse(session.Metadata["tierId"]),
StartDate = DateTimeOffset.UtcNow,
StripeSessionId = session.Id,
StripeSubscriptionId = session.SubscriptionId
};
dbContext.Subscriptions.Add(subscription);
// Record the Transaction
var transaction = new Transaction
{
Id = Guid.NewGuid(),
CreatedAt = DateTimeOffset.UtcNow,
Amount = session.AmountTotal!.Value / 100m, // Convert amount from cents to dollars
Currency = session.Currency,
Type = "Subscription",
Timestamp = DateTime.UtcNow,
// TODO: __StripeInvoiceUrl = session.Invoice.HostedInvoiceUrl__ How come nor Invoice or InvoiceId are set.
};
dbContext.Transactions.Add(transaction);
// Link the Transaction to the Subscription
subscription.Transactions.Add(transaction);
// Save the changes
await dbContext.SaveChangesAsync(ct);
// Notify the Creator
notificationService.NotifyCreator(
subscription.CreatorId,
new SubscriptionPaid(
subscription.CreatorId,
session.Metadata["creatorName"],
subscription.TierId.ToString(),
subscription.StartDate)
);
}
public async Task HandleCheckoutSessionCompleted(
Event stripeEvent,
CancellationToken ct = default)
{
if (stripeEvent.Data.Object is not Session session)
{
return;
}
switch (session.Mode)
{
// Check if this is a one-time tip
case "payment" when session.PaymentIntentId != null:
await HandleTipPayment(session, ct);
break;
// Check if this is a subscription
case "subscription" when session.SubscriptionId != null:
await HandleSubscriptionPayment(session, ct);
break;
}
}
public async Task HandleCustomerSubscriptionCreated(
Event stripeEvent,
CancellationToken ct)
{
if (stripeEvent.Data.Object is not Subscription stripeSubscription)
return;
var subscription = await dbContext
.Subscriptions
.SingleOrDefaultAsync(
subscription => subscription.StripeSubscriptionId == stripeSubscription.Id,
cancellationToken: ct);
if (subscription != null)
{
subscription.StartDate = stripeSubscription.CurrentPeriodStart;
subscription.EndDate = null; // Active subscription
await dbContext.SaveChangesAsync(ct);
}
}
public async Task HandleCustomerSubscriptionUpdated(
Event stripeEvent,
CancellationToken ct)
{
if (stripeEvent.Data.Object is Subscription stripeSubscription)
{
var subscription = await dbContext
.Subscriptions
.SingleOrDefaultAsync(
s => s.StripeSubscriptionId == stripeSubscription.Id,
cancellationToken: ct);
if (subscription != null)
{
subscription.StartDate = stripeSubscription.CurrentPeriodStart;
subscription.EndDate = null; // Active subscription
await dbContext.SaveChangesAsync(ct);
}
}
}
public async Task HandleCustomerSubscriptionDeleted(
Event stripeEvent,
CancellationToken ct)
{
var subscription = stripeEvent.Data.Object as Subscription;
var existingSubscription = await dbContext
.Subscriptions
.FirstOrDefaultAsync(x => x.StripeSubscriptionId == subscription!.Id, ct);
if (existingSubscription != null)
{
var today = DateTime.Today;
int lastDay = DateTime.DaysInMonth(today.Year, today.Month);
var lastDayOfMonth = new DateTime(today.Year, today.Month, lastDay);
existingSubscription.EndDate = new DateTimeOffset(lastDayOfMonth);
await dbContext.SaveChangesAsync(ct);
}
}
}