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; public class StripeWebhookEndpoint( ITipPaymentNotifier tipPaymentNotifier, IMembershipNotifier membershipNotifier, IOptions options, ILogger logger) : EndpointWithoutRequest { public override void Configure() { Post("/api/stripe"); AllowAnonymous(); Options(o => o.WithTags("Webhooks")); } public override async Task HandleAsync(CancellationToken ct) { using StreamReader streamReader = new(HttpContext.Request.Body); var json = await streamReader.ReadToEndAsync(ct); var signatureHeader = HttpContext.Request.Headers["Stripe-Signature"]; var stripeEvent = EventUtility.ConstructEvent(json, signatureHeader, options.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); logger.LogWarning(stripeSession.ToJson()); switch (stripeSession.Mode) { // Check if this is a one-time tip case "payment" when stripeSession.PaymentIntentId != null && stripeSession.PaymentIntent.Status == "paid": // Get the customer email from the appropriate place var customerEmail = stripeSession.CustomerDetails?.Email ?? stripeSession.Customer?.Email ?? ""; // Get the receipt URL, preferring the one directly on the charge if available var receiptUrl = stripeSession.Invoice?.HostedInvoiceUrl ?? ""; await tipPaymentNotifier.NotifyPaymentSucceedAsync( stripeSession.Id, receiptUrl, customerEmail, ct); 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); 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); break; case "customer.subscription.updated": Debug.Assert(stripeSubscription != null); await membershipNotifier.NotifySubscriptionUpdatedAsync( stripeSubscription.Id, stripeSubscription.CancelAt ?? stripeSubscription.CanceledAt, ct); break; case "customer.subscription.deleted": Debug.Assert(stripeSubscription != null); await membershipNotifier.NotifySubscriptionDeletedAsync( stripeSubscription.Id, ct); break; } await SendOkAsync(ct); } }