diff --git a/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs b/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
index babc536..5d432e2 100644
--- a/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
+++ b/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
@@ -20,6 +20,7 @@ public class PaymentGatewayConfig
public OpayConfig Opay { get; set; } = new();
public DpoGroupConfig DpoGroup { get; set; } = new();
public PawaPayConfig PawaPay { get; set; } = new();
+ public PeachPaymentsConfig PeachPayments { get; set; } = new();
}
public class PaystackConfig
@@ -156,3 +157,21 @@ public class PawaPayConfig
///
public bool IsSandbox { get; set; } = false;
}
+
+public class PeachPaymentsConfig
+{
+ ///
+ /// Peach Payments Entity ID (from your Peach Payments dashboard).
+ ///
+ public string EntityId { get; set; } = string.Empty;
+
+ ///
+ /// Peach Payments Access Token (Bearer token).
+ ///
+ public string AccessToken { get; set; } = string.Empty;
+
+ ///
+ /// Set to true to use Peach Payments test/sandbox environment.
+ ///
+ public bool IsSandbox { get; set; } = false;
+}
diff --git a/PayBridge.SDK/Enums/PaymentGatewayType.cs b/PayBridge.SDK/Enums/PaymentGatewayType.cs
index a6b2d2e..0628798 100644
--- a/PayBridge.SDK/Enums/PaymentGatewayType.cs
+++ b/PayBridge.SDK/Enums/PaymentGatewayType.cs
@@ -77,5 +77,10 @@ public enum PaymentGatewayType
///
/// PawaPay payment gateway (Africa - mobile money)
///
- PawaPay = 14
+ PawaPay = 14,
+
+ ///
+ /// Peach Payments gateway (South Africa, Kenya, Nigeria)
+ ///
+ PeachPayments = 15
}
\ No newline at end of file
diff --git a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
index da37a56..171e771 100644
--- a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
+++ b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
@@ -139,6 +139,7 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
return;
}
@@ -190,6 +191,9 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
case PaymentGatewayType.PawaPay:
services.AddScoped();
break;
+ case PaymentGatewayType.PeachPayments:
+ services.AddScoped();
+ break;
}
}
}
diff --git a/PayBridge.SDK/Factories/PaymentGatewayFactory.cs b/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
index f99e477..0a0be6a 100644
--- a/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
+++ b/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
@@ -49,6 +49,7 @@ public Dictionary CreateGateways()
TryAddGateway(gateways, PaymentGatewayType.Opay);
TryAddGateway(gateways, PaymentGatewayType.DpoGroup);
TryAddGateway(gateways, PaymentGatewayType.PawaPay);
+ TryAddGateway(gateways, PaymentGatewayType.PeachPayments);
}
else
{
@@ -96,6 +97,7 @@ private void TryAddGateway(Dictionary gatew
PaymentGatewayType.Opay => _serviceProvider.GetService(),
PaymentGatewayType.DpoGroup => _serviceProvider.GetService(),
PaymentGatewayType.PawaPay => _serviceProvider.GetService(),
+ PaymentGatewayType.PeachPayments => _serviceProvider.GetService(),
_ => null
};
diff --git a/PayBridge.SDK/Gateways/PeachPaymentsGateway.cs b/PayBridge.SDK/Gateways/PeachPaymentsGateway.cs
new file mode 100644
index 0000000..5c0f8de
--- /dev/null
+++ b/PayBridge.SDK/Gateways/PeachPaymentsGateway.cs
@@ -0,0 +1,240 @@
+using System.Net.Http.Headers;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using PayBridge.SDK.Application.Dtos;
+using PayBridge.SDK.Application.Dtos.Request;
+using PayBridge.SDK.Dtos.Request;
+using PayBridge.SDK.Dtos.Response;
+using PayBridge.SDK.Enums;
+using PayBridge.SDK.Exceptions;
+using PayBridge.SDK.Interfaces;
+
+namespace PayBridge.SDK;
+
+///
+/// Peach Payments gateway (South Africa, Kenya, Nigeria, Botswana).
+/// Currencies: ZAR, KES, NGN, BWP, USD.
+/// Auth: Bearer AccessToken + EntityId.
+/// Docs: https://developer.peachpayments.com
+///
+public class PeachPaymentsGateway : IPaymentGateway
+{
+ private const string LiveBaseUrl = "https://eu-prod.oppwa.com/v1";
+ private const string SandboxBaseUrl = "https://eu-test.oppwa.com/v1";
+ private const string TxRefPrefix = "PEACH_";
+
+ private readonly PaymentGatewayConfig _config;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ public PeachPaymentsGateway(
+ PaymentGatewayConfig config,
+ IHttpClientFactory httpClientFactory,
+ ILogger logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _httpClient = httpClientFactory.CreateClient(nameof(PeachPaymentsGateway));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public PaymentGatewayType GatewayType => PaymentGatewayType.PeachPayments;
+
+ private string BaseUrl => _config.PeachPayments.IsSandbox ? SandboxBaseUrl : LiveBaseUrl;
+
+ private void SetAuthHeader()
+ {
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue("Bearer", _config.PeachPayments.AccessToken);
+ }
+
+ public async Task CreatePaymentAsync(PaymentRequest request)
+ {
+ try
+ {
+ var txRef = $"{TxRefPrefix}{Guid.NewGuid():N}";
+
+ var formData = new Dictionary
+ {
+ ["entityId"] = _config.PeachPayments.EntityId,
+ ["amount"] = request.Amount.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
+ ["currency"] = request.Currency ?? "ZAR",
+ ["paymentType"] = "DB",
+ ["merchantTransactionId"] = txRef,
+ ["customer.email"] = request.CustomerEmail ?? string.Empty,
+ ["customer.givenName"] = request.CustomerName?.Split(' ').FirstOrDefault() ?? string.Empty,
+ ["customer.surname"] = request.CustomerName?.Split(' ').LastOrDefault() ?? string.Empty,
+ ["billing.country"] = CurrencyToCountry(request.Currency ?? "ZAR"),
+ };
+
+ SetAuthHeader();
+ var content = new FormUrlEncodedContent(formData);
+ var response = await _httpClient.PostAsync($"{BaseUrl}/checkouts", content);
+ var body = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("PeachPayments checkout response: {Body}", body);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ return new PaymentResponse
+ {
+ Success = false,
+ Message = $"PeachPayments checkout failed: {response.StatusCode}",
+ Status = PaymentStatus.Failed
+ };
+ }
+
+ var json = JsonDocument.Parse(body).RootElement;
+ var resultCode = json.TryGetProperty("result", out var resultEl) &&
+ resultEl.TryGetProperty("code", out var codeEl)
+ ? codeEl.GetString() : null;
+
+ var checkoutId = json.TryGetProperty("id", out var idEl) ? idEl.GetString() : null;
+ bool success = resultCode != null && resultCode.StartsWith("000");
+
+ var paymentUrl = checkoutId != null
+ ? $"{BaseUrl}/paymentWidgets.js?checkoutId={checkoutId}"
+ : null;
+
+ return new PaymentResponse
+ {
+ Success = success,
+ TransactionReference = txRef,
+ CheckoutUrl = paymentUrl ?? string.Empty,
+ Message = resultEl.TryGetProperty("description", out var descEl)
+ ? descEl.GetString() ?? "Unknown" : "Unknown",
+ Status = success ? PaymentStatus.Pending : PaymentStatus.Failed,
+ GatewayResponse = new Dictionary
+ {
+ ["checkoutId"] = checkoutId ?? string.Empty,
+ ["resultCode"] = resultCode ?? string.Empty
+ }
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error initiating PeachPayments checkout");
+ throw new PaymentGatewayException("Failed to initiate PeachPayments payment", ex);
+ }
+ }
+
+ public async Task VerifyPaymentAsync(string transactionReference)
+ {
+ try
+ {
+ var url = $"{BaseUrl}/payments?entityId={Uri.EscapeDataString(_config.PeachPayments.EntityId)}" +
+ $"&merchantTransactionId={Uri.EscapeDataString(transactionReference)}";
+
+ SetAuthHeader();
+ var response = await _httpClient.GetAsync(url);
+ var body = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("PeachPayments verify response: {Body}", body);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ return new VerificationResponse
+ {
+ Success = false,
+ TransactionReference = transactionReference,
+ Status = PaymentStatus.Failed,
+ Message = $"PeachPayments verification failed: {response.StatusCode}"
+ };
+ }
+
+ var json = JsonDocument.Parse(body).RootElement;
+ var payment = json.ValueKind == JsonValueKind.Array ? json[0] : json;
+
+ var resultCode = payment.TryGetProperty("result", out var resultEl) &&
+ resultEl.TryGetProperty("code", out var codeEl)
+ ? codeEl.GetString() : null;
+
+ bool success = resultCode != null &&
+ (resultCode.StartsWith("000.0") || resultCode.StartsWith("000.100"));
+
+ decimal.TryParse(
+ payment.TryGetProperty("amount", out var amtEl) ? amtEl.GetString() : "0",
+ System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture,
+ out var amount);
+
+ var currency = payment.TryGetProperty("currency", out var curEl)
+ ? curEl.GetString() ?? string.Empty : string.Empty;
+
+ var paymentBrand = payment.TryGetProperty("paymentBrand", out var pbEl)
+ ? pbEl.GetString() ?? "Card" : "Card";
+
+ return new VerificationResponse
+ {
+ Success = success,
+ TransactionReference = transactionReference,
+ Amount = amount,
+ Currency = currency,
+ Status = success ? PaymentStatus.Successful : PaymentStatus.Failed,
+ Message = resultEl.TryGetProperty("description", out var descEl)
+ ? descEl.GetString() ?? "Unknown" : "Unknown",
+ PaymentMethod = paymentBrand,
+ GatewayResponse = new Dictionary
+ {
+ ["resultCode"] = resultCode ?? string.Empty
+ }
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error verifying PeachPayments payment: {Ref}", transactionReference);
+ throw new PaymentGatewayException("Failed to verify PeachPayments payment", ex);
+ }
+ }
+
+ public async Task RefundPaymentAsync(RefundRequest request)
+ {
+ try
+ {
+ var formData = new Dictionary
+ {
+ ["entityId"] = _config.PeachPayments.EntityId,
+ ["amount"] = request.Amount.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
+ ["currency"] = "ZAR",
+ ["paymentType"] = "RF",
+ };
+
+ SetAuthHeader();
+ var content = new FormUrlEncodedContent(formData);
+ var response = await _httpClient.PostAsync($"{BaseUrl}/payments/{request.TransactionReference}", content);
+ var body = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("PeachPayments refund response: {Body}", body);
+
+ var json = JsonDocument.Parse(body).RootElement;
+ var resultCode = json.TryGetProperty("result", out var resultEl) &&
+ resultEl.TryGetProperty("code", out var codeEl)
+ ? codeEl.GetString() : null;
+
+ bool success = resultCode != null && resultCode.StartsWith("000");
+ var refundId = json.TryGetProperty("id", out var idEl) ? idEl.GetString() ?? string.Empty : string.Empty;
+
+ return new RefundResponse
+ {
+ Success = success,
+ RefundReference = refundId,
+ TransactionReference = request.TransactionReference,
+ Amount = request.Amount,
+ Status = success ? PaymentStatus.Refunded : PaymentStatus.Failed,
+ Message = resultEl.TryGetProperty("description", out var descEl)
+ ? descEl.GetString() ?? "Unknown" : "Unknown"
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing PeachPayments refund: {Ref}", request.TransactionReference);
+ throw new PaymentGatewayException("Failed to process PeachPayments refund", ex);
+ }
+ }
+
+ private static string CurrencyToCountry(string currency) => currency.ToUpperInvariant() switch
+ {
+ "ZAR" => "ZA",
+ "KES" => "KE",
+ "NGN" => "NG",
+ "BWP" => "BW",
+ _ => "ZA"
+ };
+}
diff --git a/PayBridge.SDK/Services/PaymentService.cs b/PayBridge.SDK/Services/PaymentService.cs
index 080c32f..46fc3ed 100644
--- a/PayBridge.SDK/Services/PaymentService.cs
+++ b/PayBridge.SDK/Services/PaymentService.cs
@@ -262,7 +262,10 @@ private PaymentGatewayType SelectBestGateway(PaymentRequest request)
case "XOF":
case "XAF":
case "MWK":
- return ChooseAvailableGateway(PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup, PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);
+ return ChooseAvailableGateway(PaymentGatewayType.PeachPayments, PaymentGatewayType.PawaPay, PaymentGatewayType.DpoGroup, PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);
+
+ case "BWP":
+ return ChooseAvailableGateway(PaymentGatewayType.PeachPayments, PaymentGatewayType.DpoGroup);
case "BHD":
return ChooseAvailableGateway(PaymentGatewayType.BenefitPay);
@@ -367,6 +370,11 @@ private PaymentGatewayType DetermineGatewayFromReference(string transactionRefer
{
return PaymentGatewayType.PawaPay;
}
+
+ if (transactionReference.StartsWith("PEACH_"))
+ {
+ return PaymentGatewayType.PeachPayments;
+ }
// Default to configured default gateway
_logger.LogWarning("Could not determine gateway from reference: {Reference}", transactionReference);
return _config.DefaultGateway;