fix(stripe): correcting webhook
This commit is contained in:
@@ -7,7 +7,7 @@ using Stripe.Checkout;
|
|||||||
|
|
||||||
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||||
|
|
||||||
public class StripeTipProcessor(
|
internal class StripeTipProcessor(
|
||||||
IOptions<StripeOptions> stripeOptions)
|
IOptions<StripeOptions> stripeOptions)
|
||||||
: ITipProcessor
|
: ITipProcessor
|
||||||
{
|
{
|
||||||
@@ -17,61 +17,63 @@ public class StripeTipProcessor(
|
|||||||
decimal amount,
|
decimal amount,
|
||||||
string currency,
|
string currency,
|
||||||
string message,
|
string message,
|
||||||
string successUrl,
|
Uri successUrl,
|
||||||
string cancelUrl,
|
Uri cancelUrl,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||||
|
|
||||||
// Create Stripe customer for the user if not already created
|
// Create Stripe customer for the user if not already created
|
||||||
CustomerService customerService = new();
|
CustomerService customerService = new();
|
||||||
Customer? customer = await customerService.CreateAsync(
|
var customer = await customerService
|
||||||
new CustomerCreateOptions(),
|
.CreateAsync(
|
||||||
cancellationToken: ct);
|
new CustomerCreateOptions(),
|
||||||
|
cancellationToken: ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
// Create paymentIntent for the user
|
// Create paymentIntent for the user
|
||||||
SessionService sessionService = new();
|
SessionService sessionService = new();
|
||||||
Session? session = await sessionService.CreateAsync(
|
var session = await sessionService.CreateAsync(
|
||||||
new SessionCreateOptions
|
new SessionCreateOptions
|
||||||
{
|
|
||||||
ClientReferenceId = tipId.ToString(),
|
|
||||||
Customer = customer.Id,
|
|
||||||
PaymentMethodTypes = ["card"],
|
|
||||||
LineItems =
|
|
||||||
[
|
|
||||||
new SessionLineItemOptions
|
|
||||||
{
|
|
||||||
PriceData = new SessionLineItemPriceDataOptions
|
|
||||||
{
|
|
||||||
Currency = currency,
|
|
||||||
UnitAmountDecimal = amount, // Amount in cents
|
|
||||||
ProductData = new SessionLineItemPriceDataProductDataOptions
|
|
||||||
{
|
|
||||||
Name = $"Tip for {creator.Name}", // or any descriptive name for the tip
|
|
||||||
Metadata = new Dictionary<string, string> { { "creatorId", creator.Id.ToString() } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Quantity = 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Mode = "payment",
|
|
||||||
PaymentIntentData = new SessionPaymentIntentDataOptions
|
|
||||||
{
|
{
|
||||||
ApplicationFeeAmount =
|
ClientReferenceId = tipId.ToString(),
|
||||||
Convert.ToInt64(amount * stripeOptions.Value.HutopyRate), // Platform fee
|
Customer = customer.Id,
|
||||||
TransferData = new SessionPaymentIntentDataTransferDataOptions
|
PaymentMethodTypes = ["card"],
|
||||||
|
LineItems =
|
||||||
|
[
|
||||||
|
new SessionLineItemOptions
|
||||||
|
{
|
||||||
|
PriceData = new SessionLineItemPriceDataOptions
|
||||||
|
{
|
||||||
|
Currency = currency,
|
||||||
|
UnitAmountDecimal = amount, // Amount in cents
|
||||||
|
ProductData = new SessionLineItemPriceDataProductDataOptions
|
||||||
|
{
|
||||||
|
Name = $"Tip for {creator.Name}", // or any descriptive name for the tip
|
||||||
|
Metadata = new Dictionary<string, string> { { "creatorId", creator.Id.ToString() } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Quantity = 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Mode = "payment",
|
||||||
|
PaymentIntentData = new SessionPaymentIntentDataOptions
|
||||||
{
|
{
|
||||||
Destination = creator.StripeAccountId // Creator's Stripe account ID
|
ApplicationFeeAmount = Convert.ToInt64(amount * stripeOptions.Value.HutopyRate), // Platform fee
|
||||||
|
TransferData = new SessionPaymentIntentDataTransferDataOptions
|
||||||
|
{
|
||||||
|
Destination = creator.StripeAccountId // Creator's Stripe account ID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SuccessUrl = successUrl, // Redirect after successful payment
|
cancellationToken: ct)
|
||||||
CancelUrl = cancelUrl, // Redirect after canceled payment
|
.ConfigureAwait(false);
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "creatorId", creator.Id.ToString() }, { "creatorName", creator.Name }, { "message", message }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancellationToken: ct);
|
|
||||||
|
|
||||||
return new TipCheckoutSession(session.Id, session.Url);
|
return new TipCheckoutSession(session.Id, session.Url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Hutopy.Modules.Memberships.Handlers;
|
|||||||
internal class StripeWebhookEndpoint(
|
internal class StripeWebhookEndpoint(
|
||||||
ITipPaymentNotifier tipPaymentNotifier,
|
ITipPaymentNotifier tipPaymentNotifier,
|
||||||
IMembershipNotifier membershipNotifier,
|
IMembershipNotifier membershipNotifier,
|
||||||
IOptions<StripeOptions> options)
|
IOptions<StripeOptions> stripeOptions)
|
||||||
: EndpointWithoutRequest
|
: EndpointWithoutRequest
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -28,7 +28,7 @@ internal class StripeWebhookEndpoint(
|
|||||||
|
|
||||||
var json = await streamReader.ReadToEndAsync(ct).ConfigureAwait(false);
|
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 stripeSession = stripeEvent.Data.Object as Session;
|
||||||
var stripeSubscription = stripeEvent.Data.Object as Subscription;
|
var stripeSubscription = stripeEvent.Data.Object as Subscription;
|
||||||
@@ -46,13 +46,22 @@ internal class StripeWebhookEndpoint(
|
|||||||
stripeSession.Customer?.Email ??
|
stripeSession.Customer?.Email ??
|
||||||
"";
|
"";
|
||||||
|
|
||||||
// Get the receipt URL, preferring the one directly on the charge if available
|
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||||
var receiptUrl = stripeSession.Invoice?.HostedInvoiceUrl ?? "";
|
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
|
await tipPaymentNotifier
|
||||||
.NotifyPaymentSucceedAsync(
|
.NotifyPaymentSucceedAsync(
|
||||||
stripeSession.Id,
|
stripeSession.Id,
|
||||||
receiptUrl,
|
receiptUri,
|
||||||
customerEmail,
|
customerEmail,
|
||||||
ct)
|
ct)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
namespace Hutopy.Modules.Tipping.Contracts;
|
namespace Hutopy.Modules.Tipping.Contracts;
|
||||||
|
|
||||||
public interface ITipPaymentNotifier
|
internal interface ITipPaymentNotifier
|
||||||
{
|
{
|
||||||
Task NotifyPaymentSucceedAsync(
|
Task NotifyPaymentSucceedAsync(
|
||||||
string stripeId,
|
string sessionId,
|
||||||
string invoiceUrl,
|
Uri receiptUrl,
|
||||||
string customerEmail,
|
string customerEmail,
|
||||||
CancellationToken ct);
|
CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using Hutopy.Modules.Creators.Contracts;
|
|||||||
|
|
||||||
namespace Hutopy.Modules.Tipping.Contracts;
|
namespace Hutopy.Modules.Tipping.Contracts;
|
||||||
|
|
||||||
public interface ITipProcessor
|
internal interface ITipProcessor
|
||||||
{
|
{
|
||||||
Task<TipCheckoutSession> CreateCheckoutSessionAsync(
|
Task<TipCheckoutSession> CreateCheckoutSessionAsync(
|
||||||
Guid tipId,
|
Guid tipId,
|
||||||
@@ -10,7 +10,7 @@ public interface ITipProcessor
|
|||||||
decimal amount,
|
decimal amount,
|
||||||
string currency,
|
string currency,
|
||||||
string message,
|
string message,
|
||||||
string successUrl,
|
Uri successUrl,
|
||||||
string cancelUrl,
|
Uri cancelUrl,
|
||||||
CancellationToken ct = default);
|
CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ internal static class SendTip
|
|||||||
decimal Amount,
|
decimal Amount,
|
||||||
string Currency,
|
string Currency,
|
||||||
string Message,
|
string Message,
|
||||||
string CheckoutSuccessUrl,
|
Uri CheckoutSuccessUrl,
|
||||||
string CheckoutCancelledUrl);
|
Uri CheckoutCancelledUrl);
|
||||||
|
|
||||||
internal record Response(
|
internal record Response(
|
||||||
string Id,
|
string Id,
|
||||||
string Url);
|
Uri Url);
|
||||||
|
|
||||||
internal class Validator : Validator<Request>
|
internal class Validator : Validator<Request>
|
||||||
{
|
{
|
||||||
@@ -116,7 +116,7 @@ internal static class SendTip
|
|||||||
await dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
|
await dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
await SendAsync(
|
await SendAsync(
|
||||||
new Response(checkout.Id, checkout.Url),
|
new Response(checkout.Id, new Uri(checkout.Url)),
|
||||||
cancellation: ct)
|
cancellation: ct)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Hutopy.Modules.Tipping.Data;
|
|||||||
|
|
||||||
namespace Hutopy.Modules.Tipping.Services;
|
namespace Hutopy.Modules.Tipping.Services;
|
||||||
|
|
||||||
public class TipPaymentNotifier(
|
internal class TipPaymentNotifier(
|
||||||
TippingDbContext dbContext,
|
TippingDbContext dbContext,
|
||||||
IEmailSender emailSender,
|
IEmailSender emailSender,
|
||||||
ICreatorLookup creatorLookup,
|
ICreatorLookup creatorLookup,
|
||||||
@@ -14,31 +14,40 @@ public class TipPaymentNotifier(
|
|||||||
{
|
{
|
||||||
public async Task NotifyPaymentSucceedAsync(
|
public async Task NotifyPaymentSucceedAsync(
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string receiptUrl,
|
Uri receiptUrl,
|
||||||
string customerEmail,
|
string customerEmail,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
Tip? tip = await dbContext.Tips.SingleOrDefaultAsync(
|
var tip = await dbContext
|
||||||
t => t.StripeSessionId == sessionId,
|
.Tips
|
||||||
ct);
|
.SingleOrDefaultAsync(
|
||||||
|
t => t.StripeSessionId == sessionId,
|
||||||
|
ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (tip is not null)
|
if (tip is not null)
|
||||||
{
|
{
|
||||||
tip.Status = TipStatus.Paid;
|
tip.Status = TipStatus.Paid;
|
||||||
tip.StripeInvoiceUrl = receiptUrl; // Store the receipt URL
|
tip.StripeInvoiceUrl = receiptUrl.ToString(); // Store the receipt URL
|
||||||
await dbContext.SaveChangesAsync(ct);
|
|
||||||
|
await dbContext
|
||||||
|
.SaveChangesAsync(ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
// Look up creator information
|
// 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))
|
if (!string.IsNullOrEmpty(customerEmail))
|
||||||
{
|
{
|
||||||
await SendTipConfirmationEmailAsync(
|
await SendTipConfirmationEmailAsync(
|
||||||
customerEmail,
|
customerEmail,
|
||||||
creator?.Name ?? "le créateur",
|
creator?.Name ?? "le créateur",
|
||||||
tip.Amount,
|
tip.Amount,
|
||||||
tip.Currency,
|
tip.Currency,
|
||||||
receiptUrl); // Pass the receipt URL
|
receiptUrl)
|
||||||
|
.ConfigureAwait(false); // Pass the receipt URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -52,52 +61,53 @@ public class TipPaymentNotifier(
|
|||||||
string creatorUsername,
|
string creatorUsername,
|
||||||
decimal amount,
|
decimal amount,
|
||||||
string currency,
|
string currency,
|
||||||
string receiptUrl) // Add receipt URL parameter
|
Uri receiptUrl) // Add receipt URL parameter
|
||||||
{
|
{
|
||||||
string subject = $"Merci pour votre soutien à {creatorUsername}";
|
var subject = $"Merci pour votre soutien à {creatorUsername}";
|
||||||
string message = $"""
|
var message = $"""
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; color: #333;">
|
<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>
|
<h1 style="color: #2c3e50; margin-bottom: 20px;">{creatorUsername} vous remercie !</h1>
|
||||||
|
|
||||||
<p style="font-size: 16px; line-height: 1.5; margin-bottom: 15px;">
|
<p style="font-size: 16px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
Votre paiement de <strong>{amount} {currency}</strong> a été traité avec succès.
|
Votre paiement de <strong>{amount} {currency}</strong> a été traité avec succès.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="background-color: #f8f9fa; border-radius: 4px; padding: 20px; margin: 30px 0; border-left: 4px solid #3498db;">
|
<div style="background-color: #f8f9fa; border-radius: 4px; padding: 20px; margin: 30px 0; border-left: 4px solid #3498db;">
|
||||||
<p style="font-size: 16px; margin: 0; line-height: 1.5;">
|
<p style="font-size: 16px; margin: 0; line-height: 1.5;">
|
||||||
Ce reçu confirme votre soutien à <strong>{creatorUsername}</strong>. Merci de contribuer à son travail !
|
Ce reçu confirme votre soutien à <strong>{creatorUsername}</strong>. Merci de contribuer à son travail !
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(string.IsNullOrEmpty(receiptUrl) ? "" : $"""
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
<a href='{receiptUrl}'
|
||||||
<a href='{receiptUrl}'
|
style="background-color: #3498db;
|
||||||
style="background-color: #3498db;
|
color: white;
|
||||||
color: white;
|
text-decoration: none;
|
||||||
text-decoration: none;
|
padding: 12px 24px;
|
||||||
padding: 12px 24px;
|
border-radius: 4px;
|
||||||
border-radius: 4px;
|
font-weight: bold;
|
||||||
font-weight: bold;
|
display: inline-block;
|
||||||
display: inline-block;
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
|
Voir le reçu
|
||||||
Voir le reçu
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
""")}
|
|
||||||
|
|
||||||
<p style="font-size: 14px; color: #7f8c8d; margin-top: 30px;">
|
<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.
|
Cet email sert de reçu pour votre transaction. Nous vous conseillons de le conserver pour vos archives.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-size: 14px; color: #7f8c8d; margin-top: 20px; text-align: center; border-top: 1px solid #eee; padding-top: 20px;">
|
<p style="font-size: 14px; color: #7f8c8d; margin-top: 20px; text-align: center; border-top: 1px solid #eee; padding-top: 20px;">
|
||||||
Merci d'utiliser Hutopy pour soutenir vos créateurs préférés !
|
Merci d'utiliser Hutopy pour soutenir vos créateurs préférés !
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try
|
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,
|
logger.LogInformation("Tip confirmation email sent to {Email} for tip to {Creator}", email,
|
||||||
creatorUsername);
|
creatorUsername);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user