102 lines
5.6 KiB
C#
102 lines
5.6 KiB
C#
using LiteCharms.Features.Abstractions;
|
|
using LiteCharms.Features.Api;
|
|
using LiteCharms.Features.Extensions;
|
|
using LiteCharms.Features.MidrandBooks.Payments;
|
|
using LiteCharms.Features.MidrandBooks.Payments.Events;
|
|
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
|
using static LiteCharms.Features.Extensions.Api;
|
|
|
|
namespace MidrandBooksApi.Payments.Payfast;
|
|
|
|
[ApiVersionTarget(1)]
|
|
public sealed class PayfastConfirmationEndpoint : IEndpoint
|
|
{
|
|
private static readonly ActivitySource PaymentActivitySource = new("MidrandBooksApi.Payments");
|
|
|
|
public void Map(IEndpointRouteBuilder builder)
|
|
{
|
|
builder.MapPost("payments/payfast/confirm", async (HttpRequest request, PayfastService payfastService,
|
|
IJobOrchestrator jobOrchestrator, IConfiguration configuration, IHostEnvironment hostEnvironment,
|
|
ILogger<PayfastConfirmationEndpoint> logger, CancellationToken cancellationToken) =>
|
|
{
|
|
using Activity? activity = PaymentActivitySource.StartActivity("ReceivePayfastWebhook", ActivityKind.Server);
|
|
|
|
activity?.SetTag("messaging.system", "payfast");
|
|
activity?.SetTag("messaging.destination.name", "payments/payfast/confirm");
|
|
|
|
string? remoteIp = request.HttpContext.Connection.RemoteIpAddress?.ToString();
|
|
|
|
var formCollection = await request.ReadFormAsync(cancellationToken);
|
|
|
|
if (!formCollection.TryGetValue("signature", out var signatureValues) || string.IsNullOrWhiteSpace(signatureValues.ToString()))
|
|
return Results.BadRequest("Missing Payfast validation signature.");
|
|
|
|
string incomingSignature = signatureValues.ToString().Trim();
|
|
var payload = ParseForm(formCollection, incomingSignature);
|
|
|
|
var paramDictionary = payload.ToParamDictionary();
|
|
string? passphrase = configuration["HasherSettings:PayfastPassphrase"];
|
|
|
|
var signatureCheck = PayfastService.GenerateSignature(paramDictionary, passphrase);
|
|
|
|
if (signatureCheck.IsFailed || !string.Equals(signatureCheck.Value, incomingSignature, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
logger.LogCritical($"Incoming sugnature failed validation: {incomingSignature}, {signatureCheck.Errors.Select(e => e.Message).ToList()}");
|
|
|
|
return Results.Unauthorized();
|
|
}
|
|
|
|
var formPairs = formCollection.Select(kvp => $"{kvp.Key}={HttpUtility.UrlEncode(kvp.Value.ToString())}");
|
|
|
|
string rawQueryParamString = string.Join("&", formPairs);
|
|
|
|
bool isSandbox = !hostEnvironment.IsProduction();
|
|
|
|
var serverConfirmation = await payfastService.ValidateServerConfirmationAsync(rawQueryParamString, isSandbox, cancellationToken);
|
|
|
|
if (serverConfirmation.IsFailed || !serverConfirmation.Value)
|
|
{
|
|
logger.LogCritical($"Server confirmation failed: {rawQueryParamString}, {serverConfirmation.Errors.Select(e => e.Message).ToList()}");
|
|
|
|
return Results.Unauthorized();
|
|
}
|
|
|
|
var notification = PayfastPaymentConfirmationReceivedEvent.Create(payload, payload.MerchantPaymentId!,
|
|
allowLoopback: !hostEnvironment.IsProduction(), performBackgroundChecks: false);
|
|
|
|
await jobOrchestrator.SendAsync(notification, cancellationToken);
|
|
|
|
activity?.SetStatus(ActivityStatusCode.Ok);
|
|
|
|
return Results.Ok();
|
|
})
|
|
.WithDescription("Securely confirm and process an incoming Payfast merchant payment callback.")
|
|
.WithName(typeof(PayfastConfirmationEndpoint).ToEndpointName())
|
|
.MapToApiVersion(new ApiVersion(1))
|
|
.Produces(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status401Unauthorized)
|
|
.WithTags(Api.Payments);
|
|
}
|
|
|
|
private static PayfastWebhookPayload ParseForm(IFormCollection formCollection, string incomingSignature) => new()
|
|
{
|
|
MerchantId = formCollection.TryGetValue("merchant_id", out var mId) ? mId.ToString() : null,
|
|
MerchantKey = formCollection.TryGetValue("merchant_key", out var mKey) ? mKey.ToString() : null,
|
|
Signature = incomingSignature,
|
|
MerchantPaymentId = formCollection.TryGetValue("m_payment_id", out var mPayId) ? mPayId.ToString() : null,
|
|
PaymentId = formCollection.TryGetValue("pf_payment_id", out var pfPayId) ? pfPayId.ToString() : null,
|
|
PaymentStatus = formCollection.TryGetValue("payment_status", out var status) ? status.ToString() : null,
|
|
ItemName = formCollection.TryGetValue("item_name", out var item) ? item.ToString() : null,
|
|
ItemDescription = formCollection.TryGetValue("item_description", out var desc) ? desc.ToString() : null,
|
|
AmountGross = formCollection.TryGetValue("amount_gross", out var gross) ? gross.ToString() : null,
|
|
AmountFee = formCollection.TryGetValue("amount_fee", out var fee) ? fee.ToString() : null,
|
|
AmountNet = formCollection.TryGetValue("amount_net", out var net) ? net.ToString() : null,
|
|
NameFirst = formCollection.TryGetValue("name_first", out var first) ? first.ToString() : null,
|
|
NameLast = formCollection.TryGetValue("name_last", out var last) ? last.ToString() : null,
|
|
EmailAddress = formCollection.TryGetValue("email_address", out var email) ? email.ToString() : null,
|
|
CustomStr1 = formCollection.TryGetValue("custom_str1", out var cStr1) ? cStr1.ToString() : null,
|
|
CustomInt1 = formCollection.TryGetValue("custom_int1", out var cInt1) ? cInt1.ToString() : null,
|
|
Token = formCollection.TryGetValue("token", out var tok) ? tok.ToString() : null
|
|
};
|
|
} |