diff --git a/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs b/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
index 616c485..dae6b64 100644
--- a/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
+++ b/PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
@@ -16,6 +16,7 @@ public class PaymentGatewayConfig
public SquadConfig Squad { get; set; } = new();
public KorapayConfig Korapay { get; set; } = new();
public InterswitchConfig Interswitch { get; set; } = new();
+ public RemitaConfig Remita { get; set; } = new();
}
public class PaystackConfig
@@ -105,3 +106,11 @@ public class InterswitchConfig
///
public bool IsSandbox { get; set; } = false;
}
+
+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;
+}
diff --git a/PayBridge.SDK/Enums/PaymentGatewayType.cs b/PayBridge.SDK/Enums/PaymentGatewayType.cs
index b146521..6de06d3 100644
--- a/PayBridge.SDK/Enums/PaymentGatewayType.cs
+++ b/PayBridge.SDK/Enums/PaymentGatewayType.cs
@@ -57,5 +57,10 @@ public enum PaymentGatewayType
///
/// Interswitch payment gateway (Nigeria - Quickteller / Webpay)
///
- Interswitch = 10
+ Interswitch = 10,
+
+ ///
+ /// Remita payment gateway (Nigeria)
+ ///
+ Remita = 11
}
\ No newline at end of file
diff --git a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
index b741f13..57382d1 100644
--- a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
+++ b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
@@ -3,6 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using PayBridge.SDK.Application.Dtos;
using PayBridge.SDK.Enums;
+using PayBridge.SDK.Gateways;
using PayBridge.SDK.Interfaces;
namespace PayBridge.SDK;
@@ -135,6 +136,7 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
services.AddScoped();
services.AddScoped(sp => sp.GetRequiredService());
services.AddScoped();
+ services.AddScoped();
return;
}
@@ -174,6 +176,9 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
services.AddScoped();
services.AddScoped(sp => sp.GetRequiredService());
break;
+ case PaymentGatewayType.Remita:
+ services.AddScoped();
+ break;
}
}
}
diff --git a/PayBridge.SDK/Factories/PaymentGatewayFactory.cs b/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
index 9d1ba7a..e848118 100644
--- a/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
+++ b/PayBridge.SDK/Factories/PaymentGatewayFactory.cs
@@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using PayBridge.SDK.Application.Dtos;
using PayBridge.SDK.Enums;
+using PayBridge.SDK.Gateways;
using PayBridge.SDK.Interfaces;
namespace PayBridge.SDK;
@@ -45,6 +46,7 @@ public Dictionary CreateGateways()
TryAddGateway(gateways, PaymentGatewayType.Squad);
TryAddGateway(gateways, PaymentGatewayType.Korapay);
TryAddGateway(gateways, PaymentGatewayType.Interswitch);
+ TryAddGateway(gateways, PaymentGatewayType.Remita);
}
else
{
@@ -88,6 +90,7 @@ private void TryAddGateway(Dictionary gatew
PaymentGatewayType.Squad => _serviceProvider.GetService(),
PaymentGatewayType.Korapay => _serviceProvider.GetService(),
PaymentGatewayType.Interswitch => _serviceProvider.GetService(),
+ PaymentGatewayType.Remita => _serviceProvider.GetService(),
_ => null
};
diff --git a/PayBridge.SDK/Gateways/RemitaGateway.cs b/PayBridge.SDK/Gateways/RemitaGateway.cs
new file mode 100644
index 0000000..877804c
--- /dev/null
+++ b/PayBridge.SDK/Gateways/RemitaGateway.cs
@@ -0,0 +1,223 @@
+using System.Net.Http.Headers;
+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.Gateways;
+
+///
+/// Remita payment gateway implementation for Nigerian payments.
+/// Supports government, corporate, and retail payments via Remita's API.
+///
+public class RemitaGateway : IPaymentGateway
+{
+ private readonly PaymentGatewayConfig _config;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ private const string LiveBaseUrl = "https://login.remita.net/remita";
+ private const string SandboxBaseUrl = "https://remitademo.net/remita";
+ private const string TxRefPrefix = "REM_";
+
+ public RemitaGateway(
+ PaymentGatewayConfig config,
+ IHttpClientFactory httpClientFactory,
+ ILogger logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _httpClient = httpClientFactory.CreateClient(nameof(RemitaGateway));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public PaymentGatewayType GatewayType => PaymentGatewayType.Remita;
+
+ private string BaseUrl => _config.Remita.IsSandbox ? SandboxBaseUrl : LiveBaseUrl;
+
+ ///
+ /// Generates Remita API hash: SHA512(merchantId + serviceTypeId + orderId + amount + apiKey)
+ ///
+ private string GenerateHash(string orderId, decimal amount)
+ {
+ var amountKobo = (long)(amount * 100);
+ var raw = $"{_config.Remita.MerchantId}{_config.Remita.ServiceTypeId}{orderId}{amountKobo}{_config.Remita.ApiKey}";
+ var bytes = SHA512.HashData(Encoding.UTF8.GetBytes(raw));
+ return Convert.ToHexString(bytes).ToLower();
+ }
+
+ public async Task CreatePaymentAsync(PaymentRequest request)
+ {
+ try
+ {
+ var txRef = $"{TxRefPrefix}{Guid.NewGuid():N}";
+ var hash = GenerateHash(txRef, request.Amount);
+ var authToken = Convert.ToBase64String(
+ Encoding.UTF8.GetBytes($"{_config.Remita.MerchantId}:{_config.Remita.ApiKey}:{hash}"));
+
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization",
+ $"remitaConsumerKey={_config.Remita.MerchantId},remitaConsumerToken={authToken}");
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
+
+ var payload = new
+ {
+ serviceTypeId = _config.Remita.ServiceTypeId,
+ amount = (long)(request.Amount * 100),
+ orderId = txRef,
+ payerName = request.CustomerName ?? string.Empty,
+ payerEmail = request.CustomerEmail,
+ payerPhone = request.CustomerPhone ?? string.Empty,
+ description = request.Description ?? "Payment",
+ currency = request.Currency ?? "NGN"
+ };
+
+ var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync(
+ $"{BaseUrl}/exapp/api/v1/send/api/echannelsvc/merchant/api/paymentinit", content);
+
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("Remita initiate response: {Body}", responseBody);
+
+ var doc = JsonDocument.Parse(responseBody);
+ var root = doc.RootElement;
+ var statusCode = root.TryGetProperty("statuscode", out var sc) ? sc.GetString() : null;
+
+ if (statusCode != "025")
+ {
+ var msg = root.TryGetProperty("status", out var s) ? s.GetString() : "Unknown error";
+ return new PaymentResponse
+ {
+ Success = false,
+ Message = $"Remita payment initiation failed: {msg}",
+ Status = PaymentStatus.Failed
+ };
+ }
+
+ var rrr = root.TryGetProperty("RRR", out var rrrProp) ? rrrProp.GetString() : string.Empty;
+ var checkoutUrl = _config.Remita.IsSandbox
+ ? $"https://remitademo.net/remita/ecomm/finalize.reg?merchantId={_config.Remita.MerchantId}&hash={hash}&RRR={rrr}"
+ : $"https://login.remita.net/remita/ecomm/finalize.reg?merchantId={_config.Remita.MerchantId}&hash={hash}&RRR={rrr}";
+
+ return new PaymentResponse
+ {
+ Success = true,
+ TransactionReference = txRef,
+ CheckoutUrl = checkoutUrl,
+ Message = "Payment initiated successfully",
+ Status = PaymentStatus.Pending,
+ GatewayResponse = new Dictionary { ["RRR"] = rrr ?? string.Empty }
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error initiating Remita payment");
+ throw new PaymentGatewayException("Failed to initiate Remita payment", ex);
+ }
+ }
+
+ public async Task VerifyPaymentAsync(string transactionReference)
+ {
+ try
+ {
+ var hash = GenerateHash(transactionReference, 0);
+
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation(
+ "Authorization",
+ $"remitaConsumerKey={_config.Remita.MerchantId},remitaConsumerToken={hash}");
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
+
+ var response = await _httpClient.GetAsync(
+ $"{BaseUrl}/exapp/api/v1/send/api/echannelsvc/{_config.Remita.MerchantId}/{transactionReference}/{hash}/orderstatus.reg");
+
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("Remita verify response: {Body}", responseBody);
+
+ var doc = JsonDocument.Parse(responseBody);
+ var root = doc.RootElement;
+
+ // Remita status: "01" = successful, "02" = processing, others = failed
+ var status = root.TryGetProperty("status", out var st) ? st.GetString() : null;
+ var amount = root.TryGetProperty("amount", out var am) ? am.GetDecimal() / 100m : 0m;
+
+ var paymentStatus = status == "01" ? PaymentStatus.Successful
+ : status == "02" ? PaymentStatus.Pending
+ : PaymentStatus.Failed;
+
+ return new VerificationResponse
+ {
+ Success = status == "01",
+ TransactionReference = transactionReference,
+ Amount = amount,
+ Currency = "NGN",
+ Status = paymentStatus,
+ Message = paymentStatus == PaymentStatus.Successful ? "Payment verified successfully" : "Payment not completed",
+ GatewayResponse = new Dictionary { ["status"] = status ?? string.Empty }
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error verifying Remita payment: {Ref}", transactionReference);
+ throw new PaymentGatewayException("Failed to verify Remita payment", ex);
+ }
+ }
+
+ public async Task RefundPaymentAsync(RefundRequest request)
+ {
+ try
+ {
+ var hash = GenerateHash(request.TransactionReference, request.Amount);
+
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation(
+ "Authorization",
+ $"remitaConsumerKey={_config.Remita.MerchantId},remitaConsumerToken={hash}");
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
+
+ var payload = new
+ {
+ merchantId = _config.Remita.MerchantId,
+ rrr = request.TransactionReference,
+ amount = (long)(request.Amount * 100),
+ reason = request.Reason ?? "Customer request"
+ };
+
+ var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync(
+ $"{BaseUrl}/exapp/api/v1/send/api/echannelsvc/merchant/api/reversal", content);
+
+ var responseBody = await response.Content.ReadAsStringAsync();
+ _logger.LogDebug("Remita refund response: {Body}", responseBody);
+
+ var doc = JsonDocument.Parse(responseBody);
+ var root = doc.RootElement;
+
+ var statusCode = root.TryGetProperty("statuscode", out var sc) ? sc.GetString() : null;
+ var isSuccess = statusCode == "025" || statusCode == "00";
+
+ return new RefundResponse
+ {
+ Success = isSuccess,
+ RefundReference = request.TransactionReference,
+ TransactionReference = request.TransactionReference,
+ Amount = request.Amount,
+ Message = isSuccess ? "Refund initiated successfully" : "Refund failed",
+ Status = isSuccess ? PaymentStatus.Refunded : PaymentStatus.Failed
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing Remita refund: {Ref}", request.TransactionReference);
+ throw new PaymentGatewayException("Failed to process Remita refund", ex);
+ }
+ }
+}
diff --git a/PayBridge.SDK/Services/PaymentService.cs b/PayBridge.SDK/Services/PaymentService.cs
index 24b3190..caedd97 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.Paystack, PaymentGatewayType.Flutterwave);
+ return ChooseAvailableGateway(PaymentGatewayType.Monnify, PaymentGatewayType.Squad, PaymentGatewayType.Korapay, PaymentGatewayType.Interswitch, PaymentGatewayType.Remita, PaymentGatewayType.Paystack, PaymentGatewayType.Flutterwave);
case "KES":
case "GHS":
@@ -342,6 +342,11 @@ private PaymentGatewayType DetermineGatewayFromReference(string transactionRefer
return PaymentGatewayType.Korapay;
}
+ if (transactionReference.StartsWith("REM_"))
+ {
+ return PaymentGatewayType.Remita;
+ }
+
// Default to configured default gateway
_logger.LogWarning("Could not determine gateway from reference: {Reference}", transactionReference);
return _config.DefaultGateway;