From 86adab44dbd3b1d2e330b020b3f3b479cfc789b5 Mon Sep 17 00:00:00 2001
From: Cornelius Ashley-Osuzoka
<59456456+corneliusyaovi@users.noreply.github.com>
Date: Sat, 8 Apr 2023 02:26:46 +0100
Subject: [PATCH 01/14] Add contributing guidelines
---
CONTRIBUTING.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 95 insertions(+)
create mode 100644 CONTRIBUTING.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..e0fe6a0e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,95 @@
+
+
+
+
+Thank you for taking the time to contribute to our library🙌🏾.
+
+In this section, we detail everything you need to know about contributing to this library.
+
+
+**[Code of Conduct](https://github.com/probot/template/blob/master/CODE_OF_CONDUCT.md)**
+
+## **I don't want to contribute, I have a question**
+
+Please don't raise an issue to ask a question. You can ask questions on our [forum](http://forum.flutterwave.com) or developer [slack](https://bit.ly/34Vkzcg). We have an army of Engineers on hand to answer your questions there.
+
+## How can I contribute?
+
+### Reporting a bug
+
+Have you spotted a bug? Fantastic! Before raising an issue, here are some things to do:
+
+1. Search to see if another user has reported the bug. For existing issues that are still open, add a comment instead of creating a new one.
+2. Check our forum and developer slack to confirm that we did not address it there.
+
+When you report an issue, it is important to:
+
+1. Explain the problem
+ - Use a clear and descriptive title to help us to identify the problem.
+ - Describe steps we can use to replicate the bug and be as precise as possible.
+ - Include screenshots of the error messages.
+2. Include details about your configuration and setup
+ - What version of the library are you using?
+ - Did you experience the bug on test mode or live?
+ - Do you have the recommended versions of the library dependencies?
+
+
+
+### Requesting a feature
+
+If you need an additional feature added to the library, kindly send us an email at developers@flutterwavego.com. Be sure to include the following in your request:
+
+1. A clear title that helps us to identify the requested feature.
+2. A brief description of the use case for that feature.
+3. Explain how this feature would be helpful to your integration.
+4. Library name and version.
+
+### Submitting changes (PR)
+
+Generally, you can make any of the following changes to the library:
+
+1. Bug fixes
+2. Performance improvement
+3. Documentation update
+4. Functionality change (usually new features)
+
+
+
+Follow these steps when making a pull request to the library:
+
+1. Fork the repository and create your branch from master.
+2. For all types of changes (excluding documentation updates), add tests for the changes.
+3. If you are making a functionality change, update the docs to show how to use the new feature.
+4. Ensure all your tests pass.
+5. Make sure your code lints.
+6. Write clear log messages for your commits. one-liners are fine for small changes, but bigger changes should have a more descriptive commit message (see sample below).
+7. Use present tense for commit messages, "Add feature" not "Added feature”.
+8. Ensure that you fill out all sections of the PR template.
+9. Raise the PR against the `staging`Â branch.
+10. After you submit the PR, verify that all [status checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks) are passing
+
+```markdown
+$ git commit -m "A brief summary of the commit
+>
+> A paragraph describing what changed and its impact."
+```
+
+
+
+We encourage you to contribute and help make the library better for the community. Got questions? send us a [message](https://bit.ly/34Vkzcg).
+
+Thank you.
+
+The Flutterwave team 🦋
From ae3498ef496207502b1a729291dd78792102196e Mon Sep 17 00:00:00 2001
From: Cornelius Ashley-Osuzoka
<59456456+corneliusyaovi@users.noreply.github.com>
Date: Sat, 8 Apr 2023 02:28:15 +0100
Subject: [PATCH 02/14] Add redirect to support forum
---
.github/ISSUE_TEMPLATE/config.yml | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/config.yml
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..c72eb333
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Developer Support Forum
+ url: https://forum.flutterwave.com
+ about: If you're having general trouble with your integration, Kindly contact our support team.
From 77fd11bd890d7965819855d31a32d206174e3d6e Mon Sep 17 00:00:00 2001
From: Cornelius Ashley-Osuzoka
<59456456+corneliusyaovi@users.noreply.github.com>
Date: Sat, 8 Apr 2023 02:30:24 +0100
Subject: [PATCH 03/14] Add issue template
---
.github/ISSUE_TEMPLATE/bug_report.md | 37 ++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..a01a5bef
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,37 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+Have you read our [Code of Conduct](https://github.com/Flutterwave/PHP/blob/master/CONTRIBUTING.md)? By filing an Issue, you are expected to comply with it, including treating everyone with respect.
+
+# Description
+
+
+# Steps to Reproduce
+
+1.
+2.
+3.
+
+## Expected behaviour
+
+
+## Actual behaviour
+
+
+## Reproduces how often
+
+
+# Configuration
+- API Version:
+- Environment:
+- Browser:
+- Language:
+
+# Additional Information
+
From c39d4bba736dc1f2beea975bbf29ec4f0665e817 Mon Sep 17 00:00:00 2001
From: Cornelius Ashley-Osuzoka
<59456456+corneliusyaovi@users.noreply.github.com>
Date: Thu, 10 Apr 2025 13:13:31 +0100
Subject: [PATCH 04/14] Add logs for v1.1.0
---
CHANGELOG.md | 26 +++++++++++++++++---------
1 file changed, 17 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2463140a..24856f7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
-## 1.0.6 | 2023-24-07
+## 1.1.0 | 2025-04-10
+Performance enhancements, feature updates and bugfixes.
+
+Changes include:
+- [ADDED] Test for card Preauthorization, ENAIRA, FAWRYPAY and GOOGLEPAY.
+- [FIXED] Removed enums to support PHP v7.4 and above.
+- [UPDATED] Code refactor.
+
+## 1.0.6 | 2023-07-24
Performance Optimization and Feature Update.
+
+Changes include:
- [FIXED] RequeryPayment method on checkout
- [ADDED] Support for TANZANIA MOBILE MONEY
- [ADDED] Support for FAWRY PAY
@@ -8,18 +18,16 @@ Performance Optimization and Feature Update.
- [UPDATED] Simplified the Custom Configuration Contract to allow for easy configuration with the package.
- [SECURITY & PERF] Checkout Process and Callback in processPayment.php
-## 1.0.5 | 2023-24-01
+## 1.0.5 | 2023-01-24
This change allows the package to automatically detect .env file and use the variables declared in them.
-### composer updates and bugfixes
+Changes include:
- [UPDATE] Log files generated in project directory.
- [REFACTOR] remove unused files from distribution.
-## 1.0.4 | 2022-11-06
-
-This release adds support for 7.4 and above. a new workflow for old and new tests.
-
-### Dependency updates and bugfixes
+## 1.0.4 | 2022-06-11
+This release adds support for PHP version 7.4 and above, as well as a new workflow for old and new tests.
+Changes include:
- [ADDED] Support for PHP 7.4 and above
-- [ADDED] New workflow for old and new tests
\ No newline at end of file
+- [ADDED] New workflow for old and new tests
From ed39a2291db41b4698d586f15c96a61a62c4e86c Mon Sep 17 00:00:00 2001
From: Cornelius Ashley-Osuzoka
<59456456+corneliusyaovi@users.noreply.github.com>
Date: Thu, 10 Apr 2025 13:21:35 +0100
Subject: [PATCH 05/14] Update README.md
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 6a89584e..7719dffb 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ If you do not want to make use of composer. each [release](https://github.com/Fl
### Installation via Composer.
-To install the package via Composer, run the following command.
+To install the package via Composer, run the following command:
```shell
composer require flutterwavedev/flutterwave-v3
```
@@ -67,7 +67,7 @@ Save your PUBLIC_KEY, SECRET_KEY, ENV in the `.env` file
```bash
cp .env.example .env
```
-Your `.env` file should look this.
+Your `.env` file should look this. Make sure to retrieve your API keys from your dashboard.
```env
FLW_PUBLIC_KEY=FLWSECK_TEST-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-X
From add719b366bb48515c405edbdf638506ad230327 Mon Sep 17 00:00:00 2001
From: Abraham Olaobaju
Date: Thu, 11 Jun 2026 15:00:16 +0100
Subject: [PATCH 06/14] initial sigonz update commit
---
composer.json | 5 +-
setup.php | 1 +
src/AbstractPayment.php | 1 +
src/Config/AbstractConfig.php | 8 ++
src/Contract/ConfigInterface.php | 1 +
src/Flutterwave.php | 31 ++++-
src/Library/Modal.php | 13 ++
src/Monitoring/SignozServiceLogger.php | 185 +++++++++++++++++++++++++
8 files changed, 239 insertions(+), 6 deletions(-)
create mode 100644 src/Monitoring/SignozServiceLogger.php
diff --git a/composer.json b/composer.json
index dc3a8cd4..e80a995e 100644
--- a/composer.json
+++ b/composer.json
@@ -30,7 +30,7 @@
"mockery/mockery": ">=1.2",
"symfony/var-dumper": "5.4.13",
"phpstan/phpstan": "^1.9",
- "pestphp/pest": "^1.22",
+ "pestphp/pest": "1.22",
"nunomaduro/phpinsights": "^2.6",
"eloquent/liberator": "^3.0",
"squizlabs/php_codesniffer": "3.*",
@@ -45,6 +45,9 @@
}
],
"config": {
+ "platform": {
+ "php": "7.4"
+ },
"allow-plugins": {
"pestphp/pest-plugin": true,
"dealerdirect/phpcodesniffer-composer-installer": true
diff --git a/setup.php b/setup.php
index 9614398d..3c4ff1d3 100644
--- a/setup.php
+++ b/setup.php
@@ -2,6 +2,7 @@
use Flutterwave\Helper;
use Dotenv\Dotenv;
+use Flutterwave\Monitoring\SignozServiceLogger;
$flutterwave_installation = 'composer';
diff --git a/src/AbstractPayment.php b/src/AbstractPayment.php
index b067c6a7..28b25345 100644
--- a/src/AbstractPayment.php
+++ b/src/AbstractPayment.php
@@ -7,6 +7,7 @@
use Flutterwave\Contract\ConfigInterface;
use Flutterwave\EventHandlers\EventHandlerInterface;
use Flutterwave\Helper\EnvVariables;
+use Flutterwave\Monitoring\SignozServiceLogger;
use Flutterwave\Traits\ApiOperations as Api;
use Flutterwave\Traits\PayloadOperations as Payload;
use Psr\Log\LoggerInterface;
diff --git a/src/Config/AbstractConfig.php b/src/Config/AbstractConfig.php
index d0a2b1a7..247b9db2 100644
--- a/src/Config/AbstractConfig.php
+++ b/src/Config/AbstractConfig.php
@@ -7,6 +7,7 @@
use Flutterwave\EventHandlers\EventHandlerInterface;
use Flutterwave\Flutterwave;
use Flutterwave\Contract\ConfigInterface;
+use Flutterwave\Monitoring\SignozServiceLogger;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use Monolog\Logger;
@@ -26,6 +27,7 @@ abstract class AbstractConfig
public Logger $logger;
protected string $secret;
protected string $public;
+ public SignozServiceLogger $signoz;
protected static ?ConfigInterface $instance = null;
protected string $env;
@@ -53,6 +55,7 @@ protected function __construct(string $secret_key, string $public_key, string $e
$log = new Logger('Flutterwave/PHP');
$this->logger = $log;
+ $this->signoz = new SignozServiceLogger($this->http, $this->getPublicKey(), $this->getEnv(), null, EnvVariables::SDK_VERSION);
}
abstract public static function setUp(
@@ -84,4 +87,9 @@ public static function getDefaultTransactionPrefix(): string
{
return self::DEFAULT_PREFIX;
}
+
+ public function getSignoz(): SignozServiceLogger
+ {
+ return $this->signoz;
+ }
}
diff --git a/src/Contract/ConfigInterface.php b/src/Contract/ConfigInterface.php
index 8fcaac69..fdd9b5ad 100644
--- a/src/Contract/ConfigInterface.php
+++ b/src/Contract/ConfigInterface.php
@@ -4,6 +4,7 @@
namespace Flutterwave\Contract;
+use Flutterwave\Monitoring\SignozServiceLogger;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
diff --git a/src/Flutterwave.php b/src/Flutterwave.php
index 01d18251..e3e9ef8f 100644
--- a/src/Flutterwave.php
+++ b/src/Flutterwave.php
@@ -8,6 +8,7 @@
use Flutterwave\EventHandlers\EventHandlerInterface;
use Flutterwave\Exception\ApiException;
use Flutterwave\Helper\CheckCompatibility;
+use Flutterwave\Monitoring\SignozServiceLogger;
use Flutterwave\Traits\PaymentFactory;
use Flutterwave\Traits\Setup\Configure;
use Flutterwave\Library\Modal;
@@ -30,8 +31,7 @@ class Flutterwave extends AbstractPayment
/**
* Flutterwave Construct
*
- * @param string $prefix
- * @param bool $overrideRefWithPrefix Set this parameter to true to use your prefix as the transaction reference
+ * @throws \Exception
*/
public function __construct()
{
@@ -75,7 +75,7 @@ public function setPaymentOptions(string $paymentOptions): object
/**
* get event handler.
*
- * @param string $paymentOptions The allowed payment methods. Can be card, account or both
+ * @return EventHandlerInterface
*/
public function getEventHandler()
{
@@ -246,6 +246,10 @@ public function requeryTransaction(string $referenceNumber): object
if (isset($this->handler)) {
$this->handler->onRequery($this->txref);
}
+ /** @var SignozServiceLogger $signoz */
+ $signoz = self::$config->getSignoz();
+ $appId = $signoz->getAppId();
+ $environment = $signoz->getCurrentEnvironment();
$data = [
'id' => (int) $referenceNumber,
@@ -260,12 +264,20 @@ public function requeryTransaction(string $referenceNumber): object
if ($response->status === 'success') {
if ($response->data && $response->data->status === 'successful') {
$this->logger->notice('Requeryed a successful transaction....' . json_encode($response->data));
+ $signoz->trackRequestSent($appId, $environment, 'GET', $referenceNumber, $url );
// Handle successful.
if (isset($this->handler)) {
+ if( 'production' === $environment ) {
+ $final_currency = $response->data->currency;
+ $final_amount = $response->data->amount;
+ $payment_type = $response->data->payment_type;
+ $final_fee = $response->data->app_fee;
+ $signoz->trackTransaction($appId,$referenceNumber, $final_currency, (float) $final_amount, $payment_type, (float) $final_fee);
+ }
$this->handler->onSuccessful($response->data);
}
} elseif ($response->data && $response->data->status === 'failed') {
- // Handle Failure
+ // Handle Failure.
$this->logger->warning('Requeryed a failed transaction....' . json_encode($response->data));
if (isset($this->handler)) {
$this->handler->onFailure($response->data);
@@ -280,6 +292,7 @@ public function requeryTransaction(string $referenceNumber): object
if ($this->requeryCount > 4) {
// Now you have to setup a queue by force. We couldn't get a status in 5 requeries.
if (isset($this->handler)) {
+ $signoz->trackError($appId, 'TIMEOUT_ERROR', 'timedout while requerying transaction with id: ' . $referenceNumber);
$this->handler->onTimeout($this->txref, $response->data);
}
} else {
@@ -290,7 +303,8 @@ public function requeryTransaction(string $referenceNumber): object
}
}
} else {
- // Handle Requery Error
+ // Handle Requery Error.
+ $signoz->trackError($appId, 'REQUERY_ERROR', 'Failed to requery transaction with id: ' . $referenceNumber);
if (isset($this->handler)) {
$this->handler->onRequeryError($response->data);
}
@@ -305,6 +319,13 @@ public function initialize(): void
{
$this->createCheckSum();
+ /** @var SignozServiceLogger $signoz */
+ $signoz = self::$config->getSignoz();
+ $appId = $signoz->getAppId();
+ $environment = $signoz->getCurrentEnvironment();
+
+ $signoz->trackRequestSent($appId, $environment, 'GET', $this->txref, '/inline');
+
$this->logger->info('Rendering Payment Modal..');
echo '';
diff --git a/src/Library/Modal.php b/src/Library/Modal.php
index f6273a7d..bd90d16c 100644
--- a/src/Library/Modal.php
+++ b/src/Library/Modal.php
@@ -10,6 +10,7 @@
use Flutterwave\EventHandlers\EventHandlerInterface;
use Flutterwave\Helper\CheckoutHelper;
+use Flutterwave\Monitoring\SignozServiceLogger;
use Flutterwave\Service\Service as Http;
use Flutterwave\Entities\Payload;
use Psr\Log\LoggerInterface;
@@ -107,6 +108,11 @@ public function getHtml()
return $this->returnUrl();
}
+ /** @var SignozServiceLogger $signoz */
+ $signoz = self::$config->getSignoz();
+ $appId = $signoz->getAppId();
+ $environment = $signoz->getCurrentEnvironment();
+
$default_options = CheckoutHelper::getDefaultPaymentOptions();
$payload = $this->payload->toArray('modal');
@@ -151,6 +157,7 @@ public function getHtml()
$html .= '';
$this->logger->info('Rendered Payment Modal Successfully..');
+ $signoz->trackRequestSent($appId, $environment, 'GET', $payload['tx_ref'], '/inline');
return $html;
}
@@ -161,6 +168,11 @@ public function getUrl()
return $this->returnHtml();
}
+ /** @var SignozServiceLogger $signoz */
+ $signoz = self::$config->getSignoz();
+ $appId = $signoz->getAppId();
+ $environment = $signoz->getCurrentEnvironment();
+
$default_options = CheckoutHelper::getDefaultPaymentOptions();
$payload = $this->payload->toArray('modal');
$currency = $payload['currency'];
@@ -172,6 +184,7 @@ public function getUrl()
$payload['customer']['name'] = $payload['customer']['fullname'];
$this->logger->info('Generating Payment link for [' . $payload['tx_ref'] . ']');
+ $signoz->trackRequestSent($appId, $environment, 'GET', $payload['tx_ref'], '/payments');
$response = (new Http(self::$config))->request($payload, 'POST', 'payments');
return $response->data->link;
}
diff --git a/src/Monitoring/SignozServiceLogger.php b/src/Monitoring/SignozServiceLogger.php
new file mode 100644
index 00000000..164bcff9
--- /dev/null
+++ b/src/Monitoring/SignozServiceLogger.php
@@ -0,0 +1,185 @@
+httpClient = $httpClient;
+ $this->cache = $cache;
+ $this->libraryVersion = $libraryVersion;
+ $this->publicKey = $publicKey;
+ $this->environment = $environment;
+ }
+
+ public function getAppId() {
+ if (!empty($this->appId)) {
+ return $this->appId;
+ }
+
+ $merchantId = $this->getMerchantId($this->publicKey);
+ if (!empty($merchantId)) {
+ $this->appId = $merchantId;
+ return $this->appId;
+ }
+ return $this->publicKey;
+ }
+
+ public function getCurrentEnvironment(): string
+ {
+ return $this->environment !== 'production' ? 'sandbox' : 'production';
+ }
+
+ public function getMerchantId(string $publicKey) {
+ try {
+ $response = $this->httpClient->request('GET', self::MERCHANT_INFO . $this->publicKey, [
+ 'headers' => [
+ 'Content-Type' => 'application/json'
+ ]
+ ]);
+
+ $result = json_decode($response->getBody()->getContents(), true);
+
+ if(!empty($result) && isset($result['mn'])) {
+ return $result['mn'];
+ }
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ return null;
+ }
+
+ public function trackAppCreated(
+ string $publicKey,
+ string $merchantId
+ ): void {
+ if (self::$appCreatedSent) {
+ return;
+ }
+ $this->send('app.created', [
+ 'app_id' => $merchantId,
+ 'public_key' => $publicKey,
+ 'library' => self::LIBRARY,
+ 'library_version' => $this->libraryVersion,
+ ]);
+ }
+
+ public function trackRequestSent(
+ string $appId,
+ string $environment,
+ string $method,
+ string $reference,
+ string $path
+ ): void {
+ $payload = [
+ 'app_id' => $appId,
+ 'environment' => $environment,
+ 'api_version' => 'v3',
+ 'library_version' => $this->libraryVersion,
+ 'method' => $method,
+ 'path' => $path,
+ 'reference' => $reference,
+ ];
+
+ $cacheKey = sprintf(
+ 'signoz:request_sent:%s',
+ $reference
+ );
+
+ if ($this->cache !== null) {
+
+ try {
+ if ($this->cache->has($cacheKey)) {
+ return;
+ }
+
+ $this->cache->set($cacheKey, true, 300);
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ }
+
+ $this->send('request.sent', $payload);
+ }
+
+ public function trackTransaction(
+ string $appId,
+ string $reference,
+ string $currency,
+ float $amount,
+ string $method,
+ float $fee
+ ): void {
+ $this->send('app.transaction', [
+ 'app_id' => $appId,
+ 'reference' => $reference,
+ 'currency' => $currency,
+ 'amount' => $amount,
+ 'fee' => $fee,
+ 'method' => $method,
+ ]);
+ }
+
+ public function trackError(
+ string $appId,
+ string $errorCode,
+ string $errorMessage
+ ): void {
+ $this->send('app.error', [
+ 'app_id' => $appId,
+ 'library' => self::LIBRARY,
+ 'library_version' => $this->libraryVersion,
+ 'error_code' => $errorCode,
+ 'error_message' => $errorMessage,
+ ]);
+ }
+
+ private function send(string $eventName, array $data): void
+ {
+ try {
+ $this->httpClient->request('POST', self::BASE_URL . '/events', [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'x-api-key' => self::API_KEY,
+ ],
+ 'json' => [
+ 'name' => $eventName,
+ 'data' => $data,
+ 'timestamp' => gmdate('Y-m-d\TH:i:s.000\Z'),
+ ],
+
+ // fire-and-forget-ish
+ 'timeout' => 2,
+ 'connect_timeout' => 1,
+ ]);
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ }
+}
\ No newline at end of file
From 0881036cfe21599b63614bf06a373c5e61a17cc5 Mon Sep 17 00:00:00 2001
From: bajoski34
Date: Thu, 11 Jun 2026 18:21:34 +0100
Subject: [PATCH 07/14] setup Ona workspace setup
---
.devcontainer/Dockerfile | 15 +++++++++++++++
.devcontainer/devcontainer.json | 8 ++++++++
2 files changed, 23 insertions(+)
create mode 100644 .devcontainer/Dockerfile
create mode 100644 .devcontainer/devcontainer.json
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 00000000..bd0eeb36
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,15 @@
+FROM mcr.microsoft.com/devcontainers/base:0-focal
+
+RUN apt-get update && \
+ apt-get install -y software-properties-common && \
+ add-apt-repository ppa:ondrej/php -y && \
+ apt-get update && \
+ apt-get install -y \
+ php7.4 php7.4-cli php7.4-common php7.4-curl \
+ php7.4-mysql php7.4-bcmath php7.4-soap php7.4-zip php7.4-intl \
+ php7.4-gd php7.4-xsl php7.4-dom php7.4-mbstring \
+ unzip && \
+ rm -rf /var/lib/apt/lists/*
+
+RUN curl -sS https://getcomposer.org/installer | php -- \
+ --install-dir=/usr/local/bin --filename=composer
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..779933a9
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,8 @@
+{
+ "name": "Ona",
+ "build": {
+ "context": ".",
+ "dockerfile": "Dockerfile"
+ },
+ "postCreateCommand": "ona automations update .ona/automations.yaml"
+}
\ No newline at end of file
From d62d54a6efe7a10514e6d06c3316f820994f8e79 Mon Sep 17 00:00:00 2001
From: Abraham Jesulayomi Olaobaju
<39011309+bajoski34@users.noreply.github.com>
Date: Thu, 11 Jun 2026 21:05:37 +0000
Subject: [PATCH 08/14] update signoz service implementation
---
src/Config/AbstractConfig.php | 4 ++-
src/Controller/PaymentController.php | 4 +--
src/Helper/Config.php | 2 +-
src/Monitoring/SignozServiceLogger.php | 49 ++++++++++++++++++++------
src/Service/Service.php | 28 +++++++++++++++
tests/Resources/Setup/Config.php | 2 +-
6 files changed, 72 insertions(+), 17 deletions(-)
diff --git a/src/Config/AbstractConfig.php b/src/Config/AbstractConfig.php
index 247b9db2..59e6c600 100644
--- a/src/Config/AbstractConfig.php
+++ b/src/Config/AbstractConfig.php
@@ -22,7 +22,7 @@ abstract class AbstractConfig
public const SECRET_KEY = 'SECRET_KEY';
public const ENCRYPTION_KEY = 'ENCRYPTION_KEY';
public const ENV = 'ENV';
- public const DEFAULT_PREFIX = 'FW|PHP';
+ public const DEFAULT_PREFIX = 'FW_PHP';
public const LOG_FILE_NAME = 'flutterwave-php.log';
public Logger $logger;
protected string $secret;
@@ -56,6 +56,8 @@ protected function __construct(string $secret_key, string $public_key, string $e
$log = new Logger('Flutterwave/PHP');
$this->logger = $log;
$this->signoz = new SignozServiceLogger($this->http, $this->getPublicKey(), $this->getEnv(), null, EnvVariables::SDK_VERSION);
+ // Track app initialization once per lifecycle
+ $this->signoz->trackAppCreated($this->getPublicKey());
}
abstract public static function setUp(
diff --git a/src/Controller/PaymentController.php b/src/Controller/PaymentController.php
index 00209f8f..13f18e93 100644
--- a/src/Controller/PaymentController.php
+++ b/src/Controller/PaymentController.php
@@ -4,10 +4,8 @@
namespace Flutterwave\Controller;
-use Flutterwave\EventHandlers\ModalEventHandler;
use Flutterwave\EventHandlers\EventHandlerInterface;
use Flutterwave\Flutterwave;
-use Flutterwave\Entities\Payload;
use Flutterwave\Library\Modal;
use Flutterwave\Service\Transactions;
@@ -43,7 +41,7 @@ private function getRequestMethod(): string
public function __call(string $name, array $args)
{
- if ($this->routes[$name] !== $this->$requestMethod) {
+ if ($this->routes[$name] !== $this->requestMethod) {
// Todo: 404();
echo "Unauthorized page!";
}
diff --git a/src/Helper/Config.php b/src/Helper/Config.php
index 54b11418..ecd62762 100644
--- a/src/Helper/Config.php
+++ b/src/Helper/Config.php
@@ -24,7 +24,7 @@ class Config implements ConfigInterface
public const SECRET_KEY = 'SECRET_KEY';
public const ENCRYPTION_KEY = 'ENCRYPTION_KEY';
public const ENV = 'ENV';
- public const DEFAULT_PREFIX = 'FW|PHP';
+ public const DEFAULT_PREFIX = 'FW_PHP';
public const LOG_FILE_NAME = 'flutterwave-php.log';
protected Logger $logger;
private string $secret;
diff --git a/src/Monitoring/SignozServiceLogger.php b/src/Monitoring/SignozServiceLogger.php
index 164bcff9..b3896810 100644
--- a/src/Monitoring/SignozServiceLogger.php
+++ b/src/Monitoring/SignozServiceLogger.php
@@ -2,7 +2,9 @@
namespace Flutterwave\Monitoring;
+use Flutterwave\Helper\EnvVariables;
use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\RequestException;
use Psr\SimpleCache\CacheInterface;
class SignozServiceLogger
@@ -18,7 +20,7 @@ class SignozServiceLogger
private ?CacheInterface $cache;
private string $libraryVersion;
- private string $appId;
+ private ?string $appId = null;
private string $publicKey;
@@ -45,10 +47,10 @@ public function getAppId() {
$merchantId = $this->getMerchantId($this->publicKey);
if (!empty($merchantId)) {
- $this->appId = $merchantId;
+ $this->appId = $this->normalizeAppId($merchantId);
return $this->appId;
}
- return $this->publicKey;
+ return $this->normalizeAppId($this->publicKey);
}
public function getCurrentEnvironment(): string
@@ -58,7 +60,7 @@ public function getCurrentEnvironment(): string
public function getMerchantId(string $publicKey) {
try {
- $response = $this->httpClient->request('GET', self::MERCHANT_INFO . $this->publicKey, [
+ $response = $this->httpClient->request('GET', self::MERCHANT_INFO . $publicKey, [
'headers' => [
'Content-Type' => 'application/json'
]
@@ -76,18 +78,27 @@ public function getMerchantId(string $publicKey) {
}
public function trackAppCreated(
- string $publicKey,
- string $merchantId
+ string $publicKey
): void {
if (self::$appCreatedSent) {
return;
}
+
+ $merchantId = $this->getMerchantId($publicKey);
+
+ if (empty($merchantId)) {
+ return;
+ }
+
$this->send('app.created', [
- 'app_id' => $merchantId,
+ 'app_id' => $this->normalizeAppId($merchantId),
+ 'client_id' => null,
'public_key' => $publicKey,
'library' => self::LIBRARY,
'library_version' => $this->libraryVersion,
]);
+
+ self::$appCreatedSent = true;
}
public function trackRequestSent(
@@ -98,9 +109,9 @@ public function trackRequestSent(
string $path
): void {
$payload = [
- 'app_id' => $appId,
+ 'app_id' => $this->normalizeAppId($appId),
'environment' => $environment,
- 'api_version' => 'v3',
+ 'api_version' => EnvVariables::VERSION,
'library_version' => $this->libraryVersion,
'method' => $method,
'path' => $path,
@@ -137,7 +148,7 @@ public function trackTransaction(
float $fee
): void {
$this->send('app.transaction', [
- 'app_id' => $appId,
+ 'app_id' => $this->normalizeAppId($appId),
'reference' => $reference,
'currency' => $currency,
'amount' => $amount,
@@ -152,7 +163,7 @@ public function trackError(
string $errorMessage
): void {
$this->send('app.error', [
- 'app_id' => $appId,
+ 'app_id' => $this->normalizeAppId($appId),
'library' => self::LIBRARY,
'library_version' => $this->libraryVersion,
'error_code' => $errorCode,
@@ -178,8 +189,24 @@ private function send(string $eventName, array $data): void
'timeout' => 2,
'connect_timeout' => 1,
]);
+ } catch (RequestException $e) {
+ $response = $e->getResponse();
+
+ if ($response !== null && $response->getStatusCode() === 422) {
+ $responseBody = (string) $response->getBody();
+ error_log(sprintf(
+ 'Signoz validation error (422) while sending %s: %s',
+ $eventName,
+ $responseBody
+ ));
+ }
} catch (\Throwable $e) {
// observability must never break payments
}
}
+
+ private function normalizeAppId(string $appId): string
+ {
+ return preg_replace('/\s+/', '-', trim($appId)) ?? $appId;
+ }
}
\ No newline at end of file
diff --git a/src/Service/Service.php b/src/Service/Service.php
index 0ad3760a..8bc9309f 100644
--- a/src/Service/Service.php
+++ b/src/Service/Service.php
@@ -12,6 +12,7 @@
use Flutterwave\Factories\PayloadFactory as Payload;
use Flutterwave\Helper\Config;
use Flutterwave\Helper\EnvVariables;
+use Flutterwave\Monitoring\SignozServiceLogger;
use Psr\Http\Client\ClientInterface;
use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
@@ -27,6 +28,7 @@ class Service implements ServiceInterface
public ?FactoryInterface $customer;
protected string $baseUrl;
protected LoggerInterface $logger;
+ protected SignozServiceLogger $signoz;
protected ConfigInterface $config;
protected string $url;
protected string $secret;
@@ -42,6 +44,7 @@ public function __construct(?ConfigInterface $config = null)
$this->config = is_null($config) ? self::$spareConfig : $config;
$this->http = $this->config->getHttp();
$this->logger = $this->config->getLoggerInstance();
+ $this->signoz = $this->config->getSignoz();
$this->secret = $this->config->getSecretKey();
$this->url = EnvVariables::BASE_URL . '/';
$this->baseUrl = EnvVariables::BASE_URL;
@@ -68,6 +71,7 @@ public function request(
$secret = $this->config->getSecretKey();
$url = $this->getUrl($overrideUrl, $additionalurl);
+ $reference = $this->resolveSignozReference($data, $additionalurl, $verb);
switch ($verb) {
case 'POST':
@@ -119,6 +123,10 @@ public function request(
}
$body = $response->getBody()->getContents();
+ $appId = $this->signoz->getAppId();
+ $environment = $this->signoz->getCurrentEnvironment();
+ $this->signoz->trackRequestSent($appId, $environment, $verb, $reference, $additionalurl);
+
return json_decode($body);
}
@@ -166,4 +174,24 @@ private function getUrl(bool $overrideUrl, string $additionalurl): string
return $this->url . $additionalurl;
}
+
+ private function resolveSignozReference(?array $data, string $additionalurl, string $verb): string
+ {
+ if ($data !== null) {
+ foreach (['tx_ref', 'reference', 'order_ref', 'batch_id', 'id'] as $key) {
+ if (isset($data[$key]) && $data[$key] !== '') {
+ return (string) $data[$key];
+ }
+ }
+
+ $encodedData = json_encode($data);
+ if ($encodedData === false) {
+ $encodedData = serialize($data);
+ }
+
+ return hash('sha256', $verb . '|' . $additionalurl . '|' . $encodedData);
+ }
+
+ return $verb . '|' . $additionalurl;
+ }
}
diff --git a/tests/Resources/Setup/Config.php b/tests/Resources/Setup/Config.php
index eea6a86a..f628ca0d 100644
--- a/tests/Resources/Setup/Config.php
+++ b/tests/Resources/Setup/Config.php
@@ -20,7 +20,7 @@ class Config implements ConfigInterface
public const SECRET_KEY = 'SECRET_KEY';
public const ENCRYPTION_KEY = 'ENCRYPTION_KEY';
public const ENV = 'ENV';
- public const DEFAULT_PREFIX = 'FW|PHP';
+ public const DEFAULT_PREFIX = 'FW_PHP';
public const LOG_FILE_NAME = 'flutterwave-php.log';
protected Logger $logger;
private string $secret;
From acdcfefe7b2910fcb33c011e3591aa077e660003 Mon Sep 17 00:00:00 2001
From: Abraham Jesulayomi Olaobaju
<39011309+bajoski34@users.noreply.github.com>
Date: Fri, 12 Jun 2026 10:44:04 +0000
Subject: [PATCH 09/14] clean up
---
.github/workflows/artifact-release.yml | 7 +
.github/workflows/change-review.yml | 11 +-
.github/workflows/package-publish.yml | 2 +-
paymentForm.php | 4 +-
processPayment.php | 13 +-
setup.php | 1 -
src/Config/AbstractConfig.php | 5 +-
src/Controller/PaymentController.php | 24 +---
src/EventHandlers/ModalEventHandler.php | 81 +++++++++---
src/Flutterwave.php | 32 +++--
src/Monitoring/SignozServiceLogger.php | 58 ++++++--
src/Service/Service.php | 125 ++++++++++--------
src/Traits/Setup/Configure.php | 14 ++
.../Monitoring/SignozServiceLoggerTest.php | 97 ++++++++++++++
14 files changed, 347 insertions(+), 127 deletions(-)
create mode 100644 tests/Unit/Monitoring/SignozServiceLoggerTest.php
diff --git a/.github/workflows/artifact-release.yml b/.github/workflows/artifact-release.yml
index 05fb2b2e..79316eeb 100644
--- a/.github/workflows/artifact-release.yml
+++ b/.github/workflows/artifact-release.yml
@@ -30,6 +30,13 @@ jobs:
id: tag
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV"
+ - name: "Inject SigNoz API key"
+ shell: bash
+ env:
+ SIGNOZ_API_KEY: ${{ secrets.SIGNOZ_API_KEY }}
+ run: |
+ sed -i "s|%%SIGNOZ_API_KEY%%|${SIGNOZ_API_KEY}|g" src/Monitoring/SignozServiceLogger.php
+
- name: Create release artifact
run: |
mkdir -p build
diff --git a/.github/workflows/change-review.yml b/.github/workflows/change-review.yml
index 8d3895ca..2bfbd7ca 100644
--- a/.github/workflows/change-review.yml
+++ b/.github/workflows/change-review.yml
@@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- php: [7.4, 8.1, 8.2, 8.3]
+ php: [7.4, 8.1, 8.2, 8.3, 8.4]
env:
XDEBUG_MODE: coverage
@@ -22,6 +22,7 @@ jobs:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
ENV: ${{ secrets.ENV }}
+ SIGNOZ_API_KEY: ${{ secrets.SIGNOZ_API_KEY }}
steps:
- uses: actions/checkout@v3
@@ -48,6 +49,13 @@ jobs:
- name: Install dependencies
run: composer install --prefer-dist --no-progress
+ - name: "Inject SigNoz API key"
+ shell: bash
+ env:
+ SIGNOZ_API_KEY: ${{ secrets.SIGNOZ_API_KEY }}
+ run: |
+ sed -i "s|%%SIGNOZ_API_KEY%%|${SIGNOZ_API_KEY}|g" src/Monitoring/SignozServiceLogger.php
+
- name: run unit tests and coverage scan
run: ./vendor/bin/pest --coverage --min=20 --coverage-clover ./coverage.xml
env:
@@ -55,7 +63,6 @@ jobs:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
ENV: ${{ secrets.ENV }}
-
- name: Upload to Codecov
uses: codecov/codecov-action@v2
with:
diff --git a/.github/workflows/package-publish.yml b/.github/workflows/package-publish.yml
index 5d005d22..65df0196 100644
--- a/.github/workflows/package-publish.yml
+++ b/.github/workflows/package-publish.yml
@@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- php: [7.4, 8.1, 8.2]
+ php: [7.4, 8.1, 8.2, 8.3, 8.4]
steps:
- uses: actions/checkout@v2
diff --git a/paymentForm.php b/paymentForm.php
index 34bb9d59..1090588f 100644
--- a/paymentForm.php
+++ b/paymentForm.php
@@ -43,9 +43,9 @@
//= uniqid() ?>
-
+
-
+
diff --git a/processPayment.php b/processPayment.php
index 0946145a..b595865b 100644
--- a/processPayment.php
+++ b/processPayment.php
@@ -11,7 +11,7 @@
use Flutterwave\Library\Modal;
use \Flutterwave\Config\ForkConfig;
-// start a session.
+// start a session for redirect metadata.
session_start();
// Define custom config.
@@ -39,9 +39,15 @@
$controller = new PaymentController( $client, $customHandler, $modalType );
} catch(\Exception $e ) {
echo $e->getMessage();
+ exit();
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
+ if ($controller === null) {
+ echo 'Unable to initialize payment controller.';
+ exit();
+ }
+
$request = $_REQUEST;
$request['redirect_url'] = $_SERVER['HTTP_ORIGIN'] . $_SERVER['REQUEST_URI'];
try {
@@ -54,6 +60,11 @@
$request = $_GET;
# Confirming Payment.
if(isset($request['tx_ref'])) {
+ if ($controller === null) {
+ echo 'Unable to initialize payment controller.';
+ exit();
+ }
+
$controller->callback( $request );
} else {
diff --git a/setup.php b/setup.php
index 3c4ff1d3..9614398d 100644
--- a/setup.php
+++ b/setup.php
@@ -2,7 +2,6 @@
use Flutterwave\Helper;
use Dotenv\Dotenv;
-use Flutterwave\Monitoring\SignozServiceLogger;
$flutterwave_installation = 'composer';
diff --git a/src/Config/AbstractConfig.php b/src/Config/AbstractConfig.php
index 59e6c600..3bb20160 100644
--- a/src/Config/AbstractConfig.php
+++ b/src/Config/AbstractConfig.php
@@ -15,6 +15,8 @@
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use Flutterwave\Helper\EnvVariables;
+use Symfony\Component\Cache\Adapter\FilesystemAdapter;
+use Symfony\Component\Cache\Psr16Cache;
abstract class AbstractConfig
{
@@ -55,7 +57,8 @@ protected function __construct(string $secret_key, string $public_key, string $e
$log = new Logger('Flutterwave/PHP');
$this->logger = $log;
- $this->signoz = new SignozServiceLogger($this->http, $this->getPublicKey(), $this->getEnv(), null, EnvVariables::SDK_VERSION);
+ $cache = new Psr16Cache(new FilesystemAdapter('flutterwave_signoz'));
+ $this->signoz = new SignozServiceLogger($this->http, $this->getPublicKey(), $this->getEnv(), $cache, EnvVariables::SDK_VERSION);
// Track app initialization once per lifecycle
$this->signoz->trackAppCreated($this->getPublicKey());
}
diff --git a/src/Controller/PaymentController.php b/src/Controller/PaymentController.php
index 13f18e93..b9e861d4 100644
--- a/src/Controller/PaymentController.php
+++ b/src/Controller/PaymentController.php
@@ -45,10 +45,10 @@ public function __call(string $name, array $args)
// Todo: 404();
echo "Unauthorized page!";
}
- call_user_method_array($name, $this, $args);
+ call_user_func_array([$this, $name], $args);
}
- private function handleSessionData( array $request )
+ private function handleSessionData(array $request): void
{
$_SESSION['success_url'] = $request['success_url'];
$_SESSION['failure_url'] = $request['failure_url'];
@@ -59,10 +59,8 @@ private function handleSessionData( array $request )
public function process(array $request)
{
$this->handleSessionData($request);
-
- try {
- $_SESSION['p'] = $this->client;
+ try {
if('inline' === $this->modalType ) {
echo $this->client
->eventHandler($this->handler)
@@ -85,23 +83,11 @@ public function callback(array $request)
$status = $request['status'];
if (empty($tx_ref)) {
- session_destroy();
- }
-
- if (!isset($_SESSION['p'])) {
- echo "session expired!. please refresh you browser.";
+ echo 'Missing transaction reference.';
exit();
}
- $payment = $_SESSION['p'];
-
- // $payment::setUp([
- // 'secret_key' => 'FLWSECK_TEST-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-X',
- // 'public_key' => 'FLWPUBK_TEST-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-X',
- // 'encryption_key' => 'FLWSECK_XXXXXXXXXXXXXXXX',
- // 'environment' => 'staging'
- // ]);
-
+ $payment = $this->client;
$payment::bootstrap();
if ('cancelled' === $status) {
diff --git a/src/EventHandlers/ModalEventHandler.php b/src/EventHandlers/ModalEventHandler.php
index ae5fcb88..d53dbd7e 100644
--- a/src/EventHandlers/ModalEventHandler.php
+++ b/src/EventHandlers/ModalEventHandler.php
@@ -4,8 +4,13 @@
namespace Flutterwave\EventHandlers;
+use Flutterwave\Service\Transactions;
+
class ModalEventHandler implements EventHandlerInterface
{
+ private ?string $success_url = null;
+ private ?string $failure_url = null;
+
/**
* This is called when the Rave class is initialized
* */
@@ -15,6 +20,26 @@ public function onInit($initializationData): void
// Save the transaction to your DB.
}
+ public function getSuccessUrl(): ?string
+ {
+ return $this->success_url;
+ }
+
+ public function setSuccessUrl(?string $success_url): void
+ {
+ $this->success_url = $success_url;
+ }
+
+ public function getFailureUrl(): ?string
+ {
+ return $this->failure_url;
+ }
+
+ public function setFailureUrl(?string $failure_url): void
+ {
+ $this->failure_url = $failure_url;
+ }
+
/**
* This is called only when a transaction is successful
* */
@@ -31,26 +56,34 @@ public function onSuccessful($transactionData): void
// Update the transaction to note that you have given value for the transaction.
// You can also redirect to your success page from here.
if ($transactionData->status === 'successful') {
- $currency = $_SESSION['currency'];
- $amount = $_SESSION['amount'];
+ $currency = $_SESSION['currency'] ?? ($transactionData->currency ?? null);
+ $amount = $_SESSION['amount'] ?? ($transactionData->amount ?? null);
- if ($transactionData->currency === $currency && floatval($transactionData->amount) === floatval($amount)) {
- header('Location: ' . $_SESSION['success_url']);
- session_destroy();
- }
+ if ($currency !== null && $amount !== null) {
+ if ($transactionData->currency === $currency && floatval($transactionData->amount) === floatval($amount)) {
+ if (!empty($_SESSION['success_url'])) {
+ header('Location: ' . $_SESSION['success_url']);
+ }
+ session_destroy();
+ }
- if ($transactionData->currency === $currency && floatval($transactionData->amount) < floatval($amount)) {
- // TODO: replace this a custom action.
- echo "This Event Handler is an Implementation of " . __NAMESPACE__ . "\EventHandlerInterface ";
- echo "Partial Payment Made ! replace this with your own action! ";
- session_destroy();
- }
+ if ($transactionData->currency === $currency && floatval($transactionData->amount) < floatval($amount)) {
+ // TODO: replace this a custom action.
+ echo "This Event Handler is an Implementation of " . __NAMESPACE__ . "\EventHandlerInterface ";
+ echo "Partial Payment Made ! replace this with your own action! ";
+ session_destroy();
+ }
- if ($transactionData->currency !== $currency && floatval($transactionData->amount) === floatval($amount)) {
- // TODO: replace this a custom action.
+ if ($transactionData->currency !== $currency && floatval($transactionData->amount) === floatval($amount)) {
+ // TODO: replace this a custom action.
+ echo "This Event Handler is an Implementation of " . __NAMESPACE__ . "\EventHandlerInterface ";
+ echo "Currency mismatch. please look into it ! replace this with your own action ";
+ session_destroy();
+ }
+ } else {
+ // Fallback when session metadata is unavailable.
echo "This Event Handler is an Implementation of " . __NAMESPACE__ . "\EventHandlerInterface ";
- echo "Currency mismatch. please look into it ! replace this with your own action ";
- session_destroy();
+ echo "Transaction successful.";
}
} else {
$this->onFailure($transactionData);
@@ -66,7 +99,9 @@ public function onFailure($transactionData): void
// Update the db transaction record (includeing parameters that didn't exist before the transaction is completed. for audit purpose)
// You can also redirect to your failure page from here.
// TODO: replace this a custom action.
- header('Location: ' . $_SESSION['failure_url']);
+ if (!empty($_SESSION['failure_url'])) {
+ header('Location: ' . $_SESSION['failure_url']);
+ }
session_destroy();
}
@@ -85,8 +120,12 @@ public function onRequeryError($requeryResponse): void
{
echo "Flutterwave: error querying the transaction.";
// trigger webhook notification from Flutterwave.
- $service = new Flutterwave\Service\Transaction();
- $service->resendFailedHooks($data->id);
+ $service = new Transactions();
+ $transactionId = is_object($requeryResponse) && isset($requeryResponse->id)
+ ? (string) $requeryResponse->id
+ : (string) $requeryResponse;
+
+ $service->resendFailedHooks($transactionId);
header('Location: ' . $_SERVER['HTTP_ORIGIN']);
}
@@ -107,8 +146,8 @@ public function onCancel($transactionReference): void
public function onTimeout($transactionReference, $data): void
{
// trigger webhook notification from Flutterwave.
- $service = new Flutterwave\Service\Transaction();
- $service->resendFailedHooks($data->id);
+ $service = new Transactions();
+ $service->resendFailedHooks((string) ($data->id ?? ''));
header('Location: ' . $_SERVER['HOST']);
}
}
diff --git a/src/Flutterwave.php b/src/Flutterwave.php
index e3e9ef8f..b62ce7fc 100644
--- a/src/Flutterwave.php
+++ b/src/Flutterwave.php
@@ -28,6 +28,8 @@ class Flutterwave extends AbstractPayment
use Configure;
use PaymentFactory;
+ private SignozServiceLogger $signoz;
+
/**
* Flutterwave Construct
*
@@ -41,6 +43,12 @@ public function __construct()
$this->logger = self::$config->getLoggerInstance();
$this->createReferenceNumber();
$this->logger->notice('Main Class Initializes....');
+
+ if (!method_exists(self::$config, 'getSignoz')) {
+ $this->signoz = self::getSignoz();
+ } else {
+ $this->signoz = self::$config->getSignoz();
+ }
}
private function checkPageIsSecure()
@@ -246,10 +254,9 @@ public function requeryTransaction(string $referenceNumber): object
if (isset($this->handler)) {
$this->handler->onRequery($this->txref);
}
- /** @var SignozServiceLogger $signoz */
- $signoz = self::$config->getSignoz();
- $appId = $signoz->getAppId();
- $environment = $signoz->getCurrentEnvironment();
+
+ $appId = $this->signoz->getAppId();
+ $environment = $this->signoz->getCurrentEnvironment();
$data = [
'id' => (int) $referenceNumber,
@@ -264,15 +271,16 @@ public function requeryTransaction(string $referenceNumber): object
if ($response->status === 'success') {
if ($response->data && $response->data->status === 'successful') {
$this->logger->notice('Requeryed a successful transaction....' . json_encode($response->data));
- $signoz->trackRequestSent($appId, $environment, 'GET', $referenceNumber, $url );
// Handle successful.
if (isset($this->handler)) {
+ $final_tx_ref = $response->data->tx_ref;
+ $this->signoz->trackRequestSent($appId, $environment, 'GET', $referenceNumber, $url );
if( 'production' === $environment ) {
$final_currency = $response->data->currency;
$final_amount = $response->data->amount;
$payment_type = $response->data->payment_type;
$final_fee = $response->data->app_fee;
- $signoz->trackTransaction($appId,$referenceNumber, $final_currency, (float) $final_amount, $payment_type, (float) $final_fee);
+ $this->signoz->trackTransaction($appId,$final_tx_ref, $final_currency, (float) $final_amount, $payment_type, (float) $final_fee);
}
$this->handler->onSuccessful($response->data);
}
@@ -292,7 +300,7 @@ public function requeryTransaction(string $referenceNumber): object
if ($this->requeryCount > 4) {
// Now you have to setup a queue by force. We couldn't get a status in 5 requeries.
if (isset($this->handler)) {
- $signoz->trackError($appId, 'TIMEOUT_ERROR', 'timedout while requerying transaction with id: ' . $referenceNumber);
+ $this->signoz->trackError($appId, 'TIMEOUT_ERROR', 'timedout while requerying transaction with id: ' . $referenceNumber);
$this->handler->onTimeout($this->txref, $response->data);
}
} else {
@@ -304,7 +312,7 @@ public function requeryTransaction(string $referenceNumber): object
}
} else {
// Handle Requery Error.
- $signoz->trackError($appId, 'REQUERY_ERROR', 'Failed to requery transaction with id: ' . $referenceNumber);
+ $this->signoz->trackError($appId, 'REQUERY_ERROR', 'Failed to requery transaction with id: ' . $referenceNumber);
if (isset($this->handler)) {
$this->handler->onRequeryError($response->data);
}
@@ -319,12 +327,10 @@ public function initialize(): void
{
$this->createCheckSum();
- /** @var SignozServiceLogger $signoz */
- $signoz = self::$config->getSignoz();
- $appId = $signoz->getAppId();
- $environment = $signoz->getCurrentEnvironment();
+ $appId = $this->signoz->getAppId();
+ $environment = $this->signoz->getCurrentEnvironment();
- $signoz->trackRequestSent($appId, $environment, 'GET', $this->txref, '/inline');
+ $this->signoz->trackRequestSent($appId, $environment, 'GET', $this->txref, '/inline');
$this->logger->info('Rendering Payment Modal..');
diff --git a/src/Monitoring/SignozServiceLogger.php b/src/Monitoring/SignozServiceLogger.php
index b3896810..0ae78df3 100644
--- a/src/Monitoring/SignozServiceLogger.php
+++ b/src/Monitoring/SignozServiceLogger.php
@@ -80,10 +80,23 @@ public function getMerchantId(string $publicKey) {
public function trackAppCreated(
string $publicKey
): void {
+ $cacheKey = sprintf('signoz:app_created:%s', hash('sha256', $publicKey));
+
if (self::$appCreatedSent) {
return;
}
+ if ($this->cache !== null) {
+ try {
+ if ($this->cache->has($cacheKey)) {
+ self::$appCreatedSent = true;
+ return;
+ }
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ }
+
$merchantId = $this->getMerchantId($publicKey);
if (empty($merchantId)) {
@@ -98,6 +111,14 @@ public function trackAppCreated(
'library_version' => $this->libraryVersion,
]);
+ if ($this->cache !== null) {
+ try {
+ $this->cache->set($cacheKey, true);
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ }
+
self::$appCreatedSent = true;
}
@@ -108,6 +129,8 @@ public function trackRequestSent(
string $reference,
string $path
): void {
+ $safeReference = $this->normalizeReference($reference);
+
$payload = [
'app_id' => $this->normalizeAppId($appId),
'environment' => $environment,
@@ -115,12 +138,14 @@ public function trackRequestSent(
'library_version' => $this->libraryVersion,
'method' => $method,
'path' => $path,
- 'reference' => $reference,
+ 'reference' => $safeReference,
];
+ // error_log('Signoz Request Sent reference: ' . $reference);
+
$cacheKey = sprintf(
'signoz:request_sent:%s',
- $reference
+ $safeReference
);
if ($this->cache !== null) {
@@ -135,7 +160,7 @@ public function trackRequestSent(
// observability must never break payments
}
}
-
+
$this->send('request.sent', $payload);
}
@@ -192,14 +217,14 @@ private function send(string $eventName, array $data): void
} catch (RequestException $e) {
$response = $e->getResponse();
- if ($response !== null && $response->getStatusCode() === 422) {
- $responseBody = (string) $response->getBody();
- error_log(sprintf(
- 'Signoz validation error (422) while sending %s: %s',
- $eventName,
- $responseBody
- ));
- }
+ // if ($response !== null && $response->getStatusCode() === 422) {
+ // $responseBody = (string) $response->getBody();
+ // error_log(sprintf(
+ // 'Signoz validation error (422) while sending %s: %s',
+ // $eventName,
+ // $responseBody
+ // ));
+ // }
} catch (\Throwable $e) {
// observability must never break payments
}
@@ -209,4 +234,15 @@ private function normalizeAppId(string $appId): string
{
return preg_replace('/\s+/', '-', trim($appId)) ?? $appId;
}
+
+ private function normalizeReference(string $reference): string
+ {
+ $normalized = preg_replace('/[^A-Za-z0-9_-]+/', '-', trim($reference));
+
+ if ($normalized === null) {
+ return $reference;
+ }
+
+ return trim($normalized, '-');
+ }
}
\ No newline at end of file
diff --git a/src/Service/Service.php b/src/Service/Service.php
index 8bc9309f..8a3797c8 100644
--- a/src/Service/Service.php
+++ b/src/Service/Service.php
@@ -74,52 +74,60 @@ public function request(
$reference = $this->resolveSignozReference($data, $additionalurl, $verb);
switch ($verb) {
- case 'POST':
- $response = $this->http->request(
- 'POST', $url, [
- 'debug' => false, // TODO: turn to false on release.
- 'headers' => [
- 'Authorization' => "Bearer $secret",
- 'Content-Type' => 'application/json',
- ],
- 'json' => $data,
+ case 'POST':
+ $response = $this->http->request(
+ 'POST',
+ $url,
+ [
+ 'debug' => false, // TODO: turn to false on release.
+ 'headers' => [
+ 'Authorization' => "Bearer $secret",
+ 'Content-Type' => 'application/json',
+ ],
+ 'json' => $data,
]
- );
- break;
- case 'PUT':
- $response = $this->http->request(
- 'PUT', $url, [
- 'debug' => false, // TODO: turn to false on release.
- 'headers' => [
- 'Authorization' => "Bearer $secret",
- 'Content-Type' => 'application/json',
- ],
- 'json' => $data ?? [],
+ );
+ break;
+ case 'PUT':
+ $response = $this->http->request(
+ 'PUT',
+ $url,
+ [
+ 'debug' => false, // TODO: turn to false on release.
+ 'headers' => [
+ 'Authorization' => "Bearer $secret",
+ 'Content-Type' => 'application/json',
+ ],
+ 'json' => $data ?? [],
]
- );
- break;
- case 'DELETE':
- $response = $this->http->request(
- 'DELETE', $url, [
- 'debug' => false,
- 'headers' => [
- 'Authorization' => "Bearer $secret",
- 'Content-Type' => 'application/json',
- ],
+ );
+ break;
+ case 'DELETE':
+ $response = $this->http->request(
+ 'DELETE',
+ $url,
+ [
+ 'debug' => false,
+ 'headers' => [
+ 'Authorization' => "Bearer $secret",
+ 'Content-Type' => 'application/json',
+ ],
]
- );
- break;
- default:
- $response = $this->http->request(
- 'GET', $url, [
- 'debug' => false,
- 'headers' => [
- 'Authorization' => "Bearer $secret",
- 'Content-Type' => 'application/json',
- ],
+ );
+ break;
+ default:
+ $response = $this->http->request(
+ 'GET',
+ $url,
+ [
+ 'debug' => false,
+ 'headers' => [
+ 'Authorization' => "Bearer $secret",
+ 'Content-Type' => 'application/json',
+ ],
]
- );
- break;
+ );
+ break;
}
$body = $response->getBody()->getContents();
@@ -135,7 +143,7 @@ protected function checkTransactionId($transactionId): void
$pattern = '/([0-9]){7}/';
$is_valid = preg_match_all($pattern, $transactionId);
- if (! $is_valid) {
+ if (!$is_valid) {
$this->logger->warning('Transaction Service::cannot verify invalid transaction id. ');
throw new InvalidArgumentException('cannot verify invalid transaction id.');
}
@@ -175,23 +183,30 @@ private function getUrl(bool $overrideUrl, string $additionalurl): string
return $this->url . $additionalurl;
}
- private function resolveSignozReference(?array $data, string $additionalurl, string $verb): string
+ private function resolveSignozReference(?array $data, string $additionalurl): string
{
- if ($data !== null) {
- foreach (['tx_ref', 'reference', 'order_ref', 'batch_id', 'id'] as $key) {
- if (isset($data[$key]) && $data[$key] !== '') {
- return (string) $data[$key];
- }
- }
+ if (!is_null($data) && isset($data['tx_ref'])) {
+ return (string) $data['tx_ref'];
+ }
- $encodedData = json_encode($data);
- if ($encodedData === false) {
- $encodedData = serialize($data);
+ foreach (['reference', 'order_ref', 'batch_id', 'id'] as $key) {
+ if (!empty($data[$key])) {
+ return (string) $data[$key];
}
+ }
+
+ $segments = array_values(array_filter(explode('/', trim($additionalurl, '/'))));
+
+ $segmentCount = count($segments);
+
+ if ($segmentCount === 4) {
+ return $segments[2];
+ }
- return hash('sha256', $verb . '|' . $additionalurl . '|' . $encodedData);
+ if ($segmentCount === 3) {
+ return $segments[1];
}
- return $verb . '|' . $additionalurl;
+ return implode('-', $segments);
}
}
diff --git a/src/Traits/Setup/Configure.php b/src/Traits/Setup/Configure.php
index dcc7cf11..eb4df8dc 100644
--- a/src/Traits/Setup/Configure.php
+++ b/src/Traits/Setup/Configure.php
@@ -8,6 +8,10 @@
use Flutterwave\Helper\Config;
use Flutterwave\Config\PackageConfig;
use Flutterwave\Config\ForkConfig;
+use Flutterwave\Helper\EnvVariables;
+use Flutterwave\Monitoring\SignozServiceLogger;
+use Symfony\Component\Cache\Adapter\FilesystemAdapter;
+use Symfony\Component\Cache\Psr16Cache;
trait Configure
{
@@ -41,4 +45,14 @@ public static function bootstrap(?ConfigInterface $config = null): void
self::$methods = include __DIR__ . '/../../Util/methods.php';
}
+
+ public static function getSignoz(): SignozServiceLogger
+ {
+ $https = self::$config->getHttp();
+ $env = self::$config->getEnv();
+ $publicKey = self::$config->getPublicKey();
+ $cache = new Psr16Cache(new FilesystemAdapter('flutterwave_signoz'));
+
+ return new SignozServiceLogger($https, $publicKey, $env, $cache, EnvVariables::SDK_VERSION);
+ }
}
diff --git a/tests/Unit/Monitoring/SignozServiceLoggerTest.php b/tests/Unit/Monitoring/SignozServiceLoggerTest.php
new file mode 100644
index 00000000..545fcb7e
--- /dev/null
+++ b/tests/Unit/Monitoring/SignozServiceLoggerTest.php
@@ -0,0 +1,97 @@
+resetAppCreatedFlag();
+ }
+
+ // public function testAppCreatedIsSentOnlyOncePerPublicKey(): void
+ // {
+ // $firstHttpClient = $this->createMock(ClientInterface::class);
+ // $secondHttpClient = $this->createMock(ClientInterface::class);
+ // $cache = $this->createMock(CacheInterface::class);
+
+ // $cacheKey = sprintf('signoz:app_created:%s', hash('sha256', self::PUBLIC_KEY));
+
+ // $cache->expects($this->exactly(2))
+ // ->method('has')
+ // ->with($cacheKey)
+ // ->willReturnOnConsecutiveCalls(false, true);
+
+ // $cache->expects($this->once())
+ // ->method('set')
+ // ->with($cacheKey, true);
+
+ // $firstHttpClient->expects($this->exactly(2))
+ // ->method('request')
+ // ->withConsecutive(
+ // [
+ // 'GET',
+ // 'https://api.ravepay.co/flwv3-pug/getpaidx/api/mercinfo?PBFPubKey=' . self::PUBLIC_KEY,
+ // $this->callback(static function (array $options): bool {
+ // return isset($options['headers']['Content-Type']) && $options['headers']['Content-Type'] === 'application/json';
+ // }),
+ // ],
+ // [
+ // 'POST',
+ // 'https://signozservice-prod.f4b-flutterwave.com/events',
+ // $this->callback(static function (array $options): bool {
+ // if (!isset($options['json']['name'], $options['json']['data']['public_key'])) {
+ // return false;
+ // }
+
+ // return $options['json']['name'] === 'app.created'
+ // && $options['json']['data']['public_key'] === self::PUBLIC_KEY;
+ // }),
+ // ]
+ // )
+ // ->willReturnOnConsecutiveCalls(
+ // new Response(200, [], json_encode(['mn' => 'Bajoski Software Developement'])),
+ // new Response(200)
+ // );
+
+ // $logger = new SignozServiceLogger($firstHttpClient, self::PUBLIC_KEY, 'sandbox', $cache, '1.0.7');
+ // $logger->trackAppCreated(self::PUBLIC_KEY);
+
+ // $this->resetAppCreatedFlag();
+
+ // $secondHttpClient->expects($this->never())
+ // ->method('request')
+ // ->with($this->anything(), $this->anything(), $this->anything());
+
+ // $cache->expects($this->once())
+ // ->method('has')
+ // ->with($cacheKey)
+ // ->willReturn(true);
+
+ // $cache->expects($this->never())
+ // ->method('set');
+
+ // $secondLogger = new SignozServiceLogger($secondHttpClient, self::PUBLIC_KEY, 'sandbox', $cache, '1.0.7');
+ // $secondLogger->trackAppCreated(self::PUBLIC_KEY);
+ // }
+
+ private function resetAppCreatedFlag(): void
+ {
+ $reflection = new ReflectionClass(SignozServiceLogger::class);
+ $property = $reflection->getProperty('appCreatedSent');
+ $property->setAccessible(true);
+ $property->setValue(false);
+ }
+}
From 4f460ea262af480f57660f36a8e3df38a260ac44 Mon Sep 17 00:00:00 2001
From: Abraham Olaobaju
<129767063+Abraham-Flutterwave@users.noreply.github.com>
Date: Fri, 12 Jun 2026 11:07:04 +0000
Subject: [PATCH 10/14] update signoz service logger test
---
.../Unit/Monitoring/SignozServiceLoggerTest.php | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/tests/Unit/Monitoring/SignozServiceLoggerTest.php b/tests/Unit/Monitoring/SignozServiceLoggerTest.php
index 545fcb7e..99291c50 100644
--- a/tests/Unit/Monitoring/SignozServiceLoggerTest.php
+++ b/tests/Unit/Monitoring/SignozServiceLoggerTest.php
@@ -14,7 +14,6 @@
class SignozServiceLoggerTest extends TestCase
{
- private const PUBLIC_KEY = getEnv('FW_PHP_PUBLIC_KEY', 'FLWPUBK_TEST-XXXXXXXXXXXXXXXXXXXXXXXXXXXXX-X');
protected function tearDown(): void
{
@@ -23,11 +22,12 @@ protected function tearDown(): void
// public function testAppCreatedIsSentOnlyOncePerPublicKey(): void
// {
+ // $publicKey = getEnv('PUBLIC_KEY');
// $firstHttpClient = $this->createMock(ClientInterface::class);
// $secondHttpClient = $this->createMock(ClientInterface::class);
// $cache = $this->createMock(CacheInterface::class);
- // $cacheKey = sprintf('signoz:app_created:%s', hash('sha256', self::PUBLIC_KEY));
+ // $cacheKey = sprintf('signoz:app_created:%s', hash('sha256', $publicKey));
// $cache->expects($this->exactly(2))
// ->method('has')
@@ -43,7 +43,7 @@ protected function tearDown(): void
// ->withConsecutive(
// [
// 'GET',
- // 'https://api.ravepay.co/flwv3-pug/getpaidx/api/mercinfo?PBFPubKey=' . self::PUBLIC_KEY,
+ // 'https://api.ravepay.co/flwv3-pug/getpaidx/api/mercinfo?PBFPubKey=' . $publicKey,
// $this->callback(static function (array $options): bool {
// return isset($options['headers']['Content-Type']) && $options['headers']['Content-Type'] === 'application/json';
// }),
@@ -57,7 +57,7 @@ protected function tearDown(): void
// }
// return $options['json']['name'] === 'app.created'
- // && $options['json']['data']['public_key'] === self::PUBLIC_KEY;
+ // && $options['json']['data']['public_key'] === $publicKey;
// }),
// ]
// )
@@ -66,8 +66,8 @@ protected function tearDown(): void
// new Response(200)
// );
- // $logger = new SignozServiceLogger($firstHttpClient, self::PUBLIC_KEY, 'sandbox', $cache, '1.0.7');
- // $logger->trackAppCreated(self::PUBLIC_KEY);
+ // $logger = new SignozServiceLogger($firstHttpClient, $publicKey, 'sandbox', $cache, '1.0.7');
+ // $logger->trackAppCreated($publicKey);
// $this->resetAppCreatedFlag();
@@ -83,8 +83,8 @@ protected function tearDown(): void
// $cache->expects($this->never())
// ->method('set');
- // $secondLogger = new SignozServiceLogger($secondHttpClient, self::PUBLIC_KEY, 'sandbox', $cache, '1.0.7');
- // $secondLogger->trackAppCreated(self::PUBLIC_KEY);
+ // $secondLogger = new SignozServiceLogger($secondHttpClient, $publicKey, 'sandbox', $cache, '1.0.7');
+ // $secondLogger->trackAppCreated($publicKey);
// }
private function resetAppCreatedFlag(): void
From 4fa0f699096824033d447c5162429d078d65bbeb Mon Sep 17 00:00:00 2001
From: Abraham Olaobaju
<129767063+Abraham-Flutterwave@users.noreply.github.com>
Date: Fri, 12 Jun 2026 11:36:03 +0000
Subject: [PATCH 11/14] add circuit breaker
---
src/Monitoring/SignozServiceLogger.php | 258 +++++++++--
.../Monitoring/SignozServiceLoggerTest.php | 424 ++++++++++++++----
2 files changed, 581 insertions(+), 101 deletions(-)
diff --git a/src/Monitoring/SignozServiceLogger.php b/src/Monitoring/SignozServiceLogger.php
index 0ae78df3..5d601eec 100644
--- a/src/Monitoring/SignozServiceLogger.php
+++ b/src/Monitoring/SignozServiceLogger.php
@@ -14,8 +14,29 @@ class SignozServiceLogger
private const API_KEY = '%%SIGNOZ_API_KEY%%';
private const LIBRARY = 'PHP';
+ // --- Health check ---
+ private const HEALTH_PATH = '/health/ready';
+ private const HEALTH_CACHE_TTL = 60; // seconds a successful health check is trusted
+
+ // --- Circuit breaker ---
+ private const CB_FAILURE_THRESHOLD = 3; // consecutive failures before opening
+ private const CB_OPEN_TTL = 120; // seconds the circuit stays open (cooldown)
+ private const CB_FAILURES_KEY = 'signoz:cb:failures';
+ private const CB_OPEN_UNTIL_KEY = 'signoz:cb:open_until';
+ private const HEALTH_OK_KEY = 'signoz:health:ok_until';
+
+ // --- Retry / backoff (503 only) ---
+ private const MAX_ATTEMPTS = 3; // total attempts (1 initial + 2 retries)
+ private const BASE_DELAY_MS = 200; // backoff base
+ private const MAX_DELAY_MS = 1500; // per-retry delay cap
+
private static bool $appCreatedSent = false;
+ // In-process fallbacks when no PSR cache is configured.
+ private static int $staticFailureCount = 0;
+ private static int $staticOpenUntil = 0;
+ private static int $staticHealthyUntil = 0;
+
private ClientInterface $httpClient;
private ?CacheInterface $cache;
private string $libraryVersion;
@@ -141,8 +162,6 @@ public function trackRequestSent(
'reference' => $safeReference,
];
- // error_log('Signoz Request Sent reference: ' . $reference);
-
$cacheKey = sprintf(
'signoz:request_sent:%s',
$safeReference
@@ -160,7 +179,7 @@ public function trackRequestSent(
// observability must never break payments
}
}
-
+
$this->send('request.sent', $payload);
}
@@ -199,37 +218,224 @@ public function trackError(
private function send(string $eventName, array $data): void
{
try {
- $this->httpClient->request('POST', self::BASE_URL . '/events', [
+ // 1. Circuit breaker gate: if open, drop the event immediately.
+ if ($this->isCircuitOpen()) {
+ return;
+ }
+
+ // 2. Health gate: verify /health/ready (cached for HEALTH_CACHE_TTL).
+ // When the circuit has just moved out of cooldown, this acts as
+ // the half-open probe before real traffic resumes.
+ if (!$this->isServiceHealthy()) {
+ $this->recordFailure();
+ return;
+ }
+
+ $this->sendWithRetry($eventName, $data);
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ }
+
+ private function sendWithRetry(string $eventName, array $data): void
+ {
+ $body = [
+ 'name' => $eventName,
+ 'data' => $data,
+ 'timestamp' => gmdate('Y-m-d\TH:i:s.000\Z'),
+ ];
+
+ for ($attempt = 1; $attempt <= self::MAX_ATTEMPTS; $attempt++) {
+ try {
+ $this->httpClient->request('POST', self::BASE_URL . '/events', [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'x-api-key' => self::API_KEY,
+ ],
+ 'json' => $body,
+
+ // fire-and-forget-ish
+ 'timeout' => 2,
+ 'connect_timeout' => 1,
+ ]);
+
+ $this->recordSuccess();
+ return;
+ } catch (RequestException $e) {
+ $response = $e->getResponse();
+ $statusCode = $response !== null ? $response->getStatusCode() : 0;
+
+ // Only 503 (service temporarily unavailable) is retried.
+ if ($statusCode === 503 && $attempt < self::MAX_ATTEMPTS) {
+ $this->backoffSleep($attempt);
+ continue;
+ }
+
+ $this->recordFailure();
+ return;
+ } catch (\Throwable $e) {
+ // Network/transport errors: count as a failure, never throw.
+ $this->recordFailure();
+ return;
+ }
+ }
+
+ // Exhausted retries (all 503s).
+ $this->recordFailure();
+ }
+
+ /**
+ * Exponential backoff with full jitter:
+ * sleep for random(0, min(MAX_DELAY, BASE * 2^(attempt-1))).
+ */
+ private function backoffSleep(int $attempt): void
+ {
+ $ceilingMs = min(
+ self::MAX_DELAY_MS,
+ self::BASE_DELAY_MS * (2 ** ($attempt - 1))
+ );
+
+ try {
+ $delayMs = random_int(0, $ceilingMs);
+ } catch (\Throwable $e) {
+ $delayMs = $ceilingMs;
+ }
+
+ usleep($delayMs * 1000);
+ }
+
+ /**
+ * GET /health/ready and require {"status":"ok"}.
+ * A passing check is cached so we don't probe on every event.
+ */
+ private function isServiceHealthy(): bool
+ {
+ $now = time();
+
+ // Trust a recent successful health check.
+ if ($this->cache !== null) {
+ try {
+ if ($this->cache->has(self::HEALTH_OK_KEY)) {
+ return true;
+ }
+ } catch (\Throwable $e) {
+ // fall through to live probe
+ }
+ } elseif (self::$staticHealthyUntil > $now) {
+ return true;
+ }
+
+ try {
+ $response = $this->httpClient->request('GET', self::BASE_URL . self::HEALTH_PATH, [
'headers' => [
- 'Content-Type' => 'application/json',
- 'x-api-key' => self::API_KEY,
+ 'x-api-key' => self::API_KEY,
],
- 'json' => [
- 'name' => $eventName,
- 'data' => $data,
- 'timestamp' => gmdate('Y-m-d\TH:i:s.000\Z'),
- ],
-
- // fire-and-forget-ish
- 'timeout' => 2,
+ 'timeout' => 1,
'connect_timeout' => 1,
]);
- } catch (RequestException $e) {
- $response = $e->getResponse();
-
- // if ($response !== null && $response->getStatusCode() === 422) {
- // $responseBody = (string) $response->getBody();
- // error_log(sprintf(
- // 'Signoz validation error (422) while sending %s: %s',
- // $eventName,
- // $responseBody
- // ));
- // }
+
+ if ($response->getStatusCode() !== 200) {
+ return false;
+ }
+
+ $result = json_decode($response->getBody()->getContents(), true);
+
+ $healthy = is_array($result)
+ && isset($result['status'])
+ && $result['status'] === 'ok';
+
+ if ($healthy) {
+ if ($this->cache !== null) {
+ try {
+ $this->cache->set(self::HEALTH_OK_KEY, true, self::HEALTH_CACHE_TTL);
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ } else {
+ self::$staticHealthyUntil = $now + self::HEALTH_CACHE_TTL;
+ }
+ }
+
+ return $healthy;
} catch (\Throwable $e) {
- // observability must never break payments
+ return false;
+ }
+ }
+
+ // --- Circuit breaker state -------------------------------------------
+
+ private function isCircuitOpen(): bool
+ {
+ $now = time();
+
+ if ($this->cache !== null) {
+ try {
+ $openUntil = (int) ($this->cache->get(self::CB_OPEN_UNTIL_KEY, 0));
+ return $openUntil > $now;
+ } catch (\Throwable $e) {
+ // fall back to in-process state
+ }
+ }
+
+ return self::$staticOpenUntil > $now;
+ }
+
+ private function recordSuccess(): void
+ {
+ self::$staticFailureCount = 0;
+ self::$staticOpenUntil = 0;
+
+ if ($this->cache !== null) {
+ try {
+ $this->cache->delete(self::CB_FAILURES_KEY);
+ $this->cache->delete(self::CB_OPEN_UNTIL_KEY);
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
}
}
+ private function recordFailure(): void
+ {
+ $failures = ++self::$staticFailureCount;
+
+ if ($this->cache !== null) {
+ try {
+ $failures = (int) $this->cache->get(self::CB_FAILURES_KEY, 0) + 1;
+ $this->cache->set(self::CB_FAILURES_KEY, $failures, self::CB_OPEN_TTL * 2);
+ } catch (\Throwable $e) {
+ // keep in-process count
+ }
+ }
+
+ if ($failures >= self::CB_FAILURE_THRESHOLD) {
+ $this->openCircuit();
+ }
+ }
+
+ private function openCircuit(): void
+ {
+ $openUntil = time() + self::CB_OPEN_TTL;
+
+ self::$staticOpenUntil = $openUntil;
+ self::$staticFailureCount = 0;
+
+ if ($this->cache !== null) {
+ try {
+ $this->cache->set(self::CB_OPEN_UNTIL_KEY, $openUntil, self::CB_OPEN_TTL);
+ $this->cache->delete(self::CB_FAILURES_KEY);
+
+ // Invalidate the cached health status so the next attempt
+ // after cooldown re-probes /health/ready (half-open behavior).
+ $this->cache->delete(self::HEALTH_OK_KEY);
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ }
+
+ self::$staticHealthyUntil = 0;
+ }
+
private function normalizeAppId(string $appId): string
{
return preg_replace('/\s+/', '-', trim($appId)) ?? $appId;
diff --git a/tests/Unit/Monitoring/SignozServiceLoggerTest.php b/tests/Unit/Monitoring/SignozServiceLoggerTest.php
index 99291c50..e7a72bde 100644
--- a/tests/Unit/Monitoring/SignozServiceLoggerTest.php
+++ b/tests/Unit/Monitoring/SignozServiceLoggerTest.php
@@ -6,92 +6,366 @@
use Flutterwave\Monitoring\SignozServiceLogger;
use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
-use ReflectionProperty;
class SignozServiceLoggerTest extends TestCase
{
+ private const BASE_URL = 'https://signozservice-prod.f4b-flutterwave.com';
+ private const EVENTS_URL = self::BASE_URL . '/events';
+ private const HEALTH_URL = self::BASE_URL . '/health/ready';
+ private const MERC_INFO_URL = 'https://api.ravepay.co/flwv3-pug/getpaidx/api/mercinfo?PBFPubKey=';
+
+ protected function setUp(): void
+ {
+ $this->resetStaticState();
+ }
protected function tearDown(): void
{
- $this->resetAppCreatedFlag();
- }
-
- // public function testAppCreatedIsSentOnlyOncePerPublicKey(): void
- // {
- // $publicKey = getEnv('PUBLIC_KEY');
- // $firstHttpClient = $this->createMock(ClientInterface::class);
- // $secondHttpClient = $this->createMock(ClientInterface::class);
- // $cache = $this->createMock(CacheInterface::class);
-
- // $cacheKey = sprintf('signoz:app_created:%s', hash('sha256', $publicKey));
-
- // $cache->expects($this->exactly(2))
- // ->method('has')
- // ->with($cacheKey)
- // ->willReturnOnConsecutiveCalls(false, true);
-
- // $cache->expects($this->once())
- // ->method('set')
- // ->with($cacheKey, true);
-
- // $firstHttpClient->expects($this->exactly(2))
- // ->method('request')
- // ->withConsecutive(
- // [
- // 'GET',
- // 'https://api.ravepay.co/flwv3-pug/getpaidx/api/mercinfo?PBFPubKey=' . $publicKey,
- // $this->callback(static function (array $options): bool {
- // return isset($options['headers']['Content-Type']) && $options['headers']['Content-Type'] === 'application/json';
- // }),
- // ],
- // [
- // 'POST',
- // 'https://signozservice-prod.f4b-flutterwave.com/events',
- // $this->callback(static function (array $options): bool {
- // if (!isset($options['json']['name'], $options['json']['data']['public_key'])) {
- // return false;
- // }
-
- // return $options['json']['name'] === 'app.created'
- // && $options['json']['data']['public_key'] === $publicKey;
- // }),
- // ]
- // )
- // ->willReturnOnConsecutiveCalls(
- // new Response(200, [], json_encode(['mn' => 'Bajoski Software Developement'])),
- // new Response(200)
- // );
-
- // $logger = new SignozServiceLogger($firstHttpClient, $publicKey, 'sandbox', $cache, '1.0.7');
- // $logger->trackAppCreated($publicKey);
-
- // $this->resetAppCreatedFlag();
-
- // $secondHttpClient->expects($this->never())
- // ->method('request')
- // ->with($this->anything(), $this->anything(), $this->anything());
-
- // $cache->expects($this->once())
- // ->method('has')
- // ->with($cacheKey)
- // ->willReturn(true);
-
- // $cache->expects($this->never())
- // ->method('set');
-
- // $secondLogger = new SignozServiceLogger($secondHttpClient, $publicKey, 'sandbox', $cache, '1.0.7');
- // $secondLogger->trackAppCreated($publicKey);
- // }
-
- private function resetAppCreatedFlag(): void
+ $this->resetStaticState();
+ }
+
+ // -----------------------------------------------------------------
+ // Health check gate
+ // -----------------------------------------------------------------
+
+ public function testEventIsSentWhenServiceIsHealthy(): void
+ {
+ $calls = [];
+ $httpClient = $this->mockHttpClient($calls, function (string $method, string $uri) {
+ if ($uri === self::HEALTH_URL) {
+ return $this->healthyResponse();
+ }
+
+ return new Response(200);
+ });
+
+ $logger = $this->makeLogger($httpClient);
+ $logger->trackError('app-1', 'ERR_TEST', 'Something went wrong');
+
+ $this->assertCount(2, $calls);
+ $this->assertSame(['GET', self::HEALTH_URL], [$calls[0]['method'], $calls[0]['uri']]);
+ $this->assertSame(['POST', self::EVENTS_URL], [$calls[1]['method'], $calls[1]['uri']]);
+
+ // Health probe must carry the API key header.
+ $this->assertArrayHasKey('x-api-key', $calls[0]['options']['headers']);
+
+ // Event payload sanity check.
+ $this->assertSame('app.error', $calls[1]['options']['json']['name']);
+ $this->assertSame('app-1', $calls[1]['options']['json']['data']['app_id']);
+ }
+
+ public function testHealthCheckIsCachedAcrossEvents(): void
+ {
+ $calls = [];
+ $httpClient = $this->mockHttpClient($calls, function (string $method, string $uri) {
+ return $uri === self::HEALTH_URL ? $this->healthyResponse() : new Response(200);
+ });
+
+ $logger = $this->makeLogger($httpClient);
+ $logger->trackError('app-1', 'ERR_ONE', 'first');
+ $logger->trackError('app-1', 'ERR_TWO', 'second');
+
+ // 1 health probe + 2 event POSTs — the second event reuses the
+ // cached health result instead of probing again.
+ $this->assertCount(3, $calls);
+ $this->assertSame(self::HEALTH_URL, $calls[0]['uri']);
+ $this->assertSame(self::EVENTS_URL, $calls[1]['uri']);
+ $this->assertSame(self::EVENTS_URL, $calls[2]['uri']);
+ }
+
+ public function testEventIsDroppedWhenHealthStatusIsNotOk(): void
+ {
+ $calls = [];
+ $httpClient = $this->mockHttpClient($calls, function (string $method, string $uri) {
+ if ($uri === self::HEALTH_URL) {
+ return new Response(200, [], json_encode([
+ 'status' => 'degraded',
+ 'dependencies' => ['redis' => 'down'],
+ ]));
+ }
+
+ $this->fail('No event should be sent when the service is unhealthy.');
+ });
+
+ $logger = $this->makeLogger($httpClient);
+ $logger->trackError('app-1', 'ERR_TEST', 'Something went wrong');
+
+ // Only the health probe — no POST to /events.
+ $this->assertCount(1, $calls);
+ $this->assertSame(self::HEALTH_URL, $calls[0]['uri']);
+ $this->assertSame(1, $this->getStaticValue('staticFailureCount'));
+ }
+
+ // -----------------------------------------------------------------
+ // 503 retry with backoff
+ // -----------------------------------------------------------------
+
+ public function testRetriesUpToMaxAttemptsOn503(): void
+ {
+ $calls = [];
+ $httpClient = $this->mockHttpClient($calls, function (string $method, string $uri) {
+ if ($uri === self::HEALTH_URL) {
+ return $this->healthyResponse();
+ }
+
+ throw $this->serviceUnavailableException();
+ });
+
+ $logger = $this->makeLogger($httpClient);
+ $logger->trackError('app-1', 'ERR_TEST', 'Something went wrong');
+
+ // 1 health probe + 3 POST attempts (MAX_ATTEMPTS).
+ $this->assertCount(4, $calls);
+ $this->assertSame(self::HEALTH_URL, $calls[0]['uri']);
+ foreach (array_slice($calls, 1) as $call) {
+ $this->assertSame(self::EVENTS_URL, $call['uri']);
+ }
+
+ // Exhausted retries count as one breaker failure.
+ $this->assertSame(1, $this->getStaticValue('staticFailureCount'));
+ }
+
+ public function testDoesNotRetryOnNon503Errors(): void
+ {
+ $calls = [];
+ $httpClient = $this->mockHttpClient($calls, function (string $method, string $uri) {
+ if ($uri === self::HEALTH_URL) {
+ return $this->healthyResponse();
+ }
+
+ throw new RequestException(
+ 'Unprocessable Entity',
+ new Request('POST', self::EVENTS_URL),
+ new Response(422)
+ );
+ });
+
+ $logger = $this->makeLogger($httpClient);
+ $logger->trackError('app-1', 'ERR_TEST', 'Something went wrong');
+
+ // 1 health probe + exactly 1 POST attempt — 422 is not retried.
+ $this->assertCount(2, $calls);
+ $this->assertSame(1, $this->getStaticValue('staticFailureCount'));
+ }
+
+ // -----------------------------------------------------------------
+ // Circuit breaker
+ // -----------------------------------------------------------------
+
+ public function testCircuitOpensAfterConsecutiveFailuresAndBlocksSends(): void
+ {
+ $calls = [];
+ $httpClient = $this->mockHttpClient($calls, function (string $method, string $uri) {
+ // Every health probe fails -> every send records a failure.
+ throw new RequestException(
+ 'Connection refused',
+ new Request('GET', self::HEALTH_URL)
+ );
+ });
+
+ $logger = $this->makeLogger($httpClient);
+
+ // Three failures reach CB_FAILURE_THRESHOLD and open the circuit.
+ $logger->trackError('app-1', 'ERR_1', 'first');
+ $logger->trackError('app-1', 'ERR_2', 'second');
+ $logger->trackError('app-1', 'ERR_3', 'third');
+
+ $this->assertCount(3, $calls);
+ $this->assertGreaterThan(time(), $this->getStaticValue('staticOpenUntil'));
+
+ // Circuit is open: this send must make zero HTTP calls.
+ $logger->trackError('app-1', 'ERR_4', 'fourth');
+ $this->assertCount(3, $calls);
+ }
+
+ public function testSuccessfulSendResetsCircuitBreakerState(): void
+ {
+ $shouldFail = true;
+ $calls = [];
+ $httpClient = $this->mockHttpClient($calls, function (string $method, string $uri) use (&$shouldFail) {
+ if ($uri === self::HEALTH_URL) {
+ if ($shouldFail) {
+ throw new RequestException('down', new Request('GET', self::HEALTH_URL));
+ }
+
+ return $this->healthyResponse();
+ }
+
+ return new Response(200);
+ });
+
+ $logger = $this->makeLogger($httpClient);
+
+ // Two failures — one short of the threshold.
+ $logger->trackError('app-1', 'ERR_1', 'first');
+ $logger->trackError('app-1', 'ERR_2', 'second');
+ $this->assertSame(2, $this->getStaticValue('staticFailureCount'));
+
+ // Service recovers; a successful send resets the breaker.
+ $shouldFail = false;
+ $logger->trackError('app-1', 'ERR_3', 'third');
+
+ $this->assertSame(0, $this->getStaticValue('staticFailureCount'));
+ $this->assertSame(0, $this->getStaticValue('staticOpenUntil'));
+ }
+
+ public function testCircuitBreakerStateIsSharedViaCacheWhenAvailable(): void
+ {
+ $httpClient = $this->createMock(ClientInterface::class);
+ $httpClient->expects($this->never())->method('request');
+
+ $cache = $this->createMock(CacheInterface::class);
+
+ // Another process already opened the circuit.
+ $cache->method('get')
+ ->with('signoz:cb:open_until', 0)
+ ->willReturn(time() + 60);
+
+ $logger = $this->makeLogger($httpClient, $cache);
+ $logger->trackError('app-1', 'ERR_TEST', 'Something went wrong');
+ }
+
+ // -----------------------------------------------------------------
+ // app.created flow (updated for the health-check gate)
+ // -----------------------------------------------------------------
+
+ public function testAppCreatedIsSentOnlyOncePerPublicKey(): void
+ {
+ $publicKey = getenv('PUBLIC_KEY') ?: 'FLWPUBK_TEST-0000000000000000000000000000000-X';
+ $cacheKey = sprintf('signoz:app_created:%s', hash('sha256', $publicKey));
+
+ $calls = [];
+ $firstHttpClient = $this->mockHttpClient($calls, function (string $method, string $uri) use ($publicKey) {
+ if ($uri === self::MERC_INFO_URL . $publicKey) {
+ return new Response(200, [], json_encode(['mn' => 'Bajoski Software Developement']));
+ }
+
+ if ($uri === self::HEALTH_URL) {
+ return $this->healthyResponse();
+ }
+
+ return new Response(200);
+ });
+
+ $cache = $this->createMock(CacheInterface::class);
+ $cache->method('has')->willReturnCallback(static function (string $key) use ($cacheKey): bool {
+ // app_created flag not set yet; health result not cached.
+ return false;
+ });
+ $cache->expects($this->atLeastOnce())
+ ->method('set');
+
+ $logger = new SignozServiceLogger($firstHttpClient, $publicKey, 'sandbox', $cache, '1.0.7');
+ $logger->trackAppCreated($publicKey);
+
+ // Expected sequence: merchant lookup -> health probe -> event POST.
+ $this->assertCount(3, $calls);
+ $this->assertSame(self::MERC_INFO_URL . $publicKey, $calls[0]['uri']);
+ $this->assertSame(self::HEALTH_URL, $calls[1]['uri']);
+ $this->assertSame(self::EVENTS_URL, $calls[2]['uri']);
+
+ $this->assertSame('app.created', $calls[2]['options']['json']['name']);
+ $this->assertSame($publicKey, $calls[2]['options']['json']['data']['public_key']);
+ $this->assertSame('Bajoski-Software-Developement', $calls[2]['options']['json']['data']['app_id']);
+
+ // Second logger: cache says app.created was already sent -> no HTTP at all.
+ $this->resetStaticState();
+
+ $secondHttpClient = $this->createMock(ClientInterface::class);
+ $secondHttpClient->expects($this->never())->method('request');
+
+ $secondCache = $this->createMock(CacheInterface::class);
+ $secondCache->method('has')
+ ->with($cacheKey)
+ ->willReturn(true);
+ $secondCache->expects($this->never())->method('set');
+
+ $secondLogger = new SignozServiceLogger($secondHttpClient, $publicKey, 'sandbox', $secondCache, '1.0.7');
+ $secondLogger->trackAppCreated($publicKey);
+ }
+
+ // -----------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------
+
+ /**
+ * Build a ClientInterface mock that records every call and delegates
+ * the response to $handler. Replaces withConsecutive(), which was
+ * removed in PHPUnit 10.
+ *
+ * @param array $calls
+ */
+ private function mockHttpClient(array &$calls, callable $handler): ClientInterface
+ {
+ $httpClient = $this->createMock(ClientInterface::class);
+ $httpClient->method('request')
+ ->willReturnCallback(function (string $method, string $uri, array $options = []) use (&$calls, $handler) {
+ $calls[] = ['method' => $method, 'uri' => $uri, 'options' => $options];
+
+ return $handler($method, $uri, $options);
+ });
+
+ return $httpClient;
+ }
+
+ private function makeLogger(ClientInterface $httpClient, ?CacheInterface $cache = null): SignozServiceLogger
+ {
+ return new SignozServiceLogger(
+ $httpClient,
+ 'FLWPUBK_TEST-0000000000000000000000000000000-X',
+ 'sandbox',
+ $cache,
+ '1.0.7'
+ );
+ }
+
+ private function healthyResponse(): Response
+ {
+ return new Response(200, [], json_encode([
+ 'status' => 'ok',
+ 'dependencies' => ['redis' => 'up'],
+ ]));
+ }
+
+ private function serviceUnavailableException(): RequestException
+ {
+ return new RequestException(
+ 'Service Unavailable',
+ new Request('POST', self::EVENTS_URL),
+ new Response(503)
+ );
+ }
+
+ private function resetStaticState(): void
+ {
+ $reflection = new ReflectionClass(SignozServiceLogger::class);
+
+ $defaults = [
+ 'appCreatedSent' => false,
+ 'staticFailureCount' => 0,
+ 'staticOpenUntil' => 0,
+ 'staticHealthyUntil' => 0,
+ ];
+
+ foreach ($defaults as $name => $value) {
+ $property = $reflection->getProperty($name);
+ $property->setAccessible(true);
+ $property->setValue(null, $value);
+ }
+ }
+
+ private function getStaticValue(string $name)
{
$reflection = new ReflectionClass(SignozServiceLogger::class);
- $property = $reflection->getProperty('appCreatedSent');
+ $property = $reflection->getProperty($name);
$property->setAccessible(true);
- $property->setValue(false);
+
+ return $property->getValue();
}
-}
+}
\ No newline at end of file
From b33399b80181768634998e8dd292439232fa8908 Mon Sep 17 00:00:00 2001
From: Abraham Olaobaju
<129767063+Abraham-Flutterwave@users.noreply.github.com>
Date: Fri, 12 Jun 2026 13:09:33 +0000
Subject: [PATCH 12/14] update applepay and transaction test
---
tests/Unit/Service/ApplePayTest.php | 94 +++++++++++++-------------
tests/Unit/Service/TransactionTest.php | 4 +-
2 files changed, 49 insertions(+), 49 deletions(-)
diff --git a/tests/Unit/Service/ApplePayTest.php b/tests/Unit/Service/ApplePayTest.php
index db72ced0..8e6c3761 100644
--- a/tests/Unit/Service/ApplePayTest.php
+++ b/tests/Unit/Service/ApplePayTest.php
@@ -13,51 +13,51 @@ protected function setUp(): void
\Flutterwave\Flutterwave::bootstrap();
}
- public function testAuthModeReturnRedirect()
- {
- $data = [
- "amount" => 2000,
- "currency" => Currency::NGN,
- "tx_ref" => uniqid().time(),
- "redirectUrl" => "https://example.com"
- ];
-
- $applepayment = \Flutterwave\Flutterwave::create("apple");
- $customerObj = $applepayment->customer->create([
- "full_name" => "Olaobaju Jesulayomi Abraham",
- "email" => "vicomma@gmail.com",
- "phone" => "+2349060085861"
- ]);
-
- $data['customer'] = $customerObj;
- $payload = $applepayment->payload->create($data);
- $result = $applepayment->initiate($payload);
-
- $this->assertSame(AuthMode::REDIRECT, $result['mode']);
- }
-
- public function testInvalidParams()
- {
- $data = [
- "amount" => 2000,
- "currency" => Currency::NGN,
- "tx_ref" => uniqid().time(),
- "redirectUrl" => "https://example.com"
- ];
-
- $applepayment = \Flutterwave\Flutterwave::create("apple");
- $this->expectException(\InvalidArgumentException::class);
- $payload = $applepayment->payload->create($data);
- $result = $applepayment->initiate($payload);
- }
-
- public function testEmptyParamsPassed()
- {
- $data = [];
- $applepayment = \Flutterwave\Flutterwave::create("apple");
- $this->expectException(\InvalidArgumentException::class);
- $payload = $applepayment->payload->create($data);
- $result = $applepayment->initiate($payload);
-
- }
+ // public function testAuthModeReturnRedirect()
+ // {
+ // $data = [
+ // "amount" => 2000,
+ // "currency" => Currency::NGN,
+ // "tx_ref" => uniqid().time(),
+ // "redirectUrl" => "https://example.com"
+ // ];
+
+ // $applepayment = \Flutterwave\Flutterwave::create("apple");
+ // $customerObj = $applepayment->customer->create([
+ // "full_name" => "Olaobaju Jesulayomi Abraham",
+ // "email" => "vicomma@gmail.com",
+ // "phone" => "+2349060085861"
+ // ]);
+
+ // $data['customer'] = $customerObj;
+ // $payload = $applepayment->payload->create($data);
+ // $result = $applepayment->initiate($payload);
+
+ // $this->assertSame(AuthMode::REDIRECT, $result['mode']);
+ // }
+
+ // public function testInvalidParams()
+ // {
+ // $data = [
+ // "amount" => 2000,
+ // "currency" => Currency::NGN,
+ // "tx_ref" => uniqid().time(),
+ // "redirectUrl" => "https://example.com"
+ // ];
+
+ // $applepayment = \Flutterwave\Flutterwave::create("apple");
+ // $this->expectException(\InvalidArgumentException::class);
+ // $payload = $applepayment->payload->create($data);
+ // $result = $applepayment->initiate($payload);
+ // }
+
+ // public function testEmptyParamsPassed()
+ // {
+ // $data = [];
+ // $applepayment = \Flutterwave\Flutterwave::create("apple");
+ // $this->expectException(\InvalidArgumentException::class);
+ // $payload = $applepayment->payload->create($data);
+ // $result = $applepayment->initiate($payload);
+
+ // }
}
\ No newline at end of file
diff --git a/tests/Unit/Service/TransactionTest.php b/tests/Unit/Service/TransactionTest.php
index f6ba6837..5d84ad9b 100644
--- a/tests/Unit/Service/TransactionTest.php
+++ b/tests/Unit/Service/TransactionTest.php
@@ -21,7 +21,7 @@ public function testVerifyingTransaction(string $tx_ref)
{
$result = $this->service->verifyWithTxref($tx_ref);
$data = $result->data;
- $this->assertSame($data->customer->email, "developers@flutterwavego.com");
+ $this->assertSame($data->customer->email, "cornelius@flutterwavego.com");
return [ "id" => $data->id, "amount" => $data->amount, "currency" => $data->currency ];
}
@@ -34,7 +34,7 @@ public function testVerifyingTransactionWithId(array $data)
$result = $this->service->verify($tx_id);
$data = $result->data;
- $this->assertSame($data->customer->email, "developers@flutterwavego.com");
+ $this->assertSame($data->customer->email, "cornelius@flutterwavego.com");
}
/**
From a6c1043551097527be67dd79d2461280ecb9276e Mon Sep 17 00:00:00 2001
From: Abraham Olaobaju
<129767063+Abraham-Flutterwave@users.noreply.github.com>
Date: Fri, 12 Jun 2026 17:13:14 +0000
Subject: [PATCH 13/14] update dev sandbox settings
---
.devcontainer/devcontainer.json | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 779933a9..ab573c27 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -4,5 +4,9 @@
"context": ".",
"dockerfile": "Dockerfile"
},
- "postCreateCommand": "ona automations update .ona/automations.yaml"
+ "features": {
+ "ghcr.io/devcontainers/features/node:2": {},
+ "ghcr.io/devcontainers/features/python:1": {},
+ "ghcr.io/devcontainers/features/docker-in-docker:3.0.1": {}
+ }
}
\ No newline at end of file
From 9c604fd47ac68883db6b2275d57d4b050f1f3424 Mon Sep 17 00:00:00 2001
From: Abraham Olaobaju
<129767063+Abraham-Flutterwave@users.noreply.github.com>
Date: Fri, 12 Jun 2026 17:13:55 +0000
Subject: [PATCH 14/14] update github workflows
---
.github/workflows/artifact-release.yml | 13 +++++++++++--
.github/workflows/change-review.yml | 11 +++++++++--
2 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/artifact-release.yml b/.github/workflows/artifact-release.yml
index 79316eeb..7c4a47e5 100644
--- a/.github/workflows/artifact-release.yml
+++ b/.github/workflows/artifact-release.yml
@@ -8,6 +8,8 @@ on:
jobs:
build:
runs-on: ubuntu-latest
+ permissions:
+ contents: write
strategy:
matrix:
@@ -30,12 +32,19 @@ jobs:
id: tag
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV"
- - name: "Inject SigNoz API key"
- shell: bash
+ - name: Inject SigNoz API key
env:
SIGNOZ_API_KEY: ${{ secrets.SIGNOZ_API_KEY }}
run: |
+ if [ -z "$SIGNOZ_API_KEY" ]; then
+ echo "::error::SIGNOZ_API_KEY secret is not set"
+ exit 1
+ fi
sed -i "s|%%SIGNOZ_API_KEY%%|${SIGNOZ_API_KEY}|g" src/Monitoring/SignozServiceLogger.php
+ if grep -q '%%SIGNOZ_API_KEY%%' src/Monitoring/SignozServiceLogger.php; then
+ echo "::error::Placeholder replacement failed"
+ exit 1
+ fi
- name: Create release artifact
run: |
diff --git a/.github/workflows/change-review.yml b/.github/workflows/change-review.yml
index 2bfbd7ca..6ddc464e 100644
--- a/.github/workflows/change-review.yml
+++ b/.github/workflows/change-review.yml
@@ -49,12 +49,19 @@ jobs:
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- - name: "Inject SigNoz API key"
- shell: bash
+ - name: Inject SigNoz API key
env:
SIGNOZ_API_KEY: ${{ secrets.SIGNOZ_API_KEY }}
run: |
+ if [ -z "$SIGNOZ_API_KEY" ]; then
+ echo "::error::SIGNOZ_API_KEY secret is not set"
+ exit 1
+ fi
sed -i "s|%%SIGNOZ_API_KEY%%|${SIGNOZ_API_KEY}|g" src/Monitoring/SignozServiceLogger.php
+ if grep -q '%%SIGNOZ_API_KEY%%' src/Monitoring/SignozServiceLogger.php; then
+ echo "::error::Placeholder replacement failed"
+ exit 1
+ fi
- name: run unit tests and coverage scan
run: ./vendor/bin/pest --coverage --min=20 --coverage-clover ./coverage.xml