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) : 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); } }