From 5eb6dbc8b2e58e38cbaf5217a9f92a4341d9f1f9 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 16:36:33 +0200 Subject: [PATCH] Refactored shasher payfast confirmation response handling --- .../HashServiceFeatureTests.cs | 15 +++-- LiteCharms.Features/Extensions/Hash.cs | 62 +++++++++++++++++++ LiteCharms.Features/Hasher/HashService.cs | 25 +++++--- .../Models/PayfastWebhookPayload.cs | 8 +++ 4 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 LiteCharms.Features/Models/PayfastWebhookPayload.cs diff --git a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs index 1515107..6d85422 100644 --- a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs +++ b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs @@ -1,4 +1,6 @@ using LiteCharms.Features.Hasher; +using LiteCharms.Features.Models; +using static LiteCharms.Features.Extensions.Hash; namespace LiteCharms.Features.Tests; @@ -65,17 +67,18 @@ public class HashServiceFeatureTests(Fixture fixture) : IClassFixture { var paymentId = hashService.HashEncodeLongId(1001).Value; - var incomingForm = new Dictionary + var payload = new PayfastWebhookPayload { - { "m_payment_id", paymentId }, - { "amount", "350.00" }, - { "item_name", "System Architecture Book" } + Amount = "350.00", + ItemName = "System Architecture Book", + MPaymentId = paymentId, }; - var rawPayload = $"amount=350.00&item_name=System+Architecture+Book&m_payment_id={paymentId}&passphrase={payfastPassphrase}"; + var rawPayload = payload.ToRawPayfastPayload(payfastPassphrase); + var generatedSignature = HashService.ToMd5Hash(rawPayload).Value; - var result = hashService.VerifyPayfastWebhookSignature(incomingForm, generatedSignature); + var result = hashService.VerifyPayfastWebhookSignature(payload, generatedSignature); Assert.True(result.IsSuccess); Assert.True(result.Value); diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index 6aa95ed..0c17d85 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.Hasher; using LiteCharms.Features.Hasher.Configuration; +using LiteCharms.Features.Models; namespace LiteCharms.Features.Extensions; @@ -20,4 +21,65 @@ public static class Hash return services; } + + public static string ToRawPayfastPayload(this PayfastWebhookPayload input, string passphrase) + { + var parameters = new List(); + + if (!string.IsNullOrWhiteSpace(input.Amount)) + parameters.Add($"amount={WebUtility.UrlEncode(input.Amount)}"); + + if (!string.IsNullOrWhiteSpace(input.ItemName)) + parameters.Add($"item_name={WebUtility.UrlEncode(input.ItemName)}"); + + if (!string.IsNullOrWhiteSpace(input.MPaymentId)) + parameters.Add($"m_payment_id={WebUtility.UrlEncode(input.MPaymentId)}"); + + string payload = string.Join("&", parameters); + + if (!string.IsNullOrWhiteSpace(passphrase)) + payload += $"&passphrase={WebUtility.UrlEncode(passphrase)}"; + + return payload; + } + + public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this string rawPayload) + { + string passphrase = string.Empty; + var payload = new PayfastWebhookPayload(); + + if (string.IsNullOrWhiteSpace(rawPayload)) return (payload, passphrase); + + var segments = rawPayload.Split('&', StringSplitOptions.RemoveEmptyEntries); + + foreach (var segment in segments) + { + int delimiterIndex = segment.IndexOf('='); + if (delimiterIndex == -1) + continue; + + string key = segment[..delimiterIndex].Trim(); + string rawValue = segment[(delimiterIndex + 1)..]; + + string decodedValue = WebUtility.UrlDecode(rawValue); + + switch (key.ToLowerInvariant()) + { + case "amount": + payload.Amount = decodedValue; + break; + case "item_name": + payload.ItemName = decodedValue; + break; + case "m_payment_id": + payload.MPaymentId = decodedValue; + break; + case "passphrase": + passphrase = decodedValue; + break; + } + } + + return (payload, passphrase); + } } \ No newline at end of file diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs index bbe4a47..1b38ed7 100644 --- a/LiteCharms.Features/Hasher/HashService.cs +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.Abstractions; using LiteCharms.Features.Hasher.Configuration; +using LiteCharms.Features.Models; namespace LiteCharms.Features.Hasher; @@ -40,24 +41,30 @@ public sealed partial class HashService(IHashids hasher, IOptions VerifyPayfastWebhookSignature(IDictionary incomingFormData, string incomingSignature) + public Result VerifyPayfastWebhookSignature(PayfastWebhookPayload payload, string incomingSignature) { try { if (string.IsNullOrWhiteSpace(incomingSignature)) return Result.Fail("Validation failed: Missing signature string parameter."); - var sortedFields = incomingFormData - .Where(field => !string.Equals(field.Key, "signature", StringComparison.OrdinalIgnoreCase)) - .OrderBy(field => field.Key, StringComparer.Ordinal) - .Select(field => $"{field.Key}={WebUtility.UrlEncode(field.Value)}"); + var parameters = new List(); - string payload = string.Join("&", sortedFields); + if (!string.IsNullOrWhiteSpace(payload.Amount)) + parameters.Add($"amount={WebUtility.UrlEncode(payload.Amount)}"); + + if (!string.IsNullOrWhiteSpace(payload.ItemName)) + parameters.Add($"item_name={WebUtility.UrlEncode(payload.ItemName)}"); + + if (!string.IsNullOrWhiteSpace(payload.MPaymentId)) + parameters.Add($"m_payment_id={WebUtility.UrlEncode(payload.MPaymentId)}"); + + string signatureString = string.Join("&", parameters); if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase)) - payload += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}"; + signatureString += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}"; - var localHashResult = ToMd5Hash(payload); + var localHashResult = ToMd5Hash(signatureString); if (!localHashResult.IsSuccess) return Result.Fail(localHashResult.Errors); @@ -68,7 +75,7 @@ public sealed partial class HashService(IHashids hasher, IOptions(new Error("An error occurred during MD5 verification loop.").CausedBy(ex)); + return Result.Fail(new Error("An error occurred during Payfast MD5 verification.").CausedBy(ex)); } } diff --git a/LiteCharms.Features/Models/PayfastWebhookPayload.cs b/LiteCharms.Features/Models/PayfastWebhookPayload.cs new file mode 100644 index 0000000..6b0a5db --- /dev/null +++ b/LiteCharms.Features/Models/PayfastWebhookPayload.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Models; + +public sealed class PayfastWebhookPayload +{ + public string? Amount { get; set; } + public string? ItemName { get; set; } + public string? MPaymentId { get; set; } +} -- 2.47.3