fix(stripe): correcting webhook
This commit is contained in:
@@ -8,11 +8,10 @@ using Stripe.Checkout;
|
|||||||
|
|
||||||
namespace Hutopy.Modules.Memberships.Handlers;
|
namespace Hutopy.Modules.Memberships.Handlers;
|
||||||
|
|
||||||
public class StripeWebhookEndpoint(
|
internal class StripeWebhookEndpoint(
|
||||||
ITipPaymentNotifier tipPaymentNotifier,
|
ITipPaymentNotifier tipPaymentNotifier,
|
||||||
IMembershipNotifier membershipNotifier,
|
IMembershipNotifier membershipNotifier,
|
||||||
IOptions<StripeOptions> options,
|
IOptions<StripeOptions> options)
|
||||||
ILogger<StripeWebhookEndpoint> logger)
|
|
||||||
: EndpointWithoutRequest
|
: EndpointWithoutRequest
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -24,10 +23,11 @@ public class StripeWebhookEndpoint(
|
|||||||
|
|
||||||
public override async Task HandleAsync(CancellationToken ct)
|
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 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, options.Value.WebhookSecret);
|
var stripeEvent = EventUtility.ConstructEvent(json, signatureHeader, options.Value.WebhookSecret);
|
||||||
|
|
||||||
var stripeSession = stripeEvent.Data.Object as Session;
|
var stripeSession = stripeEvent.Data.Object as Session;
|
||||||
@@ -37,16 +37,10 @@ public class StripeWebhookEndpoint(
|
|||||||
{
|
{
|
||||||
case "checkout.session.completed":
|
case "checkout.session.completed":
|
||||||
Debug.Assert(stripeSession != null);
|
Debug.Assert(stripeSession != null);
|
||||||
logger.LogWarning(stripeSession.ToJson());
|
|
||||||
logger.LogWarning("stripeSession.PaymentIntentId: {PaymentIntentId}", stripeSession.PaymentIntentId);
|
|
||||||
logger.LogWarning("stripeSession.PaymentIntent.Status: {PaymentIntentStatus}", stripeSession.PaymentIntent.Status);
|
|
||||||
logger.LogWarning(stripeSession.ToJson());
|
|
||||||
logger.LogWarning(stripeSession.ToJson());
|
|
||||||
switch (stripeSession.Mode)
|
switch (stripeSession.Mode)
|
||||||
{
|
{
|
||||||
// Check if this is a one-time tip
|
// Check if this is a one-time tip
|
||||||
case "payment" when stripeSession.PaymentIntentId != null
|
case "payment" when stripeSession is { PaymentIntentId: not null, PaymentStatus: "paid" }:
|
||||||
&& stripeSession.PaymentIntent.Status == "paid":
|
|
||||||
// Get the customer email from the appropriate place
|
// Get the customer email from the appropriate place
|
||||||
var customerEmail = stripeSession.CustomerDetails?.Email ??
|
var customerEmail = stripeSession.CustomerDetails?.Email ??
|
||||||
stripeSession.Customer?.Email ??
|
stripeSession.Customer?.Email ??
|
||||||
@@ -55,21 +49,25 @@ public class StripeWebhookEndpoint(
|
|||||||
// Get the receipt URL, preferring the one directly on the charge if available
|
// Get the receipt URL, preferring the one directly on the charge if available
|
||||||
var receiptUrl = stripeSession.Invoice?.HostedInvoiceUrl ?? "";
|
var receiptUrl = stripeSession.Invoice?.HostedInvoiceUrl ?? "";
|
||||||
|
|
||||||
await tipPaymentNotifier.NotifyPaymentSucceedAsync(
|
await tipPaymentNotifier
|
||||||
stripeSession.Id,
|
.NotifyPaymentSucceedAsync(
|
||||||
receiptUrl,
|
stripeSession.Id,
|
||||||
customerEmail,
|
receiptUrl,
|
||||||
ct);
|
customerEmail,
|
||||||
|
ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Check if this is a subscription
|
// Check if this is a subscription
|
||||||
case "subscription" when stripeSession.SubscriptionId != null:
|
case "subscription" when stripeSession.SubscriptionId != null:
|
||||||
await membershipNotifier.NotifyPaymentSucceedAsync(
|
await membershipNotifier
|
||||||
stripeSession.SubscriptionId,
|
.NotifyPaymentSucceedAsync(
|
||||||
stripeSession.Invoice.HostedInvoiceUrl,
|
stripeSession.SubscriptionId,
|
||||||
stripeSession.Invoice.Total,
|
stripeSession.Invoice.HostedInvoiceUrl,
|
||||||
stripeSession.Invoice.Currency,
|
stripeSession.Invoice.Total,
|
||||||
ct);
|
stripeSession.Invoice.Currency,
|
||||||
|
ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,29 +76,35 @@ public class StripeWebhookEndpoint(
|
|||||||
var invoice = stripeEvent.Data.Object as Invoice;
|
var invoice = stripeEvent.Data.Object as Invoice;
|
||||||
Debug.Assert(invoice != null);
|
Debug.Assert(invoice != null);
|
||||||
Debug.Assert(invoice.Subscription != null);
|
Debug.Assert(invoice.Subscription != null);
|
||||||
await membershipNotifier.NotifyPaymentSucceedAsync(
|
await membershipNotifier
|
||||||
invoice.SubscriptionId,
|
.NotifyPaymentSucceedAsync(
|
||||||
invoice.HostedInvoiceUrl,
|
invoice.SubscriptionId,
|
||||||
invoice.Total,
|
invoice.HostedInvoiceUrl,
|
||||||
invoice.Currency,
|
invoice.Total,
|
||||||
ct);
|
invoice.Currency,
|
||||||
|
ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "customer.subscription.updated":
|
case "customer.subscription.updated":
|
||||||
Debug.Assert(stripeSubscription != null);
|
Debug.Assert(stripeSubscription != null);
|
||||||
await membershipNotifier.NotifySubscriptionUpdatedAsync(
|
await membershipNotifier
|
||||||
stripeSubscription.Id,
|
.NotifySubscriptionUpdatedAsync(
|
||||||
stripeSubscription.CancelAt ?? stripeSubscription.CanceledAt,
|
stripeSubscription.Id,
|
||||||
ct);
|
stripeSubscription.CancelAt ?? stripeSubscription.CanceledAt,
|
||||||
|
ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
case "customer.subscription.deleted":
|
case "customer.subscription.deleted":
|
||||||
Debug.Assert(stripeSubscription != null);
|
Debug.Assert(stripeSubscription != null);
|
||||||
await membershipNotifier.NotifySubscriptionDeletedAsync(
|
await membershipNotifier
|
||||||
stripeSubscription.Id,
|
.NotifySubscriptionDeletedAsync(
|
||||||
ct);
|
stripeSubscription.Id,
|
||||||
|
ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await SendOkAsync(ct);
|
await SendOkAsync(ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,111 +6,119 @@ using Hutopy.Modules.Tipping.Data;
|
|||||||
namespace Hutopy.Modules.Tipping.Handlers;
|
namespace Hutopy.Modules.Tipping.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record SendTipRequest(
|
internal static class SendTip
|
||||||
Guid CreatorId,
|
|
||||||
decimal Amount,
|
|
||||||
string Currency,
|
|
||||||
string Message,
|
|
||||||
string CheckoutSuccessUrl,
|
|
||||||
string CheckoutCancelledUrl);
|
|
||||||
|
|
||||||
[PublicAPI]
|
|
||||||
public record SendTipResponse(
|
|
||||||
string Id,
|
|
||||||
string Url);
|
|
||||||
|
|
||||||
[PublicAPI]
|
|
||||||
public class SendTipRequestValidator : Validator<SendTipRequest>
|
|
||||||
{
|
{
|
||||||
public SendTipRequestValidator()
|
internal record Request(
|
||||||
|
Guid CreatorId,
|
||||||
|
decimal Amount,
|
||||||
|
string Currency,
|
||||||
|
string Message,
|
||||||
|
string CheckoutSuccessUrl,
|
||||||
|
string CheckoutCancelledUrl);
|
||||||
|
|
||||||
|
internal record Response(
|
||||||
|
string Id,
|
||||||
|
string Url);
|
||||||
|
|
||||||
|
internal class Validator : Validator<Request>
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Amount)
|
public Validator()
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("Tip amount must be greater than 0");
|
|
||||||
|
|
||||||
RuleFor(x => x.CreatorId)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("Creator ID is required");
|
|
||||||
|
|
||||||
RuleFor(x => x.CheckoutSuccessUrl)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("CheckoutSuccessUrl is required");
|
|
||||||
|
|
||||||
RuleFor(x => x.CheckoutCancelledUrl)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("CheckoutCancelledUrl is required");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[PublicAPI]
|
|
||||||
public class SendTipHandler(
|
|
||||||
TippingDbContext dbContext,
|
|
||||||
ITipProcessor tipProcessor,
|
|
||||||
ICreatorLookup creatorLookup)
|
|
||||||
: Endpoint<SendTipRequest, SendTipResponse>
|
|
||||||
{
|
|
||||||
private static readonly Guid AnonymousUserId = Guid.Parse("AAAAAAAA-0000-0000-0000-000000000000");
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Post("/api/tips");
|
|
||||||
Options(o => o.WithTags("Memberships"));
|
|
||||||
|
|
||||||
AllowAnonymous();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(
|
|
||||||
SendTipRequest req,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
|
||||||
CreatorReference? creator = await creatorLookup.GetCreatorAsync(req.CreatorId, ct);
|
|
||||||
if (creator == null)
|
|
||||||
{
|
{
|
||||||
await SendNotFoundAsync(ct);
|
RuleFor(x => x.Amount)
|
||||||
return;
|
.GreaterThan(0)
|
||||||
|
.WithMessage("Tip amount must be greater than 0");
|
||||||
|
|
||||||
|
RuleFor(x => x.CreatorId)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithMessage("Creator ID is required");
|
||||||
|
|
||||||
|
RuleFor(x => x.CheckoutSuccessUrl)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithMessage("CheckoutSuccessUrl is required");
|
||||||
|
|
||||||
|
RuleFor(x => x.CheckoutCancelledUrl)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithMessage("CheckoutCancelledUrl is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Handler(
|
||||||
|
TippingDbContext dbContext,
|
||||||
|
ITipProcessor tipProcessor,
|
||||||
|
ICreatorLookup creatorLookup)
|
||||||
|
: Endpoint<Request, Response>
|
||||||
|
{
|
||||||
|
private static readonly Guid AnonymousUserId = Guid.Parse("AAAAAAAA-0000-0000-0000-000000000000");
|
||||||
|
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/tips");
|
||||||
|
Options(o => o.WithTags("Memberships"));
|
||||||
|
|
||||||
|
AllowAnonymous();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!creator.AcceptCharges)
|
public override async Task HandleAsync(
|
||||||
|
Request req,
|
||||||
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
ArgumentNullException.ThrowIfNull(req);
|
||||||
return;
|
|
||||||
|
var userId = User.Identity?.IsAuthenticated == true
|
||||||
|
? User.GetUserId()
|
||||||
|
: AnonymousUserId;
|
||||||
|
|
||||||
|
var creator = await creatorLookup
|
||||||
|
.GetCreatorAsync(req.CreatorId, ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (creator == null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!creator.AcceptCharges)
|
||||||
|
{
|
||||||
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tipId = Guid.CreateVersion7();
|
||||||
|
|
||||||
|
var checkout = await tipProcessor
|
||||||
|
.CreateCheckoutSessionAsync(
|
||||||
|
tipId,
|
||||||
|
creator,
|
||||||
|
req.Amount,
|
||||||
|
req.Currency,
|
||||||
|
req.Message,
|
||||||
|
req.CheckoutSuccessUrl,
|
||||||
|
req.CheckoutCancelledUrl,
|
||||||
|
ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
Tip tip = new()
|
||||||
|
{
|
||||||
|
Id = tipId,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
StripeSessionId = checkout.Id,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatorId = req.CreatorId,
|
||||||
|
Status = TipStatus.Pending,
|
||||||
|
Amount = req.Amount,
|
||||||
|
Currency = req.Currency,
|
||||||
|
Message = req.Message
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.Tips.Add(tip);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await SendAsync(
|
||||||
|
new Response(checkout.Id, checkout.Url),
|
||||||
|
cancellation: ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid tipId = Guid.CreateVersion7();
|
|
||||||
|
|
||||||
TipCheckoutSession checkout = await tipProcessor.CreateCheckoutSessionAsync(
|
|
||||||
tipId,
|
|
||||||
creator,
|
|
||||||
req.Amount,
|
|
||||||
req.Currency,
|
|
||||||
req.Message,
|
|
||||||
req.CheckoutSuccessUrl,
|
|
||||||
req.CheckoutCancelledUrl,
|
|
||||||
ct);
|
|
||||||
|
|
||||||
Guid userId = User.Identity?.IsAuthenticated == true
|
|
||||||
? User.GetUserId()
|
|
||||||
: AnonymousUserId;
|
|
||||||
|
|
||||||
Tip tip = new()
|
|
||||||
{
|
|
||||||
Id = tipId,
|
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
|
||||||
StripeSessionId = checkout.Id,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatorId = req.CreatorId,
|
|
||||||
Status = TipStatus.Pending,
|
|
||||||
Amount = req.Amount,
|
|
||||||
Currency = req.Currency,
|
|
||||||
Message = req.Message
|
|
||||||
};
|
|
||||||
|
|
||||||
dbContext.Tips.Add(tip);
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
|
||||||
|
|
||||||
await SendAsync(
|
|
||||||
new SendTipResponse(checkout.Id, checkout.Url),
|
|
||||||
cancellation: ct);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user