fix(stripe): correcting webhook

This commit is contained in:
2025-08-04 17:15:13 -04:00
parent 18532963a8
commit ea8efd21a1
6 changed files with 134 additions and 113 deletions

View File

@@ -7,7 +7,7 @@ using Stripe.Checkout;
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
public class StripeTipProcessor(
internal class StripeTipProcessor(
IOptions<StripeOptions> stripeOptions)
: ITipProcessor
{
@@ -17,21 +17,23 @@ public class StripeTipProcessor(
decimal amount,
string currency,
string message,
string successUrl,
string cancelUrl,
Uri successUrl,
Uri cancelUrl,
CancellationToken ct = default)
{
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
// Create Stripe customer for the user if not already created
CustomerService customerService = new();
Customer? customer = await customerService.CreateAsync(
var customer = await customerService
.CreateAsync(
new CustomerCreateOptions(),
cancellationToken: ct);
cancellationToken: ct)
.ConfigureAwait(false);
// Create paymentIntent for the user
SessionService sessionService = new();
Session? session = await sessionService.CreateAsync(
var session = await sessionService.CreateAsync(
new SessionCreateOptions
{
ClientReferenceId = tipId.ToString(),
@@ -57,21 +59,21 @@ public class StripeTipProcessor(
Mode = "payment",
PaymentIntentData = new SessionPaymentIntentDataOptions
{
ApplicationFeeAmount =
Convert.ToInt64(amount * stripeOptions.Value.HutopyRate), // Platform fee
ApplicationFeeAmount = Convert.ToInt64(amount * stripeOptions.Value.HutopyRate), // Platform fee
TransferData = new SessionPaymentIntentDataTransferDataOptions
{
Destination = creator.StripeAccountId // Creator's Stripe account ID
}
},
SuccessUrl = successUrl, // Redirect after successful payment
CancelUrl = cancelUrl, // Redirect after canceled payment
SuccessUrl = successUrl.ToString(), // Redirect after successful payment
CancelUrl = cancelUrl.ToString(), // Redirect after canceled payment
Metadata = new Dictionary<string, string>
{
{ "creatorId", creator.Id.ToString() }, { "creatorName", creator.Name }, { "message", message }
}
},
cancellationToken: ct);
cancellationToken: ct)
.ConfigureAwait(false);
return new TipCheckoutSession(session.Id, session.Url);
}

View File

@@ -11,7 +11,7 @@ namespace Hutopy.Modules.Memberships.Handlers;
internal class StripeWebhookEndpoint(
ITipPaymentNotifier tipPaymentNotifier,
IMembershipNotifier membershipNotifier,
IOptions<StripeOptions> options)
IOptions<StripeOptions> stripeOptions)
: EndpointWithoutRequest
{
public override void Configure()
@@ -28,7 +28,7 @@ internal class StripeWebhookEndpoint(
var json = await streamReader.ReadToEndAsync(ct).ConfigureAwait(false);
var stripeEvent = EventUtility.ConstructEvent(json, signatureHeader, options.Value.WebhookSecret);
var stripeEvent = EventUtility.ConstructEvent(json, signatureHeader, stripeOptions.Value.WebhookSecret);
var stripeSession = stripeEvent.Data.Object as Session;
var stripeSubscription = stripeEvent.Data.Object as Subscription;
@@ -46,13 +46,22 @@ internal class StripeWebhookEndpoint(
stripeSession.Customer?.Email ??
"";
// Get the receipt URL, preferring the one directly on the charge if available
var receiptUrl = stripeSession.Invoice?.HostedInvoiceUrl ?? "";
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,
receiptUrl,
receiptUri,
customerEmail,
ct)
.ConfigureAwait(false);

View File

@@ -1,10 +1,10 @@
namespace Hutopy.Modules.Tipping.Contracts;
public interface ITipPaymentNotifier
internal interface ITipPaymentNotifier
{
Task NotifyPaymentSucceedAsync(
string stripeId,
string invoiceUrl,
string sessionId,
Uri receiptUrl,
string customerEmail,
CancellationToken ct);
}

View File

@@ -2,7 +2,7 @@ using Hutopy.Modules.Creators.Contracts;
namespace Hutopy.Modules.Tipping.Contracts;
public interface ITipProcessor
internal interface ITipProcessor
{
Task<TipCheckoutSession> CreateCheckoutSessionAsync(
Guid tipId,
@@ -10,7 +10,7 @@ public interface ITipProcessor
decimal amount,
string currency,
string message,
string successUrl,
string cancelUrl,
Uri successUrl,
Uri cancelUrl,
CancellationToken ct = default);
}

View File

@@ -13,12 +13,12 @@ internal static class SendTip
decimal Amount,
string Currency,
string Message,
string CheckoutSuccessUrl,
string CheckoutCancelledUrl);
Uri CheckoutSuccessUrl,
Uri CheckoutCancelledUrl);
internal record Response(
string Id,
string Url);
Uri Url);
internal class Validator : Validator<Request>
{
@@ -116,7 +116,7 @@ internal static class SendTip
await dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
await SendAsync(
new Response(checkout.Id, checkout.Url),
new Response(checkout.Id, new Uri(checkout.Url)),
cancellation: ct)
.ConfigureAwait(false);
}

View File

@@ -5,7 +5,7 @@ using Hutopy.Modules.Tipping.Data;
namespace Hutopy.Modules.Tipping.Services;
public class TipPaymentNotifier(
internal class TipPaymentNotifier(
TippingDbContext dbContext,
IEmailSender emailSender,
ICreatorLookup creatorLookup,
@@ -14,22 +14,30 @@ public class TipPaymentNotifier(
{
public async Task NotifyPaymentSucceedAsync(
string sessionId,
string receiptUrl,
Uri receiptUrl,
string customerEmail,
CancellationToken ct)
{
Tip? tip = await dbContext.Tips.SingleOrDefaultAsync(
var tip = await dbContext
.Tips
.SingleOrDefaultAsync(
t => t.StripeSessionId == sessionId,
ct);
ct)
.ConfigureAwait(false);
if (tip is not null)
{
tip.Status = TipStatus.Paid;
tip.StripeInvoiceUrl = receiptUrl; // Store the receipt URL
await dbContext.SaveChangesAsync(ct);
tip.StripeInvoiceUrl = receiptUrl.ToString(); // Store the receipt URL
await dbContext
.SaveChangesAsync(ct)
.ConfigureAwait(false);
// Look up creator information
CreatorReference? creator = await creatorLookup.GetCreatorAsync(tip.CreatorId, ct);
var creator = await creatorLookup
.GetCreatorAsync(tip.CreatorId, ct)
.ConfigureAwait(false);
if (!string.IsNullOrEmpty(customerEmail))
{
@@ -38,7 +46,8 @@ public class TipPaymentNotifier(
creator?.Name ?? "le créateur",
tip.Amount,
tip.Currency,
receiptUrl); // Pass the receipt URL
receiptUrl)
.ConfigureAwait(false); // Pass the receipt URL
}
}
else
@@ -52,10 +61,10 @@ public class TipPaymentNotifier(
string creatorUsername,
decimal amount,
string currency,
string receiptUrl) // Add receipt URL parameter
Uri receiptUrl) // Add receipt URL parameter
{
string subject = $"Merci pour votre soutien à {creatorUsername}";
string message = $"""
var subject = $"Merci pour votre soutien à {creatorUsername}";
var message = $"""
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;">
<h1 style="color: #2c3e50; margin-bottom: 20px;">{creatorUsername} vous remercie !</h1>
@@ -69,7 +78,6 @@ public class TipPaymentNotifier(
</p>
</div>
{(string.IsNullOrEmpty(receiptUrl) ? "" : $"""
<div style="text-align: center; margin: 30px 0;">
<a href='{receiptUrl}'
style="background-color: #3498db;
@@ -83,7 +91,6 @@ public class TipPaymentNotifier(
Voir le reçu
</a>
</div>
""")}
<p style="font-size: 14px; color: #7f8c8d; margin-top: 30px;">
Cet email sert de reçu pour votre transaction. Nous vous conseillons de le conserver pour vos archives.
@@ -97,7 +104,10 @@ public class TipPaymentNotifier(
try
{
await emailSender.SendEmailAsync(email, subject, message);
await emailSender
.SendEmailAsync(email, subject, message)
.ConfigureAwait(false);
logger.LogInformation("Tip confirmation email sent to {Email} for tip to {Creator}", email,
creatorUsername);
}