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;