diff --git a/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs b/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
index 627884e..9161f26 100644
--- a/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
+++ b/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
@@ -18,6 +18,7 @@ public class PaymentGatewayConfig
public InterswitchConfig Interswitch { get; set; } = new();
public RemitaConfig Remita { get; set; } = new();
public OpayConfig Opay { get; set; } = new();
+ public DpoGroupConfig DpoGroup { get; set; } = new();
}
public class PaystackConfig
@@ -123,3 +124,21 @@ public class OpayConfig
public string SecretKey { get; set; } = string.Empty;
public bool IsSandbox { get; set; } = false;
}
+
+public class DpoGroupConfig
+{
+ ///
+ /// DPO Company Token (from your DPO merchant account).
+ ///
+ public string CompanyToken { get; set; } = string.Empty;
+
+ ///
+ /// Default payment currency (e.g. KES, GHS, UGX, ZAR, USD).
+ ///
+ public string PaymentCurrency { get; set; } = "USD";
+
+ ///
+ /// Set to true to use DPO sandbox environment.
+ ///
+ public bool IsSandbox { get; set; } = false;
+}
diff --git a/PayBridge.SDK/Enums/PaymentGatewayType.cs b/PayBridge.SDK/Enums/PaymentGatewayType.cs
index 71f0264..6fa59cf 100644
--- a/PayBridge.SDK/Enums/PaymentGatewayType.cs
+++ b/PayBridge.SDK/Enums/PaymentGatewayType.cs
@@ -67,5 +67,10 @@ public enum PaymentGatewayType
///
/// OPay payment gateway (Nigeria / Africa)
///
- Opay = 12
+ Opay = 12,
+
+ ///
+ /// DPO Group payment gateway (Africa - 19+ countries)
+ ///
+ DpoGroup = 13
}
\ No newline at end of file
diff --git a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
index 8878660..af5193f 100644
--- a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
+++ b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
@@ -135,9 +135,7 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
services.AddScoped();
services.AddScoped(sp => sp.GetRequiredService());
services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- return;
+ AddGatewayRegistration(services);
}
// Register only the enabled gateways
@@ -182,6 +180,9 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
case PaymentGatewayType.Opay:
services.AddScoped();
break;
+ case PaymentGatewayType.DpoGroup:
+ AddGatewayRegistration(services);
+ break;
}
}
}
diff --git a/PayBridge.SDK/Factories/PaymentGatewayFactory.cs b/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
index eac0466..26fdc33 100644
--- a/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
+++ b/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
@@ -47,6 +47,7 @@ public Dictionary CreateGateways()
TryAddGateway(gateways, PaymentGatewayType.Interswitch);
TryAddGateway(gateways, PaymentGatewayType.Remita);
TryAddGateway(gateways, PaymentGatewayType.Opay);
+ TryAddGateway(gateways, PaymentGatewayType.DpoGroup);
}
else
{
@@ -92,6 +93,7 @@ private void TryAddGateway(Dictionary gatew
PaymentGatewayType.Interswitch => _serviceProvider.GetService(),
PaymentGatewayType.Remita => _serviceProvider.GetService(),
PaymentGatewayType.Opay => _serviceProvider.GetService(),
+ PaymentGatewayType.DpoGroup => _serviceProvider.GetService(),
_ => null
};
diff --git a/PayBridge.SDK/Gateways/DpoGroupGateway.cs b/PayBridge.SDK/Gateways/DpoGroupGateway.cs
new file mode 100644
index 0000000..b357f88
--- /dev/null
+++ b/PayBridge.SDK/Gateways/DpoGroupGateway.cs
@@ -0,0 +1,231 @@
+using System.Text;
+using System.Xml.Linq;
+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;
+
+///
+/// DPO Group payment gateway implementation (Africa - 19+ countries).
+/// Supports KES, GHS, UGX, TZS, ZAR, RWF, USD, EUR and more.
+/// Uses XML-based API v6 at https://secure.3gdirectpay.com/API/v6/
+///
+public class DpoGroupGateway : IPaymentGateway
+{
+ private readonly PaymentGatewayConfig _config;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ private const string LiveApiUrl = "https://secure.3gdirectpay.com/API/v6/";
+ private const string SandboxApiUrl = "https://secure.3gdirectpay.com/API/v6/";
+ private const string LivePayUrl = "https://secure.3gdirectpay.com/payv2.php";
+ private const string SandboxPayUrl = "https://secure.3gdirectpay.com/payv2.php";
+ private const string TxRefPrefix = "DPO_";
+
+ public DpoGroupGateway(
+ PaymentGatewayConfig config,
+ IHttpClientFactory httpClientFactory,
+ ILogger logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _httpClient = httpClientFactory.CreateClient(nameof(DpoGroupGateway));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public PaymentGatewayType GatewayType => PaymentGatewayType.DpoGroup;
+
+ private string ApiUrl => _config.DpoGroup.IsSandbox ? SandboxApiUrl : LiveApiUrl;
+ private string PayUrl => _config.DpoGroup.IsSandbox ? SandboxPayUrl : LivePayUrl;
+
+ public async Task CreatePaymentAsync(PaymentRequest request)
+ {
+ try
+ {
+ var txRef = $"{TxRefPrefix}{Guid.NewGuid():N}";
+ var currency = request.Currency ?? _config.DpoGroup.PaymentCurrency;
+ var amount = request.Amount.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
+ // Build XML payload for createToken
+ var xml = new XDocument(
+ new XElement("API3G",
+ new XElement("CompanyToken", _config.DpoGroup.CompanyToken),
+ new XElement("Request", "createToken"),
+ new XElement("Transaction",
+ new XElement("PaymentAmount", amount),
+ new XElement("PaymentCurrency", currency),
+ new XElement("CompanyRef", txRef),
+ new XElement("RedirectURL", request.RedirectUrl ?? string.Empty),
+ new XElement("BackURL", request.RedirectUrl ?? string.Empty),
+ new XElement("CompanyRefUnique", "0"),
+ new XElement("PTL", "5")
+ ),
+ new XElement("Services",
+ new XElement("Service",
+ new XElement("ServiceType", "3854"),
+ new XElement("ServiceDescription", request.Description ?? "Payment"),
+ new XElement("ServiceDate", DateTime.UtcNow.ToString("yyyy/MM/dd HH:mm"))
+ )
+ )
+ )
+ );
+
+ var xmlString = xml.ToString();
+ _logger.LogDebug("DPO createToken request prepared for CompanyRef: {CompanyRef}", txRef);
+ var content = new StringContent(xmlString, Encoding.UTF8, "application/xml");
+ var response = await _httpClient.PostAsync(ApiUrl, content);
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("DPO createToken response: {Body}", responseBody);
+
+ var doc = XDocument.Parse(responseBody);
+ var result = doc.Root?.Element("Result")?.Value;
+ var resultExplanation = doc.Root?.Element("ResultExplanation")?.Value;
+
+ if (result != "000")
+ {
+ return new PaymentResponse
+ {
+ Success = false,
+ Message = $"DPO token creation failed: {resultExplanation}",
+ Status = PaymentStatus.Failed
+ };
+ }
+
+ var transToken = doc.Root?.Element("TransToken")?.Value ?? string.Empty;
+ var checkoutUrl = $"{PayUrl}?ID={transToken}";
+
+ return new PaymentResponse
+ {
+ Success = true,
+ TransactionReference = $"{TxRefPrefix}{transToken}",
+ CheckoutUrl = checkoutUrl,
+ Message = "Payment initiated successfully",
+ Status = PaymentStatus.Pending,
+ GatewayResponse = new Dictionary
+ {
+ ["transToken"] = transToken,
+ ["companyRef"] = txRef,
+ ["checkoutUrl"] = checkoutUrl
+ }
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error initiating DPO Group payment");
+ throw new PaymentGatewayException("Failed to initiate DPO Group payment", ex);
+ }
+ }
+
+ public async Task VerifyPaymentAsync(string transactionReference)
+ {
+ try
+ {
+ // For DPO, transactionReference is the TransToken returned at creation
+ // Strip DPO_ prefix if present to get the companyRef, but we need the TransToken
+ // Convention: we store the TransToken as the gateway's transaction reference
+ var xml = new XDocument(
+ new XElement("API3G",
+ new XElement("CompanyToken", _config.DpoGroup.CompanyToken),
+ new XElement("Request", "verifyToken"),
+ new XElement("TransactionToken", transactionReference.StartsWith(TxRefPrefix, StringComparison.Ordinal)
+ ? transactionReference.Substring(TxRefPrefix.Length)
+ : transactionReference)
+ )
+ );
+
+ var xmlString = xml.ToString();
+ var content = new StringContent(xmlString, Encoding.UTF8, "application/xml");
+ var response = await _httpClient.PostAsync(ApiUrl, content);
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("DPO verifyToken response: {Body}", responseBody);
+
+ var doc = XDocument.Parse(responseBody);
+ var result = doc.Root?.Element("Result")?.Value;
+ var resultExplanation = doc.Root?.Element("ResultExplanation")?.Value;
+ var customerName = doc.Root?.Element("CustomerName")?.Value ?? string.Empty;
+ var customerEmail = doc.Root?.Element("CustomerEmail")?.Value ?? string.Empty;
+ var transactionAmountStr = doc.Root?.Element("TransactionAmount")?.Value ?? "0";
+ var transactionCurrency = doc.Root?.Element("TransactionCurrency")?.Value ?? _config.DpoGroup.PaymentCurrency;
+ var companyRef = doc.Root?.Element("CompanyRef")?.Value ?? transactionReference;
+
+ decimal.TryParse(transactionAmountStr, System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out var transactionAmount);
+
+ // DPO result codes: "000" = payment verified successfully
+ var paymentStatus = result == "000" ? PaymentStatus.Successful
+ : result == "001" ? PaymentStatus.Pending
+ : PaymentStatus.Failed;
+
+ return new VerificationResponse
+ {
+ Success = result == "000",
+ TransactionReference = companyRef,
+ Amount = transactionAmount,
+ Currency = transactionCurrency,
+ Status = paymentStatus,
+ Message = result == "000" ? "Payment verified successfully" : (resultExplanation ?? $"Payment status code: {result}"),
+ GatewayResponse = new Dictionary
+ {
+ ["result"] = result ?? string.Empty,
+ ["resultExplanation"] = resultExplanation ?? string.Empty,
+ ["customerName"] = customerName,
+ ["customerEmail"] = customerEmail
+ }
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error verifying DPO Group payment: {Ref}", transactionReference);
+ throw new PaymentGatewayException("Failed to verify DPO Group payment", ex);
+ }
+ }
+
+ public async Task RefundPaymentAsync(RefundRequest request)
+ {
+ try
+ {
+ // DPO Group refunds are initiated via refundToken request
+ var xml = new XDocument(
+ new XElement("API3G",
+ new XElement("CompanyToken", _config.DpoGroup.CompanyToken),
+ new XElement("Request", "refundToken"),
+ new XElement("TransactionToken", request.TransactionReference.StartsWith(TxRefPrefix, StringComparison.Ordinal)
+ ? request.TransactionReference.Substring(TxRefPrefix.Length)
+ : request.TransactionReference),
+ new XElement("refundAmount", request.Amount.ToString("F2")),
+ new XElement("refundDetails", request.Reason ?? "Customer request")
+ )
+ );
+
+ var xmlString = xml.ToString();
+ var content = new StringContent(xmlString, Encoding.UTF8, "application/xml");
+ var response = await _httpClient.PostAsync(ApiUrl, content);
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("DPO refundToken response: {Body}", responseBody);
+
+ var doc = XDocument.Parse(responseBody);
+ var result = doc.Root?.Element("Result")?.Value;
+ var resultExplanation = doc.Root?.Element("ResultExplanation")?.Value;
+ var isSuccess = result == "000";
+
+ return new RefundResponse
+ {
+ Success = isSuccess,
+ RefundReference = request.TransactionReference,
+ TransactionReference = request.TransactionReference,
+ Amount = request.Amount,
+ Message = isSuccess ? "Refund initiated successfully" : (resultExplanation ?? "Refund failed"),
+ Status = isSuccess ? PaymentStatus.Refunded : PaymentStatus.Failed
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing DPO Group refund: {Ref}", request.TransactionReference);
+ throw new PaymentGatewayException("Failed to process DPO Group refund", ex);
+ }
+ }
+}
diff --git a/PayBridge.SDK/Services/PaymentService.cs b/PayBridge.SDK/Services/PaymentService.cs
index b7a5f5f..a37a90f 100644
--- a/PayBridge.SDK/Services/PaymentService.cs
+++ b/PayBridge.SDK/Services/PaymentService.cs
@@ -256,7 +256,8 @@ private PaymentGatewayType SelectBestGateway(PaymentRequest request)
case "UGX":
case "TZS":
case "ZAR":
- return ChooseAvailableGateway(PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);
+ case "RWF":
+ return ChooseAvailableGateway(PaymentGatewayType.DpoGroup, PaymentGatewayType.Flutterwave, PaymentGatewayType.Paystack);
case "BHD":
return ChooseAvailableGateway(PaymentGatewayType.BenefitPay);
@@ -351,6 +352,11 @@ private PaymentGatewayType DetermineGatewayFromReference(string transactionRefer
{
return PaymentGatewayType.Opay;
}
+
+ if (transactionReference.StartsWith("DPO_"))
+ {
+ return PaymentGatewayType.DpoGroup;
+ }
// Default to configured default gateway
_logger.LogWarning("Could not determine gateway from reference: {Reference}", transactionReference);
return _config.DefaultGateway;