Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37e0d3b93f | |||
| 4d2b37ace7 | |||
| b42c0fcc4f | |||
| 3daf192ce9 |
+6
-6
@@ -1,15 +1,16 @@
|
|||||||
using LiteCharms.Features.Hasher;
|
using LiteCharms.Features.Api.Configuration;
|
||||||
using LiteCharms.Features.Hasher.Configuration;
|
using LiteCharms.Features.Hasher;
|
||||||
using LiteCharms.Features.Mediator;
|
using LiteCharms.Features.Mediator;
|
||||||
using LiteCharms.Features.MidrandBooks.Orders;
|
using LiteCharms.Features.MidrandBooks.Orders;
|
||||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers;
|
namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers;
|
||||||
|
|
||||||
public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, IOptions<HasherSettings> hasherOptions, ILogger<PayfastPaymentConfirmationReceivedEvent> logger) :
|
public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services,
|
||||||
|
IOptions<PayfastSettings> payfastOptions, ILogger<PayfastPaymentConfirmationReceivedEvent> logger) :
|
||||||
INotificationHandler<PayfastPaymentConfirmationReceivedEvent>
|
INotificationHandler<PayfastPaymentConfirmationReceivedEvent>
|
||||||
{
|
{
|
||||||
private readonly HasherSettings hasherSettings = hasherOptions.Value;
|
private readonly PayfastSettings pasfastSettings = payfastOptions.Value;
|
||||||
|
|
||||||
public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken)
|
public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -25,7 +26,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi
|
|||||||
var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null.");
|
var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null.");
|
||||||
|
|
||||||
var dict = payload.ToParamDictionary();
|
var dict = payload.ToParamDictionary();
|
||||||
var localSignature = PayfastService.GenerateSignature(dict, hasherSettings.PayfastPassphrase);
|
var localSignature = PayfastService.GenerateSignature(dict, pasfastSettings.Passphrase);
|
||||||
|
|
||||||
if (localSignature.IsFailed)
|
if (localSignature.IsFailed)
|
||||||
throw new Exception("Failed to generate local signature for incoming webhook payload.");
|
throw new Exception("Failed to generate local signature for incoming webhook payload.");
|
||||||
@@ -159,6 +160,5 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi
|
|||||||
logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status);
|
logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status);
|
||||||
}
|
}
|
||||||
activity?.SetStatus(ActivityStatusCode.Ok);
|
activity?.SetStatus(ActivityStatusCode.Ok);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using LiteCharms.Features.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
|
using LiteCharms.Features.Api.Configuration;
|
||||||
using LiteCharms.Features.Hasher;
|
using LiteCharms.Features.Hasher;
|
||||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
@@ -6,13 +7,11 @@ using LiteCharms.Features.MidrandBooks.Postgres;
|
|||||||
namespace LiteCharms.Features.MidrandBooks.Payments;
|
namespace LiteCharms.Features.MidrandBooks.Payments;
|
||||||
|
|
||||||
public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbContext> contextFactory,
|
public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbContext> contextFactory,
|
||||||
ILogger<PayfastService> logger, IHttpClientFactory httpClientFactory, IConfiguration configuration) : IService
|
IOptions<PayfastSettings> payfastOptions, ILogger<PayfastService> logger, IHttpClientFactory httpClientFactory) : IService
|
||||||
{
|
{
|
||||||
[GeneratedRegex(@"%[0-9A-Fa-f]{2}", RegexOptions.None, matchTimeoutMilliseconds: 1000)]
|
[GeneratedRegex(@"%[0-9A-Fa-f]{2}", RegexOptions.None, matchTimeoutMilliseconds: 1000)]
|
||||||
public static partial Regex PercentEncodingRegex { get; }
|
public static partial Regex PercentEncodingRegex { get; }
|
||||||
|
|
||||||
public readonly string[] ValidHosts = configuration.GetSection("ValidPayfastHosts").Get<string[]>() ?? [];
|
|
||||||
|
|
||||||
public async ValueTask<Result<long>> WriteLedgerEntryAsync(CreateGatewayLedgerEntry request, CancellationToken cancellationToken = default)
|
public async ValueTask<Result<long>> WriteLedgerEntryAsync(CreateGatewayLedgerEntry request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -51,6 +50,9 @@ public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbConte
|
|||||||
|
|
||||||
public async ValueTask<Result<bool>> ValidateReferrerIpAsync(string remoteIpAddress, bool allowLoopback = false, CancellationToken cancellationToken = default)
|
public async ValueTask<Result<bool>> ValidateReferrerIpAsync(string remoteIpAddress, bool allowLoopback = false, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if(payfastOptions.Value?.ValidHosts?.Length == 0)
|
||||||
|
return Result.Fail<bool>("Valid payfast hosts not configured.");
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(remoteIpAddress))
|
if (string.IsNullOrWhiteSpace(remoteIpAddress))
|
||||||
return Result.Fail<bool>("Remote IP address is null or whitespace.");
|
return Result.Fail<bool>("Remote IP address is null or whitespace.");
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbConte
|
|||||||
{
|
{
|
||||||
var validIps = new HashSet<IPAddress>();
|
var validIps = new HashSet<IPAddress>();
|
||||||
|
|
||||||
foreach (var host in ValidHosts)
|
foreach (var host in payfastOptions.Value!.ValidHosts!)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class Fixture : IDisposable
|
|||||||
.AddHashServices(Configuration)
|
.AddHashServices(Configuration)
|
||||||
.AddLiteCharmsApiSecurity(Configuration)
|
.AddLiteCharmsApiSecurity(Configuration)
|
||||||
.AddSecurityApiSdk(Configuration)
|
.AddSecurityApiSdk(Configuration)
|
||||||
|
.AddPayfastServices(Configuration)
|
||||||
.BuildServiceProvider(); ;
|
.BuildServiceProvider(); ;
|
||||||
|
|
||||||
Mediator = Services.GetRequiredService<IMediator>();
|
Mediator = Services.GetRequiredService<IMediator>();
|
||||||
|
|||||||
@@ -12,11 +12,7 @@
|
|||||||
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
{
|
{
|
||||||
|
"PayfastSettings": {
|
||||||
|
"CheckoutUrl": "https://sandbox.payfast.co.za/eng/process",
|
||||||
|
"ValidHosts": [
|
||||||
|
"www.payfast.co.za",
|
||||||
|
"sandbox.payfast.co.za",
|
||||||
|
"w1w.payfast.co.za",
|
||||||
|
"w2w.payfast.co.za",
|
||||||
|
"ips.payfast.co.za",
|
||||||
|
"api.payfast.co.za",
|
||||||
|
"payment.payfast.io"
|
||||||
|
]
|
||||||
|
},
|
||||||
"LiteCharmsSettings": {
|
"LiteCharmsSettings": {
|
||||||
"Authority": "https://sts.security.khongisa.co.za",
|
"Authority": "https://sts.security.khongisa.co.za",
|
||||||
"Audience": "midrandbooks-api"
|
"Audience": "midrandbooks-api"
|
||||||
},
|
},
|
||||||
"LiteCharmsClientSettings": {
|
"LiteCharmsClientSettings": {
|
||||||
"Authority": "https://sts.security.khongisa.co.za",
|
"Authority": "https://sts.security.khongisa.co.za",
|
||||||
"GrantType": "client_credentials",
|
"GrantType": "client_credentials",
|
||||||
"Scope": "midrandbooks-api"
|
"Scope": "midrandbooks-api"
|
||||||
},
|
},
|
||||||
"ValidPayfastHosts": [
|
|
||||||
"www.payfast.co.za",
|
|
||||||
"sandbox.payfast.co.za",
|
|
||||||
"w1w.payfast.co.za",
|
|
||||||
"w2w.payfast.co.za",
|
|
||||||
"ips.payfast.co.za",
|
|
||||||
"api.payfast.co.za",
|
|
||||||
"payment.payfast.io"
|
|
||||||
],
|
|
||||||
"HasherSettings": {
|
"HasherSettings": {
|
||||||
"MinHashLength": 11
|
"MinHashLength": 11
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<Using Include="System.Text" />
|
<Using Include="System.Text" />
|
||||||
<Using Include="Mediator" />
|
<Using Include="Mediator" />
|
||||||
<Using Include="Xunit.Abstractions" />
|
<Using Include="Xunit.Abstractions" />
|
||||||
|
<Using Include="Microsoft.Extensions.Options" />
|
||||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<Using Include="Microsoft.Extensions.Configuration" />
|
<Using Include="Microsoft.Extensions.Configuration" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using LiteCharms.Features.Api.Configuration;
|
||||||
|
using LiteCharms.Features.Tests.Common;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
|
public sealed class PayfastFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||||
|
{
|
||||||
|
private readonly PayfastSettings payfastSettings = fixture.Services.GetRequiredService<IOptions<PayfastSettings>>().Value;
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public void PayfastSettings_ShouldFail_IfNotLoaded()
|
||||||
|
{
|
||||||
|
Assert.NotEmpty(payfastSettings.CheckoutUrl!);
|
||||||
|
Assert.NotEmpty(payfastSettings.MerchantId!);
|
||||||
|
Assert.NotEmpty(payfastSettings.MerchantKey!);
|
||||||
|
Assert.NotEmpty(payfastSettings.Passphrase!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace LiteCharms.Features.Api.Configuration;
|
||||||
|
|
||||||
|
public sealed class PayfastSettings
|
||||||
|
{
|
||||||
|
public string? CheckoutUrl { get; set; }
|
||||||
|
|
||||||
|
public string? Passphrase { get; set; }
|
||||||
|
|
||||||
|
public string? MerchantId { get; set; }
|
||||||
|
|
||||||
|
public string? MerchantKey { get; set; }
|
||||||
|
|
||||||
|
public string[]? ValidHosts { get; set; }
|
||||||
|
}
|
||||||
@@ -9,6 +9,15 @@ public static class Api
|
|||||||
{
|
{
|
||||||
public const string Books = nameof(Books);
|
public const string Books = nameof(Books);
|
||||||
public const string Payments = nameof(Payments);
|
public const string Payments = nameof(Payments);
|
||||||
|
|
||||||
|
public static IServiceCollection AddPayfastServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var configSection = configuration.GetSection(nameof(PayfastSettings));
|
||||||
|
|
||||||
|
services.Configure<PayfastSettings>(configSection);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user