Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Breaking change: RemitaConfig sandbox default flipped to production.

Changing IsSandbox default from true to false means existing deployments that relied on the sandbox default will now hit Remita's production environment without explicit configuration. This could cause unintended live transactions or API errors if production credentials aren't configured.

⚠️ Mitigation options
  1. Preferred: Revert to true and require explicit opt-in to production via configuration.
  2. Alternative: Document this as a breaking change in release notes and migration guide, and validate that MerchantId, ServiceTypeId, and ApiKey are configured before allowing production mode.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Dtos/PaymentGatewayConfig.cs` at line 116, The change flipped
the Remita sandbox default causing deployments to hit production; revert the
IsSandbox property's default back to true on the
PaymentGatewayConfig/RemitaConfig so sandbox remains the default (i.e., set
IsSandbox to true), and if you prefer the alternative mitigation add a runtime
guard in the payment initialization path (check MerchantId, ServiceTypeId,
ApiKey in the remita-related config/initializer) to validate those credentials
before allowing IsSandbox == 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;
}
7 changes: 6 additions & 1 deletion PayBridge.SDK/Enums/PaymentGatewayType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,10 @@ public enum PaymentGatewayType
/// <summary>
/// Remita payment gateway (Nigeria)
/// </summary>
Remita = 11
Remita = 11,

/// <summary>
/// OPay payment gateway (Nigeria / Africa)
/// </summary>
Opay = 12
}
5 changes: 4 additions & 1 deletion PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -137,6 +136,7 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
services.AddScoped<IPaymentGateway>(sp => sp.GetRequiredService<KorapayGateway>());
services.AddScoped<InterswitchGateway>();
services.AddScoped<RemitaGateway>();
services.AddScoped<OpayGateway>();
return;
}

Expand Down Expand Up @@ -179,6 +179,9 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway
case PaymentGatewayType.Remita:
services.AddScoped<RemitaGateway>();
break;
case PaymentGatewayType.Opay:
services.AddScoped<OpayGateway>();
break;
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion PayBridge.SDK/Factories/PaymentGatewayFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,6 +46,7 @@ public Dictionary<PaymentGatewayType, IPaymentGateway> CreateGateways()
TryAddGateway(gateways, PaymentGatewayType.Korapay);
TryAddGateway(gateways, PaymentGatewayType.Interswitch);
TryAddGateway(gateways, PaymentGatewayType.Remita);
TryAddGateway(gateways, PaymentGatewayType.Opay);
}
else
{
Expand Down Expand Up @@ -91,6 +91,7 @@ private void TryAddGateway(Dictionary<PaymentGatewayType, IPaymentGateway> gatew
PaymentGatewayType.Korapay => _serviceProvider.GetService<KorapayGateway>(),
PaymentGatewayType.Interswitch => _serviceProvider.GetService<InterswitchGateway>(),
PaymentGatewayType.Remita => _serviceProvider.GetService<RemitaGateway>(),
PaymentGatewayType.Opay => _serviceProvider.GetService<OpayGateway>(),
_ => null
};

Expand Down
246 changes: 246 additions & 0 deletions PayBridge.SDK/Gateways/OpayGateway.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// OPay payment gateway implementation (Nigeria / Africa).
/// Supports card, bank transfer, USSD, and mobile money payments.
/// </summary>
public class OpayGateway : IPaymentGateway
{
private readonly PaymentGatewayConfig _config;
private readonly HttpClient _httpClient;
private readonly ILogger<OpayGateway> _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<OpayGateway> logger)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_httpClient = httpClientFactory.CreateClient(nameof(OpayGateway));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
Comment on lines +34 to +36
}

public PaymentGatewayType GatewayType => PaymentGatewayType.Opay;

private string BaseUrl => _config.Opay.IsSandbox ? SandboxBaseUrl : LiveBaseUrl;

/// <summary>
/// Generates HMAC-SHA512 signature: HMAC512(jsonBody, secretKey)
/// </summary>
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();
Comment on lines +50 to +51
}

public async Task<PaymentResponse> 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,
Comment on lines +66 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

OPay cashier API authentication header format and required fields for create payment

💡 Result:

To authenticate requests for the OPay Cashier create payment API, you must include specific headers in your HTTP request. Authentication Header Format For the cashier creation endpoint, OPay utilizes public key authentication [1][2]. You must provide the following headers [2][3]: Authorization: Bearer {PublicKey} MerchantId: {YourMerchantId} Content-Type: application/json Accept: application/json In this format, {PublicKey} is your OPay merchant account public key, and {YourMerchantId} is your assigned merchant ID [2][3]. Both values can be retrieved from the API Keys & Webhooks section of your OPay merchant dashboard [1]. Required Fields for Create Payment While specific requirements can vary based on regional implementation, the typical JSON payload for a cashier create payment request includes the following fields [2][3]: Amount: The transaction amount. Currency: The currency code (e.g., NGN, EGP) [2][3]. Reference: A unique identifier for the payment order in your system [2][3]. CallbackUrl: The URL where OPay will send status update notifications [2][3]. ReturnUrl: The URL to redirect the customer to after the payment process completes [2][3]. Country: The country code (e.g., NG, EG) [2][3]. ExpireAt: The expiration time for the payment link (often in minutes) [2][3]. Product: An object containing details such as the product name and description [2][3]. UserInfo (Optional but recommended): An object containing details like userId, userMobile, and userName [3]. Note: Ensure you are using the correct endpoint for your region (e.g., sandboxapi.opaycheckout.com for testing) [2]. Always refer to the official OPay documentation portal for the most accurate and region-specific implementation details [1][4][5].

Citations:


🌐 Web query:

OPay cashier create payment CancelUrl field name required or optional

💡 Result:

In the OPay Cashier Create Payment API, the cancelUrl field is optional [1][2]. It is defined as the URL to which the OPay cashier should redirect your client if they choose to cancel the payment process [1][2]. While not mandatory, providing this URL allows you to manage the user experience when a payment is not completed [2][3]. In contrast, the returnUrl field is typically required to handle the completion of the checkout process [2][3].

Citations:


🌐 Web query:

OPay cashier create payment CallbackUrl ReturnUrl empty string allowed

💡 Result:

According to official OPay technical documentation, the requirements for CallbackUrl and ReturnUrl in the Cashier Create Payment API are as follows: 1. ReturnUrl is mandatory. It is explicitly listed as a "required" parameter in the API specifications for Cashier Create Payment and other payment methods [1][2]. Providing an empty string for this parameter will likely cause the API request to fail, as it is required for redirecting the client after the checkout process [1][2]. 2. CallbackUrl is optional. It is documented as an "optional" parameter [1][2]. If you do not provide a CallbackUrl in your API request, OPay will instead send callback notifications to the webhook URL configured in your merchant dashboard [1][2]. Because ReturnUrl is a required field, you must provide a valid URL string to ensure the payment flow functions correctly. While the API documentation defines it as a required string, using an empty string will not satisfy this requirement and is not recommended [1][2].

Citations:


Fix OPay URL handling for returnUrl / callbackUrl / cancelUrl

  • Auth header format is correct for OPay Cashier Create Payment: Authorization: Bearer {PublicKey}.
  • returnUrl: OPay documents this as required. Defaulting request.RedirectUrl ?? string.Empty can send an empty string and likely breaks the API request—ensure a non-empty ReturnUrl (or fail/omit the request) when RedirectUrl is null.
  • callbackUrl: OPay documents this as optional (webhooks can be used if omitted), so avoid sending empty strings when WebhookUrl is null.
  • cancelUrl: OPay documents this as optional; avoid sending cancelUrl: "" when RedirectUrl is null (omit unless you have a real URL).
returnUrl = request.RedirectUrl ?? string.Empty,
callbackUrl = request.WebhookUrl ?? string.Empty,
cancelUrl = request.RedirectUrl ?? string.Empty,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Gateways/OpayGateway.cs` around lines 66 - 68, The payload
currently defaults returnUrl/callbackUrl/cancelUrl to empty strings which can
break OPay; in the OpayGateway code that builds the payment payload (look for
returnUrl/callbackUrl/cancelUrl assignments and where headers are set) enforce
that request.RedirectUrl is non-null/ non-empty and fail/throw or return an
error before calling OPay (because returnUrl is required), and only include
callbackUrl and cancelUrl keys in the outgoing JSON when request.WebhookUrl or
request.RedirectUrl are present (omit them instead of setting to "") ; also
ensure the Authorization header uses the exact format "Authorization: Bearer
{PublicKey}" when setting headers for the OPay Cashier Create Payment request.

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;
Comment on lines +98 to +99

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<string, string>
{
["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<VerificationResponse> 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<string, string> { ["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<RefundResponse> 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);
}
}
Comment on lines +196 to +245

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Hardcoded "NGN" currency in refunds breaks multi-currency support.

Line 204 hardcodes currency = "NGN" for all refund requests, regardless of the original transaction's currency. If OPay supports non-NGN currencies (or will in the future), this will cause refunds to fail or process incorrectly. The refund currency must match the original payment currency.

🐛 Proposed fix

Pass the original currency through the RefundRequest or retrieve it from the transaction record:

             var payload = new
             {
                 merchantId = _config.Opay.MerchantId,
                 reference = request.TransactionReference,
-                amount = new { total = (long)(request.Amount * 100), currency = "NGN" },
+                amount = new { total = (long)(request.Amount * 100), currency = request.Currency ?? "NGN" },
                 reason = request.Reason ?? "Customer request"
             };

Ensure RefundRequest includes a Currency property populated from the original transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Gateways/OpayGateway.cs` around lines 196 - 245, The refund
payload currently hardcodes currency = "NGN" in RefundPaymentAsync which breaks
multi-currency refunds; update the flow so RefundRequest carries the original
currency (add a Currency property to RefundRequest and ensure callers populate
it from the original transaction) and replace the hardcoded "NGN" with
request.Currency (with a sensible fallback such as _config.Opay.DefaultCurrency
or throw if missing) when building the payload; keep all existing
header/signature logic (GenerateSignature, RefundResponse mapping) unchanged.

}
2 changes: 1 addition & 1 deletion PayBridge.SDK/Gateways/RemitaGateway.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
using PayBridge.SDK.Exceptions;
using PayBridge.SDK.Interfaces;

namespace PayBridge.SDK.Gateways;
namespace PayBridge.SDK;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check namespace declarations for all gateway implementations.

# Find all gateway files and extract their namespaces
rg -n '^namespace ' --glob '*Gateway.cs' -A 0

Repository: teesofttech/PayBridge

Length of output: 1043


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find any remaining gateway files using the old namespace
rg -n 'namespace\s+PayBridge\.SDK\.Gateways' --glob '*Gateway.cs' .

# 2) Check for any references to the old fully-qualified RemitaGateway name
rg -n 'PayBridge\.SDK\.Gateways\.RemitaGateway' .

# 3) Show where RemitaGateway is declared and what namespace it uses
rg -n 'class\s+RemitaGateway\b|interface\s+IRemitaGateway\b|record\s+RemitaGateway\b' --glob '*RemitaGateway*.cs' .

Repository: teesofttech/PayBridge

Length of output: 47


🏁 Script executed:

#!/bin/bash
set +euo pipefail

echo "=== Old namespace declarations (PayBridge.SDK.Gateways) in *Gateway.cs ==="
rg -n 'namespace\s+PayBridge\.SDK\.Gateways' --glob '*Gateway.cs' . || true

echo
echo "=== References to old fully-qualified RemitaGateway (PayBridge.SDK.Gateways.RemitaGateway) ==="
rg -n 'PayBridge\.SDK\.Gateways\.RemitaGateway' . || true

echo
echo "=== RemitaGateway declarations (class/interface/record) ==="
rg -n 'class\s+RemitaGateway\b|interface\s+IRemitaGateway\b|record\s+RemitaGateway\b' --glob '*Remita*' . || true

echo
echo "=== RemitaGateway file content (namespace line + any relevant type signature) ==="
f="PayBridge.SDK/Gateways/RemitaGateway.cs"
if [ -f "$f" ]; then
  rg -n 'namespace\s+|class\s+RemitaGateway\b|interface\s+IRemitaGateway\b|record\s+RemitaGateway\b' "$f" || true
else
  echo "Missing $f"
fi

Repository: teesofttech/PayBridge

Length of output: 546


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== References to old namespace prefix PayBridge.SDK.Gateways. ==="
rg -n 'PayBridge\.SDK\.Gateways\.' .

echo
echo "=== Using directives for PayBridge.SDK.Gateways ==="
rg -n 'using\s+PayBridge\.SDK\.Gateways\b' .

Repository: teesofttech/PayBridge

Length of output: 132


Breaking change: RemitaGateway namespace moved to PayBridge.SDK (PayBridge.SDK/Gateways/RemitaGateway.cs:14). Consumers referencing PayBridge.SDK.Gateways.RemitaGateway will break; the repo already uses namespace PayBridge.SDK; consistently across all *Gateway.cs files and contains no remaining PayBridge.SDK.Gateways.* references, so focus on documenting the migration (and version bump/migration guidance) for consumers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Gateways/RemitaGateway.cs` at line 14, Breaking change: the
RemitaGateway class namespace was changed from PayBridge.SDK.Gateways to
PayBridge.SDK; update the release notes/CHANGELOG, README and a migration guide
showing the exact find-and-replace consumers must perform (replace
PayBridge.SDK.Gateways.RemitaGateway with PayBridge.SDK.RemitaGateway), include
a short code example of old vs new fully-qualified type usage and any necessary
using/import changes, and mark a major version bump in your release/versioning
guidance so downstream consumers are alerted to the breaking namespace change.


/// <summary>
/// Remita payment gateway implementation for Nigerian payments.
Expand Down
6 changes: 5 additions & 1 deletion PayBridge.SDK/Services/PaymentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all NGN gateway selection logic to verify consistency.

# Search for NGN currency handling
rg -n 'case.*"NGN"' -A 3 -B 1

Repository: teesofttech/PayBridge

Length of output: 623


Align NGN gateway preference order with stated routing priority.

In PayBridge.SDK/Services/PaymentService.cs (case "NGN"), the preference list is Monnify → Squad → Korapay → Interswitch → Remita → Opay → Paystack → Flutterwave—so OPay is 6th and Paystack comes after OPay—contradicting the PR summary’s intended “Paystack → OPay → Remita → Flutterwave” priority. Update either the code or the stated business priority so they match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PayBridge.SDK/Services/PaymentService.cs` at line 252, The NGN routing order
in the case "NGN" uses ChooseAvailableGateway with PaymentGatewayType values out
of sync with the PR summary; update the argument order so Paystack is preferred
before Opay and Remita/Flutterwave (e.g., ensure PaymentGatewayType.Paystack
appears ahead of PaymentGatewayType.Opay and
PaymentGatewayType.Remita/Flutterwave) when calling ChooseAvailableGateway in
PaymentService.cs to match the intended "Paystack → OPay → Remita → Flutterwave"
priority.


case "KES":
case "GHS":
Expand Down Expand Up @@ -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;
Expand Down