Skip to content

feat: implement OpayGateway (Nigeria/Africa - mobile money & card)#30

Merged
teesofttech merged 1 commit into
masterfrom
feature/issue-13-opay-gateway
Jun 7, 2026
Merged

feat: implement OpayGateway (Nigeria/Africa - mobile money & card)#30
teesofttech merged 1 commit into
masterfrom
feature/issue-13-opay-gateway

Conversation

@teesofttech

@teesofttech teesofttech commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the OPay payment gateway for Nigerian and African payments, supporting card, bank transfer, USSD, and mobile money.

Changes

  • Added Opay = 8 to PaymentGatewayType enum
  • Added RemitaConfig and OpayConfig (MerchantId, PublicKey, SecretKey, IsSandbox) to PaymentGatewayConfig.cs
  • Created RemitaGateway.cs (included as dependency for this branch)
  • Created OpayGateway.cs with full implementation:
    • HMAC-SHA512 request signing per API call
    • Live (cashierapi.opayweb.com) / Sandbox (sandboxapi.opaycheckout.com) toggle
    • Amounts in kobo (×100)
    • Status codes: 00000 = success, SUCCESS/PENDING/FAIL for verification
    • Tx reference prefix: OP_
  • Registered OpayGateway in IServiceCollectionExtensions.cs
  • Added factory switch case in PaymentGatewayFactory.cs
  • Updated PaymentService.cs: NGN routing (Paystack → OPay → Remita → Flutterwave) + OP_ prefix detection

Closes #13

Summary by CodeRabbit

New Features

  • Added Opay payment gateway support for payment processing, including payment creation, verification, and refund operations with configurable merchant credentials and sandbox/live environment switching.

Changes

  • Updated RemitaConfig sandbox mode default to disabled.

Copilot AI review requested due to automatic review settings June 4, 2026 09:30
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces OPay, a Nigeria-focused payment gateway, into the PayBridge SDK. It defines configuration and enum types, implements a full OpayGateway with HMAC-SHA512 request signing and payment operations (create, verify, refund), wires the gateway into DI and factory resolution, and integrates it into service-level payment routing and transaction reference detection.

Changes

OPay Payment Gateway Integration

Layer / File(s) Summary
Configuration and Type Definitions
PayBridge.SDK/Dtos/PaymentGatewayConfig.cs, PayBridge.SDK/Enums/PaymentGatewayType.cs, PayBridge.SDK/Gateways/RemitaGateway.cs
OpayConfig DTO with MerchantId, PublicKey, SecretKey, and IsSandbox flag is added to PaymentGatewayConfig; PaymentGatewayType.Opay enum member is defined; RemitaConfig sandbox default changes to false; RemitaGateway namespace is migrated to PayBridge.SDK for consistency.
OPay Gateway Implementation
PayBridge.SDK/Gateways/OpayGateway.cs
Implements OpayGateway : IPaymentGateway with HMAC-SHA512 signature generation. CreatePaymentAsync generates transaction reference, builds and signs JSON payload, POSTs to OPay endpoint, parses response code "00000", and returns PaymentResponse with cashier URL. VerifyPaymentAsync queries payment status and amount, maps OPay status to internal PaymentStatus. RefundPaymentAsync constructs and signs refund payload, posts to refund endpoint, and sets RefundResponse with success/failure state. All methods catch exceptions and throw PaymentGatewayException.
Dependency Injection and Factory Integration
PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs, PayBridge.SDK/Factories/PaymentGatewayFactory.cs
Removed unused PayBridge.SDK.Gateways using directive. Added OpayGateway registration in default gateways and switch case in RegisterGateways. PaymentGatewayFactory resolves OpayGateway from service provider and includes it when no gateways are explicitly enabled.
Service Routing and Reference Detection
PayBridge.SDK/Services/PaymentService.cs
SelectBestGateway for NGN currency now includes PaymentGatewayType.Opay in preferred gateway list. DetermineGatewayFromReference recognizes "OP_" transaction reference prefix and maps it to PaymentGatewayType.Opay.

Possibly Related PRs

  • teesofttech/PayBridge#31: Refactors RegisterGateways to centralize gateway-to-IPaymentGateway wiring while this PR adds the Opay gateway to the same registration flow.
  • teesofttech/PayBridge#28: Extends the same gateway infrastructure (enum, config, DI, factory, service routing) to add Interswitch gateway with ISW_ reference prefix, mirroring the Opay integration pattern.
  • teesofttech/PayBridge#27: Integrates Squad gateway using the same gateway-plug-in flow (enum update, DI/factory wiring, NGN routing, and SQ_ prefix detection) as this PR's Opay implementation.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit hops to OPay's door,
With HMAC keys and signatures galore,
Three operations signed and sealed tight,
Nigeria's NGN payments now take flight! 🐰💚

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning RemitaGateway namespace change and RemitaConfig default value change (IsSandbox: true→false) are outside the scope of issue #13, which focuses solely on OPay implementation. Revert the RemitaGateway namespace change and RemitaConfig.IsSandbox default modification to a separate PR or clarify their necessity in the issue scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing OpayGateway for Nigeria/Africa mobile money and card payments, which aligns with the primary objective of the PR.
Linked Issues check ✅ Passed All coding requirements from issue #13 are met: OpayGateway.cs implemented with HMAC-SHA512 signing, sandbox/live URL toggles, amount handling in kobo, status code mapping, transaction reference prefix (OP_), and proper integration in factory, service collection, and payment service routing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-13-opay-gateway

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds new Nigeria-focused payment gateway support to the SDK by introducing OPay (card, bank transfer, USSD, mobile money) and Remita, wiring them into gateway selection, dependency injection, and factory creation so NGN payments can route across more providers.

Changes:

  • Added RemitaGateway and OpayGateway implementations.
  • Extended config (PaymentGatewayConfig) and enum (PaymentGatewayType) to support Remita/OPay.
  • Updated routing and gateway detection: NGN preference order and REM_ / OP_ reference prefix mapping; registered gateways in DI and factory.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
PayBridge.SDK/Services/PaymentService.cs Updates NGN gateway preference order and adds REM_/OP_ prefix detection for automatic verification routing.
PayBridge.SDK/Gateways/RemitaGateway.cs Adds Remita gateway implementation (init/verify/refund) and reference prefixing.
PayBridge.SDK/Gateways/OpayGateway.cs Adds OPay gateway implementation (init/verify/refund) with HMAC signing and env toggle.
PayBridge.SDK/Factories/PaymentGatewayFactory.cs Adds Remita/OPay to gateway creation and DI resolution switch.
PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs Registers Remita/OPay gateway types in DI.
PayBridge.SDK/Enums/PaymentGatewayType.cs Adds Remita = 7 and Opay = 8.
PayBridge.SDK/Dtos/PaymentGatewayConfig.cs Adds RemitaConfig and OpayConfig sections to configuration model.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +42 to +48
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();
}
Comment on lines +59 to +63
_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");
Comment on lines +173 to +178
_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");
Comment on lines +84 to +85
var doc = JsonDocument.Parse(responseBody);
var root = doc.RootElement;
Comment on lines +180 to +186
var payload = new
{
merchantId = _config.Remita.MerchantId,
rrr = request.TransactionReference,
amount = (long)(request.Amount * 100),
reason = request.Reason ?? "Customer request"
};
Comment on lines +33 to +35
_config = config ?? throw new ArgumentNullException(nameof(config));
_httpClient = httpClientFactory.CreateClient(nameof(RemitaGateway));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
Comment on lines +34 to +36
_config = config ?? throw new ArgumentNullException(nameof(config));
_httpClient = httpClientFactory.CreateClient(nameof(OpayGateway));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
Comment on lines +98 to +99
var doc = JsonDocument.Parse(responseBody);
var root = doc.RootElement;
Comment on lines +50 to +51
var hash = HMACSHA512.HashData(keyBytes, msgBytes);
return Convert.ToHexString(hash).ToLower();
@teesofttech
teesofttech force-pushed the feature/issue-13-opay-gateway branch from 1c62f59 to 312611c Compare June 6, 2026 16:05
@teesofttech
teesofttech force-pushed the feature/issue-13-opay-gateway branch from 312611c to 212e07e Compare June 6, 2026 23:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
PayBridge.SDK/Gateways/OpayGateway.cs (1)

69-69: 💤 Low value

Hardcoded payment expiration time lacks documentation.

expireAt = 30 is hardcoded without indicating the unit (minutes, seconds, hours?). Consider making this configurable via OpayConfig or at minimum document the unit and OPay's acceptable range.

♻️ Suggested improvement

Add a property to OpayConfig:

 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;
+    /// <summary>
+    /// Payment expiration time in minutes (OPay default: 30).
+    /// </summary>
+    public int ExpirationMinutes { get; set; } = 30;
 }

Then use _config.Opay.ExpirationMinutes in the payload.

🤖 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` at line 69, The payload sets expireAt
= 30 with no unit or configurability; update the design so the expiration is
configurable via OpayConfig (e.g., add an ExpirationMinutes or ExpirationSeconds
property under the Opay section) and replace the hardcoded value in OpayGateway
(the expireAt assignment) with _config.Opay.ExpirationMinutes (or the chosen
property), and ensure the property name and unit are documented in OpayConfig's
comments and any public config validation (and keep the payload unit matching
OPay's API).
🤖 Prompt for all review comments with 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.

Inline comments:
In `@PayBridge.SDK/Dtos/PaymentGatewayConfig.cs`:
- 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.

In `@PayBridge.SDK/Gateways/OpayGateway.cs`:
- Around line 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.
- Around line 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.

In `@PayBridge.SDK/Gateways/RemitaGateway.cs`:
- 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.

In `@PayBridge.SDK/Services/PaymentService.cs`:
- 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.

---

Nitpick comments:
In `@PayBridge.SDK/Gateways/OpayGateway.cs`:
- Line 69: The payload sets expireAt = 30 with no unit or configurability;
update the design so the expiration is configurable via OpayConfig (e.g., add an
ExpirationMinutes or ExpirationSeconds property under the Opay section) and
replace the hardcoded value in OpayGateway (the expireAt assignment) with
_config.Opay.ExpirationMinutes (or the chosen property), and ensure the property
name and unit are documented in OpayConfig's comments and any public config
validation (and keep the payload unit matching OPay's API).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7dfbc11d-7456-4448-bc07-d6f4d2115e46

📥 Commits

Reviewing files that changed from the base of the PR and between 2f193f4 and 212e07e.

📒 Files selected for processing (7)
  • PayBridge.SDK/Dtos/PaymentGatewayConfig.cs
  • PayBridge.SDK/Enums/PaymentGatewayType.cs
  • PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
  • PayBridge.SDK/Factories/PaymentGatewayFactory.cs
  • PayBridge.SDK/Gateways/OpayGateway.cs
  • PayBridge.SDK/Gateways/RemitaGateway.cs
  • PayBridge.SDK/Services/PaymentService.cs

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.

Comment on lines +66 to +68
returnUrl = request.RedirectUrl ?? string.Empty,
callbackUrl = request.WebhookUrl ?? string.Empty,
cancelUrl = request.RedirectUrl ?? string.Empty,

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.

Comment on lines +196 to +245
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);
}
}

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.

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.

{
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement OpayGateway (Nigeria - mobile money, wallet, QR)

2 participants