feat: use stripe sdk to create intent

This commit is contained in:
Stevan Freeborn
2025-12-27 08:38:31 -06:00
parent fe3928bbc7
commit 2df6bff9a7
5 changed files with 178 additions and 19 deletions
+5
View File
@@ -23,6 +23,11 @@ insert_final_newline = false
#### .NET Coding Conventions #### #### .NET Coding Conventions ####
[*.{cs,vb}] [*.{cs,vb}]
# Analyzer severity levels
dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.IDE0100.severity = none
dotnet_diagnostic.IDE3000.severity = none
# Organize usings # Organize usings
dotnet_separate_import_directive_groups = true dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true dotnet_sort_system_directives_first = true
+130 -17
View File
@@ -1,7 +1,27 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Microsoft.Extensions.Options;
using Stripe;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
builder.Services.AddValidation();
builder.Services.AddProblemDetails();
builder.Services.AddHttpClient();
builder.Services.ConfigureOptions<StripeOptionsSetup>();
builder.Services.AddSingleton<IStripeService, StripeService>();
builder.Services.ConfigureHttpJsonOptions(
static options => options.SerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
);
var app = builder.Build(); var app = builder.Build();
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
@@ -9,26 +29,119 @@ if (app.Environment.IsDevelopment())
app.MapOpenApi(); app.MapOpenApi();
} }
// We need a /create-payment-intent endpoint
app.MapPost("/create-payment-intent", static () =>
{
// we need to get the amount and email
// from the frontend
// we need to validate the amount and email
// we are going to use the Stripe API to create
// a payment intent
// return the secret for that intent
// to the frontend
return Results.Ok("Hello");
});
app.UseDefaultFiles(); app.UseDefaultFiles();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseStatusCodePages();
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.MapPost("/create-payment-intent", CreatePaymentIntentHandler);
app.Run(); app.Run();
static async Task<IResult> CreatePaymentIntentHandler(
CreatePaymentIntentRequest request,
IStripeService stripeService
)
{
var validationErrors = request.Validate(new ValidationContext(request));
if (validationErrors.Any())
{
return Results.ValidationProblem(validationErrors
.GroupBy(static e => e.MemberNames.FirstOrDefault() ?? string.Empty)
.ToDictionary(static g => g.Key, static g => g.Select(static e => e.ErrorMessage ?? string.Empty).ToArray()));
}
var (isSuccess, intent) = await stripeService.CreatePaymentIntentAsync(request.Amount, request.Email);
if (isSuccess is false)
{
return Results.InternalServerError();
}
return Results.Ok(intent);
}
internal sealed record CreatePaymentIntentRequest(decimal Amount, string? Email)
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Amount <= 0)
{
yield return new ValidationResult("Amount must be greater than zero", [nameof(Amount)]);
}
if (string.IsNullOrWhiteSpace(Email) is false && new EmailAddressAttribute().IsValid(Email) is false)
{
yield return new ValidationResult("Email is not valid", [nameof(Email)]);
}
}
}
internal sealed record StripeOptions
{
public string ApiKey { get; init; } = string.Empty;
}
internal sealed record StripeOptionsSetup : IConfigureOptions<StripeOptions>
{
private const string SectionName = nameof(StripeOptions);
private readonly IConfiguration _configuration;
public StripeOptionsSetup(IConfiguration configuration)
{
_configuration = configuration;
}
public void Configure(StripeOptions options)
{
_configuration.GetSection(SectionName).Bind(options);
}
}
internal sealed record Intent(string ClientSecret);
internal interface IStripeService
{
Task<(bool IsSuccess, Intent Intent)> CreatePaymentIntentAsync(decimal amount, string? email);
}
internal sealed class StripeService(
IOptions<StripeOptions> options,
HttpClient httpClient
) : IStripeService
{
private readonly StripeClient _client = new(options.Value.ApiKey, httpClient: new SystemNetHttpClient(httpClient));
public async Task<(bool IsSuccess, Intent Intent)> CreatePaymentIntentAsync(decimal amount, string? email)
{
try
{
var createOptions = new PaymentIntentCreateOptions
{
Amount = (long)(amount * 100),
Currency = "usd",
AutomaticPaymentMethods = new PaymentIntentAutomaticPaymentMethodsOptions
{
Enabled = true,
},
};
if (string.IsNullOrWhiteSpace(email) is false)
{
createOptions.ReceiptEmail = email;
}
var intent = await _client.V1.PaymentIntents.CreateAsync(createOptions);
return (true, new Intent(intent.ClientSecret));
}
catch (Exception)
{
// TODO: Log exception
return (false, new Intent(string.Empty));
}
}
}
@@ -0,0 +1,5 @@
{
"StripeOptions": {
"ApiKey": "ApiKey"
}
}
+2
View File
@@ -0,0 +1,2 @@
[*.cs]
dotnet_diagnostic.CA1707.severity = none
@@ -1,5 +1,6 @@
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using GroundsForSupport.API.Tests.Integration.Infra; using GroundsForSupport.API.Tests.Integration.Infra;
namespace GroundsForSupport.API.Tests.Integration; namespace GroundsForSupport.API.Tests.Integration;
@@ -13,7 +14,8 @@ public sealed class PaymentTests(TestApi api) : IClassFixture<TestApi>
{ {
var client = _api.CreateClient(); var client = _api.CreateClient();
var request = new { var request = new
{
amount = 0, amount = 0,
email = string.Empty, email = string.Empty,
}; };
@@ -22,4 +24,36 @@ public sealed class PaymentTests(TestApi api) : IClassFixture<TestApi>
response.StatusCode.Should().Be(HttpStatusCode.BadRequest); response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
} }
[Fact]
public async Task CreatePaymentIntent_WhenGivenInvalidEmail_ItShouldReturnBadRequest()
{
var client = _api.CreateClient();
var request = new
{
amount = 5000,
email = "invalid-email",
};
var response = await client.PostAsJsonAsync("/create-payment-intent", request, TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
[Fact]
public async Task CreatePaymentIntent_WhenGivenValidRequest_ItShouldReturnCreated()
{
var client = _api.CreateClient();
var request = new
{
amount = 5000,
email = "test@test.com",
};
var response = await client.PostAsJsonAsync("/create-payment-intent", request, TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
} }