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
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 247b9db2..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
{
@@ -22,7 +24,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;
@@ -55,7 +57,10 @@ 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());
}
abstract public static function setUp(
diff --git a/src/Controller/PaymentController.php b/src/Controller/PaymentController.php
index 00209f8f..b9e861d4 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,14 +41,14 @@ 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!";
}
- 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'];
@@ -61,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)
@@ -87,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/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..0ae78df3 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,48 @@ public function getMerchantId(string $publicKey) {
}
public function trackAppCreated(
- string $publicKey,
- string $merchantId
+ 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)) {
+ 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,
]);
+
+ if ($this->cache !== null) {
+ try {
+ $this->cache->set($cacheKey, true);
+ } catch (\Throwable $e) {
+ // observability must never break payments
+ }
+ }
+
+ self::$appCreatedSent = true;
}
public function trackRequestSent(
@@ -97,19 +129,23 @@ public function trackRequestSent(
string $reference,
string $path
): void {
+ $safeReference = $this->normalizeReference($reference);
+
$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,
- 'reference' => $reference,
+ 'reference' => $safeReference,
];
+ // error_log('Signoz Request Sent reference: ' . $reference);
+
$cacheKey = sprintf(
'signoz:request_sent:%s',
- $reference
+ $safeReference
);
if ($this->cache !== null) {
@@ -124,7 +160,7 @@ public function trackRequestSent(
// observability must never break payments
}
}
-
+
$this->send('request.sent', $payload);
}
@@ -137,7 +173,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 +188,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 +214,35 @@ 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;
+ }
+
+ 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 0ad3760a..8a3797c8 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,57 +71,70 @@ public function request(
$secret = $this->config->getSecretKey();
$url = $this->getUrl($overrideUrl, $additionalurl);
+ $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();
+ $appId = $this->signoz->getAppId();
+ $environment = $this->signoz->getCurrentEnvironment();
+ $this->signoz->trackRequestSent($appId, $environment, $verb, $reference, $additionalurl);
+
return json_decode($body);
}
@@ -127,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.');
}
@@ -166,4 +182,31 @@ private function getUrl(bool $overrideUrl, string $additionalurl): string
return $this->url . $additionalurl;
}
+
+ private function resolveSignozReference(?array $data, string $additionalurl): string
+ {
+ if (!is_null($data) && isset($data['tx_ref'])) {
+ return (string) $data['tx_ref'];
+ }
+
+ 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];
+ }
+
+ if ($segmentCount === 3) {
+ return $segments[1];
+ }
+
+ 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/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;
diff --git a/tests/Unit/Monitoring/SignozServiceLoggerTest.php b/tests/Unit/Monitoring/SignozServiceLoggerTest.php
new file mode 100644
index 00000000..99291c50
--- /dev/null
+++ b/tests/Unit/Monitoring/SignozServiceLoggerTest.php
@@ -0,0 +1,97 @@
+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
+ {
+ $reflection = new ReflectionClass(SignozServiceLogger::class);
+ $property = $reflection->getProperty('appCreatedSent');
+ $property->setAccessible(true);
+ $property->setValue(false);
+ }
+}