feat: use stripe sdk to create intent
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user