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, 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 ipValidation = await payfastService.ValidateReferrerIpAsync(remoteIp!, !hostEnvironment.IsProduction(), cancellationToken); if (ipValidation.IsFailed || !ipValidation.Value) return Results.Unauthorized(); 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)) 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) 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 }; }