diff --git a/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs b/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
index dae6b64..627884e 100644
--- a/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
+++ b/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
@@ -17,6 +17,7 @@ public class PaymentGatewayConfig
public KorapayConfig Korapay { get; set; } = new();
public InterswitchConfig Interswitch { get; set; } = new();
public RemitaConfig Remita { get; set; } = new();
+ public OpayConfig Opay { get; set; } = new();
}
public class PaystackConfig
@@ -112,5 +113,13 @@ public class RemitaConfig
public string MerchantId { get; set; } = string.Empty;
public string ServiceTypeId { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
- public bool IsSandbox { get; set; } = true;
+ public bool IsSandbox { get; set; } = false;
+}
+
+public class OpayConfig
+{
+ public string MerchantId { get; set; } = string.Empty;
+ public string PublicKey { get; set; } = string.Empty;
+ public string SecretKey { get; set; } = string.Empty;
+ public bool IsSandbox { get; set; } = false;
}
diff --git a/PayBridge.SDK/Enums/PaymentGatewayType.cs b/PayBridge.SDK/Enums/PaymentGatewayType.cs
index 6de06d3..71f0264 100644
--- a/PayBridge.SDK/Enums/PaymentGatewayType.cs
+++ b/PayBridge.SDK/Enums/PaymentGatewayType.cs
@@ -62,5 +62,10 @@ public enum PaymentGatewayType
///
/// Remita payment gateway (Nigeria)
///
- Remita = 11
+ Remita = 11,
+
+ ///
+ /// OPay payment gateway (Nigeria / Africa)
+ ///
+ Opay = 12
}
\ No newline at end of file
diff --git a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
index 57382d1..8878660 100644
--- a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
+++ b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
@@ -3,7 +3,6 @@
using Microsoft.Extensions.DependencyInjection;
using PayBridge.SDK.Application.Dtos;
using PayBridge.SDK.Enums;
-using PayBridge.SDK.Gateways;
using PayBridge.SDK.Interfaces;
namespace PayBridge.SDK;
@@ -137,6 +136,7 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
services.AddScoped(sp => sp.GetRequiredService());
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
return;
}
@@ -179,6 +179,9 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
case PaymentGatewayType.Remita:
services.AddScoped();
break;
+ case PaymentGatewayType.Opay:
+ services.AddScoped();
+ break;
}
}
}
diff --git a/PayBridge.SDK/Factories/PaymentGatewayFactory.cs b/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
index e848118..eac0466 100644
--- a/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
+++ b/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
@@ -2,7 +2,6 @@
using Microsoft.Extensions.Logging;
using PayBridge.SDK.Application.Dtos;
using PayBridge.SDK.Enums;
-using PayBridge.SDK.Gateways;
using PayBridge.SDK.Interfaces;
namespace PayBridge.SDK;
@@ -47,6 +46,7 @@ public Dictionary CreateGateways()
TryAddGateway(gateways, PaymentGatewayType.Korapay);
TryAddGateway(gateways, PaymentGatewayType.Interswitch);
TryAddGateway(gateways, PaymentGatewayType.Remita);
+ TryAddGateway(gateways, PaymentGatewayType.Opay);
}
else
{
@@ -91,6 +91,7 @@ private void TryAddGateway(Dictionary gatew
PaymentGatewayType.Korapay => _serviceProvider.GetService(),
PaymentGatewayType.Interswitch => _serviceProvider.GetService(),
PaymentGatewayType.Remita => _serviceProvider.GetService(),
+ PaymentGatewayType.Opay => _serviceProvider.GetService(),
_ => null
};
diff --git a/PayBridge.SDK/Gateways/OpayGateway.cs b/PayBridge.SDK/Gateways/OpayGateway.cs
new file mode 100644
index 0000000..145e633
--- /dev/null
+++ b/PayBridge.SDK/Gateways/OpayGateway.cs
@@ -0,0 +1,246 @@
+using System.Security.Cryptography;
+using System.Text;
+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;
+
+///
+/// OPay payment gateway implementation (Nigeria / Africa).
+/// Supports card, bank transfer, USSD, and mobile money payments.
+///
+public class OpayGateway : IPaymentGateway
+{
+ private readonly PaymentGatewayConfig _config;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ private const string LiveBaseUrl = "https://cashierapi.opayweb.com/api/v3";
+ private const string SandboxBaseUrl = "https://sandboxapi.opaycheckout.com/api/v3";
+ private const string TxRefPrefix = "OP_";
+
+ public OpayGateway(
+ PaymentGatewayConfig config,
+ IHttpClientFactory httpClientFactory,
+ ILogger logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _httpClient = httpClientFactory.CreateClient(nameof(OpayGateway));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public PaymentGatewayType GatewayType => PaymentGatewayType.Opay;
+
+ private string BaseUrl => _config.Opay.IsSandbox ? SandboxBaseUrl : LiveBaseUrl;
+
+ ///
+ /// Generates HMAC-SHA512 signature: HMAC512(jsonBody, secretKey)
+ ///
+ private string GenerateSignature(string jsonBody)
+ {
+ var keyBytes = Encoding.UTF8.GetBytes(_config.Opay.SecretKey);
+ var msgBytes = Encoding.UTF8.GetBytes(jsonBody);
+ var hash = HMACSHA512.HashData(keyBytes, msgBytes);
+ return Convert.ToHexString(hash).ToLower();
+ }
+
+ public async Task CreatePaymentAsync(PaymentRequest request)
+ {
+ try
+ {
+ var txRef = $"{TxRefPrefix}{Guid.NewGuid():N}";
+ var amountKobo = (long)(request.Amount * 100);
+
+ var payload = new
+ {
+ merchantId = _config.Opay.MerchantId,
+ reference = txRef,
+ amount = new { total = amountKobo, currency = request.Currency ?? "NGN" },
+ returnUrl = request.RedirectUrl ?? string.Empty,
+ callbackUrl = request.WebhookUrl ?? string.Empty,
+ cancelUrl = request.RedirectUrl ?? string.Empty,
+ expireAt = 30,
+ userInfo = new
+ {
+ userEmail = request.CustomerEmail,
+ userName = request.CustomerName ?? string.Empty,
+ userMobile = request.CustomerPhone ?? string.Empty
+ },
+ product = new
+ {
+ name = request.Description ?? "Payment",
+ description = request.Description ?? "Payment"
+ }
+ };
+
+ var json = JsonSerializer.Serialize(payload);
+ var signature = GenerateSignature(json);
+
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {_config.Opay.PublicKey}");
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("MerchantId", _config.Opay.MerchantId);
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Signature", signature);
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
+
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync($"{BaseUrl}/international/cashier/create", content);
+
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("OPay create response: {Body}", responseBody);
+
+ var doc = JsonDocument.Parse(responseBody);
+ var root = doc.RootElement;
+
+ var code = root.TryGetProperty("code", out var c) ? c.GetString() : null;
+
+ if (code != "00000")
+ {
+ var msg = root.TryGetProperty("message", out var m) ? m.GetString() : "Unknown error";
+ return new PaymentResponse
+ {
+ Success = false,
+ Message = $"OPay payment initiation failed: {msg}",
+ Status = PaymentStatus.Failed
+ };
+ }
+
+ var data = root.GetProperty("data");
+ var cashierUrl = data.TryGetProperty("cashierUrl", out var cu) ? cu.GetString() : string.Empty;
+
+ return new PaymentResponse
+ {
+ Success = true,
+ TransactionReference = txRef,
+ CheckoutUrl = cashierUrl ?? string.Empty,
+ Message = "Payment initiated successfully",
+ Status = PaymentStatus.Pending,
+ GatewayResponse = new Dictionary
+ {
+ ["reference"] = txRef,
+ ["cashierUrl"] = cashierUrl ?? string.Empty
+ }
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error initiating OPay payment");
+ throw new PaymentGatewayException("Failed to initiate OPay payment", ex);
+ }
+ }
+
+ public async Task VerifyPaymentAsync(string transactionReference)
+ {
+ try
+ {
+ var payload = new
+ {
+ merchantId = _config.Opay.MerchantId,
+ reference = transactionReference
+ };
+
+ var json = JsonSerializer.Serialize(payload);
+ var signature = GenerateSignature(json);
+
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {_config.Opay.PublicKey}");
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("MerchantId", _config.Opay.MerchantId);
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Signature", signature);
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
+
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync($"{BaseUrl}/international/cashier/query", content);
+
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("OPay verify response: {Body}", responseBody);
+
+ var doc = JsonDocument.Parse(responseBody);
+ var root = doc.RootElement;
+
+ var code = root.TryGetProperty("code", out var c) ? c.GetString() : null;
+ var data = code == "00000" && root.TryGetProperty("data", out var d) ? d : (JsonElement?)null;
+
+ // OPay statuses: SUCCESS, PENDING, FAIL, CLOSE
+ var status = data?.TryGetProperty("status", out var st) == true ? st.GetString() : null;
+ var amountKobo = data?.TryGetProperty("amount", out var am) == true ? am.GetInt64() : 0L;
+ var currency = data?.TryGetProperty("currency", out var cur) == true ? cur.GetString() : "NGN";
+
+ var paymentStatus = status == "SUCCESS" ? PaymentStatus.Successful
+ : status == "PENDING" ? PaymentStatus.Pending
+ : PaymentStatus.Failed;
+
+ return new VerificationResponse
+ {
+ Success = status == "SUCCESS",
+ TransactionReference = transactionReference,
+ Amount = amountKobo / 100m,
+ Currency = currency ?? "NGN",
+ Status = paymentStatus,
+ Message = status == "SUCCESS" ? "Payment verified successfully" : $"Payment status: {status}",
+ GatewayResponse = new Dictionary { ["status"] = status ?? string.Empty }
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error verifying OPay payment: {Ref}", transactionReference);
+ throw new PaymentGatewayException("Failed to verify OPay payment", ex);
+ }
+ }
+
+ public async Task RefundPaymentAsync(RefundRequest request)
+ {
+ try
+ {
+ var payload = new
+ {
+ merchantId = _config.Opay.MerchantId,
+ reference = request.TransactionReference,
+ amount = new { total = (long)(request.Amount * 100), currency = "NGN" },
+ reason = request.Reason ?? "Customer request"
+ };
+
+ var json = JsonSerializer.Serialize(payload);
+ var signature = GenerateSignature(json);
+
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {_config.Opay.PublicKey}");
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("MerchantId", _config.Opay.MerchantId);
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Signature", signature);
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
+
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync($"{BaseUrl}/international/cashier/refund", content);
+
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("OPay refund response: {Body}", responseBody);
+
+ var doc = JsonDocument.Parse(responseBody);
+ var root = doc.RootElement;
+
+ var code = root.TryGetProperty("code", out var c) ? c.GetString() : null;
+ var isSuccess = code == "00000";
+ var msg = root.TryGetProperty("message", out var m) ? m.GetString() : null;
+
+ return new RefundResponse
+ {
+ Success = isSuccess,
+ RefundReference = request.TransactionReference,
+ TransactionReference = request.TransactionReference,
+ Amount = request.Amount,
+ Message = isSuccess ? "Refund initiated successfully" : (msg ?? "Refund failed"),
+ Status = isSuccess ? PaymentStatus.Refunded : PaymentStatus.Failed
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing OPay refund: {Ref}", request.TransactionReference);
+ throw new PaymentGatewayException("Failed to process OPay refund", ex);
+ }
+ }
+}
diff --git a/PayBridge.SDK/Gateways/RemitaGateway.cs b/PayBridge.SDK/Gateways/RemitaGateway.cs
index 877804c..024351d 100644
--- a/PayBridge.SDK/Gateways/RemitaGateway.cs
+++ b/PayBridge.SDK/Gateways/RemitaGateway.cs
@@ -11,7 +11,7 @@
using PayBridge.SDK.Exceptions;
using PayBridge.SDK.Interfaces;
-namespace PayBridge.SDK.Gateways;
+namespace PayBridge.SDK;
///
/// Remita payment gateway implementation for Nigerian payments.
diff --git a/PayBridge.SDK/Services/PaymentService.cs b/PayBridge.SDK/Services/PaymentService.cs
index caedd97..b7a5f5f 100644
--- a/PayBridge.SDK/Services/PaymentService.cs
+++ b/PayBridge.SDK/Services/PaymentService.cs
@@ -249,7 +249,7 @@ private PaymentGatewayType SelectBestGateway(PaymentRequest request)
switch (request.Currency?.ToUpper())
{
case "NGN":
- return ChooseAvailableGateway(PaymentGatewayType.Monnify, PaymentGatewayType.Squad, PaymentGatewayType.Korapay, PaymentGatewayType.Interswitch, PaymentGatewayType.Remita, PaymentGatewayType.Paystack, PaymentGatewayType.Flutterwave);
+ return ChooseAvailableGateway(PaymentGatewayType.Monnify, PaymentGatewayType.Squad, PaymentGatewayType.Korapay, PaymentGatewayType.Interswitch, PaymentGatewayType.Remita, PaymentGatewayType.Opay, PaymentGatewayType.Paystack, PaymentGatewayType.Flutterwave);
case "KES":
case "GHS":
@@ -347,6 +347,10 @@ private PaymentGatewayType DetermineGatewayFromReference(string transactionRefer
return PaymentGatewayType.Remita;
}
+ if (transactionReference.StartsWith("OP_"))
+ {
+ return PaymentGatewayType.Opay;
+ }
// Default to configured default gateway
_logger.LogWarning("Could not determine gateway from reference: {Reference}", transactionReference);
return _config.DefaultGateway;