120 lines
5.0 KiB
C#
120 lines
5.0 KiB
C#
using System.Diagnostics;
|
|
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
|
using Hutopy.Modules.Memberships.Contracts;
|
|
using Hutopy.Modules.Tipping.Contracts;
|
|
using Microsoft.Extensions.Options;
|
|
using Stripe;
|
|
using Stripe.Checkout;
|
|
|
|
namespace Hutopy.Modules.Memberships.Handlers;
|
|
|
|
internal class StripeWebhookEndpoint(
|
|
ITipPaymentNotifier tipPaymentNotifier,
|
|
IMembershipNotifier membershipNotifier,
|
|
IOptions<StripeOptions> stripeOptions)
|
|
: EndpointWithoutRequest
|
|
{
|
|
public override void Configure()
|
|
{
|
|
Post("/api/stripe");
|
|
AllowAnonymous();
|
|
Options(o => o.WithTags("Webhooks"));
|
|
}
|
|
|
|
public override async Task HandleAsync(CancellationToken ct)
|
|
{
|
|
var signatureHeader = HttpContext.Request.Headers["Stripe-Signature"];
|
|
using StreamReader streamReader = new(HttpContext.Request.Body);
|
|
|
|
var json = await streamReader.ReadToEndAsync(ct).ConfigureAwait(false);
|
|
|
|
var stripeEvent = EventUtility.ConstructEvent(json, signatureHeader, stripeOptions.Value.WebhookSecret);
|
|
|
|
var stripeSession = stripeEvent.Data.Object as Session;
|
|
var stripeSubscription = stripeEvent.Data.Object as Subscription;
|
|
|
|
switch (stripeEvent.Type)
|
|
{
|
|
case "checkout.session.completed":
|
|
Debug.Assert(stripeSession != null);
|
|
switch (stripeSession.Mode)
|
|
{
|
|
// Check if this is a one-time tip
|
|
case "payment" when stripeSession is { PaymentIntentId: not null, PaymentStatus: "paid" }:
|
|
// Get the customer email from the appropriate place
|
|
var customerEmail = stripeSession.CustomerDetails?.Email ??
|
|
stripeSession.Customer?.Email ??
|
|
"";
|
|
|
|
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
|
var paymentIntentService = new PaymentIntentService();
|
|
var paymentIntent = await paymentIntentService
|
|
.GetAsync(
|
|
stripeSession.PaymentIntentId,
|
|
new PaymentIntentGetOptions { Expand = ["latest_charge"] },
|
|
cancellationToken: ct)
|
|
.ConfigureAwait(false);
|
|
var receiptUrl = paymentIntent.LatestCharge.ReceiptUrl;
|
|
var receiptUri = new Uri(receiptUrl);
|
|
|
|
// Get the receipt URL, preferring the one directly on the charge if available
|
|
await tipPaymentNotifier
|
|
.NotifyPaymentSucceedAsync(
|
|
stripeSession.Id,
|
|
receiptUri,
|
|
customerEmail,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
break;
|
|
|
|
// Check if this is a subscription
|
|
case "subscription" when stripeSession.SubscriptionId != null:
|
|
await membershipNotifier
|
|
.NotifyPaymentSucceedAsync(
|
|
stripeSession.SubscriptionId,
|
|
stripeSession.Invoice.HostedInvoiceUrl,
|
|
stripeSession.Invoice.Total,
|
|
stripeSession.Invoice.Currency,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
break;
|
|
}
|
|
|
|
break;
|
|
case "invoice.payment_succeeded":
|
|
var invoice = stripeEvent.Data.Object as Invoice;
|
|
Debug.Assert(invoice != null);
|
|
Debug.Assert(invoice.Subscription != null);
|
|
await membershipNotifier
|
|
.NotifyPaymentSucceedAsync(
|
|
invoice.SubscriptionId,
|
|
invoice.HostedInvoiceUrl,
|
|
invoice.Total,
|
|
invoice.Currency,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
break;
|
|
|
|
case "customer.subscription.updated":
|
|
Debug.Assert(stripeSubscription != null);
|
|
await membershipNotifier
|
|
.NotifySubscriptionUpdatedAsync(
|
|
stripeSubscription.Id,
|
|
stripeSubscription.CancelAt ?? stripeSubscription.CanceledAt,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
break;
|
|
case "customer.subscription.deleted":
|
|
Debug.Assert(stripeSubscription != null);
|
|
await membershipNotifier
|
|
.NotifySubscriptionDeletedAsync(
|
|
stripeSubscription.Id,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
break;
|
|
}
|
|
|
|
await SendOkAsync(ct).ConfigureAwait(false);
|
|
}
|
|
}
|