diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8d3d90b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Exclude development files from Composer exports (composer install / git archive) +/dev export-ignore +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/CONTRIBUTING.md export-ignore + +# Force LF line endings for shell scripts (prevents CRLF issues in Docker) +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index 5f4da91..aba8ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ composer.phar \#* /nbproject/ +# Environment .env +dev/.env diff --git a/Controller/Payment/AbstractPaystackStandard.php b/Controller/Payment/AbstractPaystackStandard.php index 2438181..26d3cee 100644 --- a/Controller/Payment/AbstractPaystackStandard.php +++ b/Controller/Payment/AbstractPaystackStandard.php @@ -23,18 +23,19 @@ namespace Pstk\Paystack\Controller\Payment; use Magento\Payment\Helper\Data as PaymentHelper; +use Pstk\Paystack\Gateway\PaystackApiClient; abstract class AbstractPaystackStandard extends \Magento\Framework\App\Action\Action { protected $resultPageFactory; - + /** * - * @var \Magento\Sales\Api\OrderRepositoryInterface + * @var \Magento\Sales\Api\OrderRepositoryInterface */ protected $orderRepository; - + /** * * @var \Magento\Sales\Api\Data\OrderInterface @@ -43,33 +44,33 @@ abstract class AbstractPaystackStandard extends \Magento\Framework\App\Action\Ac protected $checkoutSession; protected $method; protected $messageManager; - + /** * - * @var \Pstk\Paystack\Model\Ui\ConfigProvider + * @var \Pstk\Paystack\Model\Ui\ConfigProvider */ protected $configProvider; - + /** * - * @var \Yabacon\Paystack + * @var PaystackApiClient */ - protected $paystack; - + protected $paystackClient; + /** * @var \Magento\Framework\Event\Manager */ protected $eventManager; - + /** * * @var \Psr\Log\LoggerInterface */ protected $logger; - + /** * - * @var \Magento\Framework\App\Request\Http + * @var \Magento\Framework\App\Request\Http */ protected $request; @@ -90,7 +91,8 @@ public function __construct( \Pstk\Paystack\Model\Ui\ConfigProvider $configProvider, \Magento\Framework\Event\Manager $eventManager, \Magento\Framework\App\Request\Http $request, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + PaystackApiClient $paystackClient ) { $this->resultPageFactory = $resultPageFactory; $this->orderRepository = $orderRepository; @@ -102,21 +104,11 @@ public function __construct( $this->eventManager = $eventManager; $this->request = $request; $this->logger = $logger; - - $this->paystack = $this->initPaystackPHP(); - - + $this->paystackClient = $paystackClient; + parent::__construct($context); } - protected function initPaystackPHP() { - $secretKey = $this->method->getConfigData('live_secret_key'); - if ($this->method->getConfigData('test_mode')) { - $secretKey = $this->method->getConfigData('test_secret_key'); - } - return new \Yabacon\Paystack($secretKey); - } - protected function redirectToFinal($successFul = true, $message="") { if($successFul){ if($message) $this->messageManager->addSuccessMessage(__($message)); diff --git a/Controller/Payment/Callback.php b/Controller/Payment/Callback.php index cc85505..5ae7169 100644 --- a/Controller/Payment/Callback.php +++ b/Controller/Payment/Callback.php @@ -39,9 +39,7 @@ public function execute() { } try { - $transactionDetails = $this->paystack->transaction->verify([ - 'reference' => $reference - ]); + $transactionDetails = $this->paystackClient->verifyTransaction($reference); $reference = explode('_', $transactionDetails->data->reference, 2); $reference = ($reference[0])?: 0; @@ -60,7 +58,7 @@ public function execute() { $message = "Invalid reference or order number"; - } catch (\Yabacon\Paystack\Exception\ApiException $e) { + } catch (\Pstk\Paystack\Gateway\Exception\ApiException $e) { $message = $e->getMessage(); } catch (Exception $e) { diff --git a/Controller/Payment/Setup.php b/Controller/Payment/Setup.php index e5cbc3d..128c6b7 100644 --- a/Controller/Payment/Setup.php +++ b/Controller/Payment/Setup.php @@ -37,7 +37,7 @@ public function execute() { try { return $this->processAuthorization($order); - } catch (\Yabacon\Paystack\Exception\ApiException $e) { + } catch (\Pstk\Paystack\Gateway\Exception\ApiException $e) { $message = $e->getMessage(); $order->addStatusToHistory($order->getStatus(), $message); $this->orderRepository->save($order); @@ -48,7 +48,7 @@ public function execute() { } protected function processAuthorization(\Magento\Sales\Model\Order $order) { - $tranx = $this->paystack->transaction->initialize([ + $tranx = $this->paystackClient->initializeTransaction([ 'first_name' => $order->getCustomerFirstname(), 'last_name' => $order->getCustomerLastname(), 'amount' => $order->getGrandTotal() * 100, // in kobo diff --git a/Controller/Payment/Webhook.php b/Controller/Payment/Webhook.php index feab4c7..f01b80f 100644 --- a/Controller/Payment/Webhook.php +++ b/Controller/Payment/Webhook.php @@ -3,94 +3,87 @@ /** * Paystack Magento2 Module using \Magento\Payment\Model\Method\AbstractMethod * Copyright (C) 2019 Paystack.com - * + * * This file is part of Pstk/Paystack. - * + * * Pstk/Paystack is free software => you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see //www.gnu.org/licenses/>. */ namespace Pstk\Paystack\Controller\Payment; - -use Magento\Sales\Model\Order; - class Webhook extends AbstractPaystackStandard { public function execute() { $finalMessage = "failed"; - + $resultFactory = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_RAW); try { // Retrieve the request's body and parse it as JSON - $event = \Yabacon\Paystack\Event::capture(); + $rawBody = $this->request->getContent(); http_response_code(200); - - /* It is a important to log all events received. Add code * - * here to log the signature and body to db or file */ - $this->logger->debug("PAYSTACK_LOG: {$event->raw}"); - /* Verify that the signature matches one of your keys */ - $secretKey = $this->configProvider->getSecretKeyArray(); - $owner = $event->discoverOwner($secretKey); + $this->logger->info("Paystack Webhook: received request"); - if (!$owner) { - // None of the keys matched the event's signature + // Validate webhook signature + $signature = $this->request->getHeader('X-Paystack-Signature') ?: ''; + if (!$signature || !$this->paystackClient->validateWebhookSignature($rawBody, $signature)) { + $this->logger->warning("Paystack Webhook: signature validation failed"); $resultFactory->setContents("auth failed"); return $resultFactory; } - // Do something with $event->obj - // Give value to your customer but don't give any output - // Remember that this is a call from Paystack's servers and - // Your customer is not seeing the response here at all - switch ($event->obj->event) { - // charge.success - case 'charge.success': - if ('success' === $event->obj->data->status) { - $transactionDetails = $this->paystack->transaction->verify([ - 'reference' => $event->obj->data->reference - ]); + $this->logger->info("Paystack Webhook: signature valid"); - $reference = $transactionDetails->data->reference; + $event = json_decode($rawBody); + if (!$event) { + $resultFactory->setContents("invalid payload"); + return $resultFactory; + } - $order = $this->orderInterface->loadByIncrementId($reference); + $this->logger->info("Paystack Webhook: event type = " . ($event->event ?? 'unknown')); + + switch ($event->event) { + case 'charge.success': + if ('success' === $event->data->status) { + $transactionDetails = $this->paystackClient->verifyTransaction($event->data->reference); - //if is popup mode, reference is generated by Paystack and we provided quoteId instead - if((!$order || !$order->getId()) && isset($event->obj->data->metadata->quoteId)){ + $reference = $transactionDetails->data->reference; + $this->logger->info("Paystack Webhook: verified transaction", ['reference' => $reference]); + $this->paystackClient->logTransactionSuccess($reference, $this->configProvider->getPublicKey()); - $reference = $transactionDetails->data->reference; - //PSTK_LOGGER HERE - log_transaction_success($reference); - //------------------------ - $order = $this->orderInterface->loadByIncrementId($reference); - - //if is popup mode, reference is generated by Paystack and we provided quoteId instead - if((!$order || !$order->getId()) && isset($event->obj->data->metadata->quoteId)){ - - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); - $searchCriteriaBuilder = $objectManager->create('Magento\Framework\Api\SearchCriteriaBuilder'); - $searchCriteria = $searchCriteriaBuilder->addFilter('quote_id', $event->obj->data->metadata->quoteId, 'eq')->create(); - $items = $this->orderRepository->getList($searchCriteria); - if($items->getTotalCount() == 1){ - $order = $items->getFirstItem(); + $order = $this->orderInterface->loadByIncrementId($reference); - } + // In popup mode, reference is generated by Paystack and we provided quoteId instead + if ((!$order || !$order->getId()) && isset($event->data->metadata->quoteId)) { + $this->logger->info("Paystack Webhook: order not found by reference, searching by quoteId", ['quoteId' => $event->data->metadata->quoteId]); + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $searchCriteriaBuilder = $objectManager->create('Magento\Framework\Api\SearchCriteriaBuilder'); + $searchCriteria = $searchCriteriaBuilder->addFilter('quote_id', $event->data->metadata->quoteId, 'eq')->create(); + $items = $this->orderRepository->getList($searchCriteria); + if ($items->getTotalCount() == 1) { + $order = $items->getFirstItem(); + } + } if ($order && $order->getId()) { + $this->logger->info("Paystack Webhook: order found, dispatching verify event", [ + 'order_id' => $order->getIncrementId(), + 'current_status' => $order->getStatus(), + ]); // dispatch the `payment_verify_after` event to update the order status $this->eventManager->dispatch('paystack_payment_verify_after', [ "paystack_order" => $order, @@ -99,43 +92,16 @@ public function execute() { $resultFactory->setContents("success"); return $resultFactory; } + $this->logger->warning("Paystack Webhook: order not found for reference " . $reference); } - } break; } - } - } catch (Exception $exc) { + } catch (\Exception $exc) { + $this->logger->error("Paystack Webhook: exception", ['error' => $exc->getMessage()]); $finalMessage = $exc->getMessage(); } - + $resultFactory->setContents($finalMessage); return $resultFactory; } - - function log_transaction_success($trx_ref){ - //send reference to logger along with plugin name and public key - $url = "https://plugin-tracker.paystackintegrations.com/log/charge_success"; - $plugin_name = 'magento-2'; - $public_key = $this->configProvider->getPublicKey(); - - $fields = [ - 'plugin_name' => $plugin_name, - 'transaction_reference' => $trx_ref, - 'public_key' => $public_key - ]; - - $fields_string = http_build_query($fields); - - $ch = curl_init(); - - curl_setopt($ch,CURLOPT_URL, $url); - curl_setopt($ch,CURLOPT_POST, true); - curl_setopt($ch,CURLOPT_POSTFIELDS, $fields_string); - - curl_setopt($ch,CURLOPT_RETURNTRANSFER, true); - - //execute post - $result = curl_exec($ch); - // echo $result; - } } diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 6a112ee..0000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM alexcheng/magento2 - -WORKDIR /var/www/html/ - -COPY ./setup_magento /usr/local/bin/setup_magento - -RUN chmod +x /usr/local/bin/setup_magento - -ARG magento_username - -ARG magento_password - -# setting up authentication credentials for magento -COPY ./auth.json var/composer_home/auth.json - -RUN sed -i -e "s/user_placeholder/$magento_username/g" var/composer_home/auth.json && sed -i -e "s/pass_placeholder/$magento_password/g" var/composer_home/auth.json - -RUN chown -R www-data:www-data /var/www/html/var/composer_home - -# We change to the www-data user who is the magento file system owner. -# Visit https://devdocs.magento.com/guides/v2.3/install-gde/prereq/file-sys-perms-over.html for more information -USER www-data - -RUN /var/www/html/bin/magento sampledata:deploy - -USER root - -ENTRYPOINT [ "setup_magento" ] diff --git a/Gateway/Exception/ApiException.php b/Gateway/Exception/ApiException.php new file mode 100644 index 0000000..ededf07 --- /dev/null +++ b/Gateway/Exception/ApiException.php @@ -0,0 +1,7 @@ +getMethodInstance(PaystackModel::CODE); + $this->secretKey = $method->getConfigData('live_secret_key'); + if ($method->getConfigData('test_mode')) { + $this->secretKey = $method->getConfigData('test_secret_key'); + } + } + + /** + * Initialize a transaction (Standard/Redirect flow). + * + * @param array $params + * @return object Decoded JSON response with ->data->authorization_url + * @throws ApiException + */ + public function initializeTransaction(array $params): object + { + return $this->request('POST', '/transaction/initialize', $params); + } + + /** + * Verify a transaction by reference. + * + * @param string $reference + * @return object Decoded JSON response with ->data (reference, status, metadata, etc.) + * @throws ApiException + */ + public function verifyTransaction(string $reference): object + { + return $this->request('GET', '/transaction/verify/' . rawurlencode($reference)); + } + + /** + * Validate a Paystack webhook signature (HMAC-SHA512). + * + * @param string $rawBody Raw request body from php://input + * @param string $signature Value of the X-Paystack-Signature header + * @return bool + */ + public function validateWebhookSignature(string $rawBody, string $signature): bool + { + $computed = hash_hmac('sha512', $rawBody, $this->secretKey); + return hash_equals($computed, $signature); + } + + /** + * Log a successful transaction to the Paystack plugin tracker. + * + * @param string $transactionReference + * @param string $publicKey + * @return void + */ + public function logTransactionSuccess(string $transactionReference, string $publicKey): void + { + $url = 'https://plugin-tracker.paystackintegrations.com/log/charge_success'; + + $fields = http_build_query([ + 'plugin_name' => 'magento-2', + 'transaction_reference' => $transactionReference, + 'public_key' => $publicKey, + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $fields, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + ]); + curl_exec($ch); + curl_close($ch); + } + + /** + * @param string $method HTTP method (GET or POST) + * @param string $endpoint API path (e.g. /transaction/verify/ref) + * @param array|null $data POST body data (will be JSON-encoded) + * @return object Decoded JSON response body + * @throws ApiException + */ + private function request(string $method, string $endpoint, ?array $data = null): object + { + $url = self::BASE_URL . $endpoint; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $this->secretKey, + 'Content-Type: application/json', + ], + CURLOPT_TIMEOUT => 30, + ]); + + if ($method === 'POST' && $data !== null) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } + + $response = curl_exec($ch); + + if (curl_errno($ch)) { + $error = curl_error($ch); + curl_close($ch); + throw new ApiException('Paystack API request failed: ' . $error); + } + + $statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $body = json_decode($response); + + if (!$body) { + throw new ApiException('Invalid JSON response from Paystack API', $statusCode); + } + + if ($statusCode >= 400 || (isset($body->status) && !$body->status)) { + $message = $body->message ?? 'Paystack API request failed'; + throw new ApiException($message, $statusCode); + } + + return $body; + } +} diff --git a/Model/CspPolicyCollector.php b/Model/CspPolicyCollector.php new file mode 100644 index 0000000..8a9c271 --- /dev/null +++ b/Model/CspPolicyCollector.php @@ -0,0 +1,46 @@ +paystackClient = $paystackClient; $this->eventManager = $eventManager; - $this->paystackPaymentInstance = $paymentHelper->getMethodInstance(PaystackModel::CODE); - $this->orderInterface = $orderInterface; $this->checkoutSession = $checkoutSession; - - $secretKey = $this->paystackPaymentInstance->getConfigData('live_secret_key'); - if ($this->paystackPaymentInstance->getConfigData('test_mode')) { - $secretKey = $this->paystackPaymentInstance->getConfigData('test_secret_key'); - } - - $this->paystackLib = new PaystackLib($secretKey); + $this->logger = $logger; } /** @@ -74,19 +69,32 @@ public function verifyPayment($reference) $reference = $ref[0]; $quoteId = $ref[1]; + $this->logger->info('Paystack: verifyPayment called', ['reference' => $reference, 'quoteId' => $quoteId]); + try { - $transaction_details = $this->paystackLib->transaction->verify([ - 'reference' => $reference + $transaction_details = $this->paystackClient->verifyTransaction($reference); + $this->logger->info('Paystack: transaction verified via API', [ + 'tx_status' => $transaction_details->data->status ?? 'unknown', + 'tx_quoteId' => $transaction_details->data->metadata->quoteId ?? 'missing', ]); - + $order = $this->getOrder(); - if ($order && $order->getQuoteId() === $quoteId && $transaction_details->data->metadata->quoteId === $quoteId) { - + $this->logger->info('Paystack: getOrder result', [ + 'order_found' => $order ? 'yes' : 'no', + 'order_quoteId' => $order ? $order->getQuoteId() : 'N/A', + 'url_quoteId' => $quoteId, + 'tx_meta_quoteId' => $transaction_details->data->metadata->quoteId ?? 'missing', + ]); + + if ($order && (string)$order->getQuoteId() === (string)$quoteId && (string)$transaction_details->data->metadata->quoteId === (string)$quoteId) { + // dispatch the `paystack_payment_verify_after` event to update the order status $this->eventManager->dispatch('paystack_payment_verify_after', [ "paystack_order" => $order, ]); + $this->logger->info('Paystack: verification successful, event dispatched'); + // Return consistent response format return json_encode([ 'status' => true, @@ -94,7 +102,9 @@ public function verifyPayment($reference) 'data' => $transaction_details->data ]); } + $this->logger->warning('Paystack: quoteId mismatch — order not updated'); } catch (Exception $e) { + $this->logger->error('Paystack: verifyPayment exception', ['error' => $e->getMessage()]); return json_encode([ 'status' => false, 'message' => $e->getMessage() diff --git a/Model/Ui/ConfigProvider.php b/Model/Ui/ConfigProvider.php index 4e9741e..bdb0681 100644 --- a/Model/Ui/ConfigProvider.php +++ b/Model/Ui/ConfigProvider.php @@ -12,6 +12,7 @@ class ConfigProvider implements ConfigProviderInterface { protected $method; + protected $store; public function __construct(PaymentHelper $paymentHelper, Store $store) { diff --git a/Observer/ObserverAfterPaymentVerify.php b/Observer/ObserverAfterPaymentVerify.php index 3d91905..22024c2 100644 --- a/Observer/ObserverAfterPaymentVerify.php +++ b/Observer/ObserverAfterPaymentVerify.php @@ -31,8 +31,12 @@ public function execute(\Magento\Framework\Event\Observer $observer) ->setCanSendNewEmailFlag(true) ->setCustomerNoteNotify(true); $order->save(); - - $this->orderSender->send($order, true); + + try { + $this->orderSender->send($order, true); + } catch (\Exception $e) { + // Email sending failure should not affect order status + } } } } diff --git a/README.md b/README.md index e09d36a..9b55164 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,12 @@ Paystack payment gateway Magento2 extension -**Version:** 2.5.0 (Paystack v2 Inline.js API) +**Version:** 3.0.0 (Paystack v2 Inline.js API) ## Requirements -- Magento 2.x -- PHP 8.3+ -- yabacon/paystack-php v2.2.0 or newer +- Magento 2.4.x +- PHP 8.2+ ## Installation @@ -21,46 +20,26 @@ Paystack payment gateway Magento2 extension Go to your Magento2 root folder and run: ```bash -composer require pstk/paystack-magento2-module:^2.5.0 +composer require pstk/paystack-magento2-module php bin/magento module:enable Pstk_Paystack php bin/magento setup:upgrade +php bin/magento setup:di:compile php bin/magento cache:flush ``` -### Manual Installation (Custom Source) +### Manual Installation -Copy all files from your source folder (`plugin-magento-2`) to `app/code/Pstk/Paystack/` in your Magento installation. +Copy all files to `app/code/Pstk/Paystack/` in your Magento installation, then run: -Then run: ```bash php bin/magento module:enable Pstk_Paystack php bin/magento setup:upgrade -php bin/magento cache:flush -``` - -## Install - -* Go to Magento2 root folder - -* Enter following command to install module: - -```bash -composer require pstk/paystack-magento2-module -``` - -* Wait while dependencies are updated. - -* Enter following commands to enable module: - -```bash -php bin/magento module:enable Pstk_Paystack --clear-static-content -php bin/magento setup:upgrade php bin/magento setup:di:compile +php bin/magento cache:flush ``` ## Configuration - To configure the plugin in *Magento Admin*: 1. Go to **Stores > Configuration > Sales > Payment Methods**. 2. Find **Paystack** and configure: @@ -72,48 +51,65 @@ To configure the plugin in *Magento Admin*: - **Test/Live Public Key**: Get from your [Paystack dashboard](https://dashboard.paystack.com/#/settings/developer) 3. Click **Save Config**. -**Note:** Inline (Popup) uses Paystack v2 Inline.js API. Make sure your CSP whitelist and RequireJS config are updated as shown in the migration guide. - -![Magento Settings](https://res.cloudinary.com/drps6uoe4/image/upload/v1617968546/Screenshot_2021-04-09_at_10.51.31_outbpi.png) +### Webhook Setup -## Known Errors +For reliable payment confirmation (especially for the redirect flow), set up a webhook in your Paystack dashboard: -Sometimes after receiving payment for an order you get an error like: Class Yabacon\Paystack not found and magento doesn't redirect to the `success` page. +1. Go to **Settings > API Keys & Webhooks** on your [Paystack dashboard](https://dashboard.paystack.com/#/settings/developer) +2. Set the Webhook URL to: `https://yourdomain.com/paystack/payment/webhook` +3. The module handles `charge.success` events and automatically updates order status -**Fix:** +## Development Environment -Run: -```bash -composer require yabacon/paystack-php -``` - Enable and configure Paystack in Magento Admin under Stores/Configuration/Sales/Payment Methods +A Docker-based development environment is included in the `dev/` directory for contributors and testing. -**Fail to redirect to success page after payment** - -Ensure you are using Paystack v2 Inline.js and your CSP/RequireJS configs are correct. +### Prerequisites -[ico-version]: https://img.shields.io/packagist/v/pstk/paystack-magento2-module.svg?style=flat-square -[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square -[ico-downloads]: https://img.shields.io/packagist/dt/pstk/paystack-magento2-module.svg?style=flat-square +- [Docker](https://www.docker.com/) (or [Rancher Desktop](https://rancherdesktop.io/) with `dockerd` runtime) +- A [Paystack test account](https://dashboard.paystack.com/#/signup) -[link-packagist]: https://packagist.org/packages/pstk/paystack-magento2-module -[link-downloads]: https://packagist.org/packages/pstk/paystack-magento2-module +### Quick Start +```bash +cd dev +cp .env.example .env # Add your Paystack test keys +docker compose up -d # First run builds the image (~5 min) and installs Magento (~3 min) +bash setup.sh # Enables module, creates test products, configures everything +``` -## Running the magento2 on docker +Once complete you'll see: -Contained within this repo is a Dockerfile and a docker-compose file to quickly spin up Magento 2 and MySQL containers with the Paystack plugin installed. +``` +============================================ + Setup complete! + + Storefront: http://localhost:8080 + Admin panel: http://localhost:8080/admin + Admin login: admin / Admin12345! + + Test card: 4084 0840 8408 4081 + Expiry: 12/30 + CVV: 408 + PIN: 0000 + OTP: 123456 +============================================ +``` -### Prerequisites -- Install [Docker](https://www.docker.com/) +### What's Included -### Quick Steps +- **Magento 2.4.8-p3** via [Mage-OS](https://mage-os.org/) public mirror (no Adobe marketplace auth needed) +- **OpenSearch 2.19.1** + **MariaDB 10.6** +- **5 test products** with images and a configured homepage +- **Paystack payment** pre-configured in test mode (inline popup) +- Container names: `paystack-magento`, `paystack-db`, `paystack-search` -- Create a `.env` file from `.env.sample` in the root directory. Fill in your values. -- Run `docker-compose up` from the root directory to build and start the containers. -- Visit `localhost:8000` for the Magento store. For admin, visit `localhost:8000/` (set in `.env`). -- Run `docker-compose down` to stop the containers. +### Tear Down +```bash +cd dev +docker compose down # Stop containers (preserves data) +docker compose down -v # Stop containers and delete all data +``` ## Documentation @@ -122,7 +118,7 @@ Contained within this repo is a Dockerfile and a docker-compose file to quickly ## Support -For bug reports and feature requests directly related to this plugin, please use the [issue tracker](https://github.com/PaystackHQ/plugin-magento-2/issues). +For bug reports and feature requests directly related to this plugin, please use the [issue tracker](https://github.com/PaystackHQ/plugin-magento-2/issues). For general support or questions about your Paystack account, you can reach out by sending a message from [our website](https://paystack.com/contact). @@ -134,3 +130,9 @@ If you are a developer, please join our Developer Community on [Slack](https://s If you have a patch or have stumbled upon an issue with the Magento 2 plugin, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/PaystackHQ/plugin-magento-2/blob/master/CONTRIBUTING.md) for more information how you can do this. +[ico-version]: https://img.shields.io/packagist/v/pstk/paystack-magento2-module.svg?style=flat-square +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-downloads]: https://img.shields.io/packagist/dt/pstk/paystack-magento2-module.svg?style=flat-square + +[link-packagist]: https://packagist.org/packages/pstk/paystack-magento2-module +[link-downloads]: https://packagist.org/packages/pstk/paystack-magento2-module diff --git a/composer.json b/composer.json index b8a4a37..942427b 100644 --- a/composer.json +++ b/composer.json @@ -1,10 +1,8 @@ { "name": "pstk/paystack-magento2-module", "description": "Paystack Magento2 Module using \\Magento\\Payment\\Model\\Method\\AbstractMethod", - "version": "2.5.0", - "require": { - "yabacon/paystack-php": "2.*" - }, + "version": "3.0.0", + "require": {}, "type": "magento2-module", "license": [ "MIT" diff --git a/dev/.env.example b/dev/.env.example new file mode 100644 index 0000000..ae47057 --- /dev/null +++ b/dev/.env.example @@ -0,0 +1,2 @@ +PAYSTACK_TEST_PK=pk_test_your_public_key_here +PAYSTACK_TEST_SK=sk_test_your_secret_key_here diff --git a/dev/Dockerfile b/dev/Dockerfile new file mode 100644 index 0000000..c7280f5 --- /dev/null +++ b/dev/Dockerfile @@ -0,0 +1,54 @@ +FROM php:8.3-apache + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git unzip curl \ + libfreetype6-dev libjpeg62-turbo-dev libpng-dev \ + libicu-dev libxml2-dev libxslt-dev libzip-dev libonig-dev \ + libsodium-dev default-mysql-client \ + && rm -rf /var/lib/apt/lists/* + +# Install PHP extensions required by Magento 2 +RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) \ + bcmath ftp gd intl mbstring pdo_mysql soap xsl zip sockets sodium opcache + +# Enable Apache modules +RUN a2enmod rewrite headers + +# Install Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Configure PHP for Magento +RUN echo "memory_limit = 2G" > /usr/local/etc/php/conf.d/magento.ini \ + && echo "max_execution_time = 1800" >> /usr/local/etc/php/conf.d/magento.ini \ + && echo "realpath_cache_size = 10M" >> /usr/local/etc/php/conf.d/magento.ini \ + && echo "realpath_cache_ttl = 7200" >> /usr/local/etc/php/conf.d/magento.ini + +WORKDIR /var/www/html + +# Download Magento via Mage-OS public mirror (no Adobe marketplace auth needed) +RUN composer config --global audit.block-insecure false \ + && composer create-project \ + --repository-url=https://mirror.mage-os.org/ \ + magento/project-community-edition=2.4.8-p3 . \ + --no-dev --no-interaction + +# Point Apache to Magento's pub/ directory +RUN sed -i 's|/var/www/html|/var/www/html/pub|g' /etc/apache2/sites-available/000-default.conf +RUN printf '\n AllowOverride All\n\n' >> /etc/apache2/apache2.conf + +# Disable CSP enforcement for local development +# (Magento 2.4.8 CSP blocks its own inline scripts without this) +RUN echo 'Header unset Content-Security-Policy' >> /etc/apache2/apache2.conf + +# Fix permissions +RUN chown -R www-data:www-data /var/www/html + +COPY entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.sh + +EXPOSE 80 + +ENTRYPOINT ["entrypoint.sh"] +CMD ["apache2-foreground"] diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml new file mode 100644 index 0000000..42a197f --- /dev/null +++ b/dev/docker-compose.yml @@ -0,0 +1,41 @@ +name: paystack-magento + +services: + db: + container_name: paystack-db + image: mariadb:10.6 + environment: + MARIADB_ROOT_PASSWORD: magento + MARIADB_DATABASE: magento + MARIADB_USER: magento + MARIADB_PASSWORD: magento + volumes: + - db_data:/var/lib/mysql + + search: + container_name: paystack-search + image: opensearchproject/opensearch:2.19.1 + environment: + - discovery.type=single-node + - "OPENSEARCH_JAVA_OPTS=-Xms256m -Xmx256m" + - DISABLE_SECURITY_PLUGIN=true + volumes: + - search_data:/usr/share/opensearch/data + + magento: + container_name: paystack-magento + build: . + ports: + - "8080:80" + environment: + - PAYSTACK_TEST_PK=${PAYSTACK_TEST_PK} + - PAYSTACK_TEST_SK=${PAYSTACK_TEST_SK} + volumes: + - ..:/var/www/html/app/code/Pstk/Paystack + depends_on: + - db + - search + +volumes: + db_data: + search_data: diff --git a/dev/entrypoint.sh b/dev/entrypoint.sh new file mode 100644 index 0000000..88ca037 --- /dev/null +++ b/dev/entrypoint.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +# Only run Magento setup on first boot +if [ ! -f /var/www/html/app/etc/env.php ]; then + echo "==> Waiting for database to be ready..." + until php -r "new PDO('mysql:host=paystack-db;dbname=magento', 'magento', 'magento');" 2>/dev/null; do + sleep 2 + done + + echo "==> Installing Magento..." + php bin/magento setup:install \ + --base-url=http://localhost:8080 \ + --db-host=paystack-db \ + --db-name=magento \ + --db-user=magento \ + --db-password=magento \ + --search-engine=opensearch \ + --opensearch-host=paystack-search \ + --opensearch-port=9200 \ + --admin-firstname=Admin \ + --admin-lastname=MyStore \ + --admin-email=admin@example.com \ + --admin-user=admin \ + --admin-password='Admin12345!' \ + --backend-frontname=admin \ + --language=en_US \ + --currency=NGN \ + --timezone=Africa/Lagos \ + --use-rewrites=1 \ + --cleanup-database + + chown -R www-data:www-data /var/www/html + echo "==> Magento installation complete." +fi + +exec "$@" diff --git a/dev/img/products/paystack-cap.png b/dev/img/products/paystack-cap.png new file mode 100644 index 0000000..71d07ff Binary files /dev/null and b/dev/img/products/paystack-cap.png differ diff --git a/dev/img/products/paystack-hoodie.png b/dev/img/products/paystack-hoodie.png new file mode 100644 index 0000000..99a95ad Binary files /dev/null and b/dev/img/products/paystack-hoodie.png differ diff --git a/dev/img/products/paystack-stickers.png b/dev/img/products/paystack-stickers.png new file mode 100644 index 0000000..cca2a75 Binary files /dev/null and b/dev/img/products/paystack-stickers.png differ diff --git a/dev/img/products/paystack-t-shirt.png b/dev/img/products/paystack-t-shirt.png new file mode 100644 index 0000000..368f4fd Binary files /dev/null and b/dev/img/products/paystack-t-shirt.png differ diff --git a/dev/img/products/paystack-water-bottle.png b/dev/img/products/paystack-water-bottle.png new file mode 100644 index 0000000..b607965 Binary files /dev/null and b/dev/img/products/paystack-water-bottle.png differ diff --git a/dev/seed-products.php b/dev/seed-products.php new file mode 100644 index 0000000..d444123 --- /dev/null +++ b/dev/seed-products.php @@ -0,0 +1,153 @@ + Paystack -> Pstk -> code -> app -> html +$magentoRoot = dirname(__DIR__, 5); +require $magentoRoot . '/app/bootstrap.php'; + +$bootstrap = Bootstrap::create(BP, $_SERVER); +$objectManager = $bootstrap->getObjectManager(); + +$state = $objectManager->get(\Magento\Framework\App\State::class); +try { + $state->setAreaCode(\Magento\Framework\App\Area::AREA_ADMINHTML); +} catch (\Exception $e) { + // Area code already set +} + +// Path where product images are mounted inside the container +$imgDir = '/var/www/html/app/code/Pstk/Paystack/dev/img/products'; + +// --- 1. Create a "Shop" category under root category (id=2) --- + +$categoryFactory = $objectManager->get(\Magento\Catalog\Model\CategoryFactory::class); +$categoryRepository = $objectManager->get(\Magento\Catalog\Api\CategoryRepositoryInterface::class); +$categoryCollection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\CollectionFactory::class)->create(); + +$existing = $categoryCollection + ->addAttributeToFilter('name', 'Shop') + ->addAttributeToFilter('parent_id', 2) + ->getFirstItem(); + +if ($existing && $existing->getId()) { + $categoryId = (int)$existing->getId(); + echo "Category 'Shop' already exists (ID: $categoryId)\n"; +} else { + $category = $categoryFactory->create(); + $category->setName('Shop'); + $category->setParentId(2); + $category->setIsActive(true); + $category->setIncludeInMenu(true); + $category->setPath('1/2'); + + $parentCategory = $categoryRepository->get(2); + $category->setPath($parentCategory->getPath()); + + $categoryRepository->save($category); + $categoryId = (int)$category->getId(); + echo "Created category 'Shop' (ID: $categoryId)\n"; +} + +// --- 2. Create 5 test products --- + +$productFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductInterfaceFactory::class); +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$stockRegistry = $objectManager->get(\Magento\CatalogInventory\Api\StockRegistryInterface::class); +$categoryLinkManagement = $objectManager->get(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$products = [ + ['sku' => 'paystack-tshirt', 'name' => 'Paystack T-Shirt', 'price' => 5000, 'image' => 'paystack-t-shirt.png'], + ['sku' => 'paystack-hoodie', 'name' => 'Paystack Hoodie', 'price' => 15000, 'image' => 'paystack-hoodie.png'], + ['sku' => 'paystack-cap', 'name' => 'Paystack Cap', 'price' => 3000, 'image' => 'paystack-cap.png'], + ['sku' => 'paystack-sticker-pack', 'name' => 'Paystack Sticker Pack', 'price' => 500, 'image' => 'paystack-stickers.png'], + ['sku' => 'paystack-water-bottle', 'name' => 'Paystack Water Bottle', 'price' => 7500, 'image' => 'paystack-water-bottle.png'], +]; + +foreach ($products as $p) { + try { + $existing = $productRepository->get($p['sku']); + echo " Already exists: {$p['name']} (SKU: {$p['sku']})\n"; + continue; + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + // Product doesn't exist, create it + } + + $product = $productFactory->create(); + $product->setSku($p['sku']); + $product->setName($p['name']); + $product->setPrice($p['price']); + $product->setAttributeSetId(4); + $product->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + $product->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH); + $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE); + $product->setWeight(1); + + // Add product image if available + $imagePath = $imgDir . '/' . $p['image']; + if (file_exists($imagePath)) { + // Copy to Magento's import directory (addImageToMediaGallery moves from there) + $importDir = BP . '/pub/media/import'; + if (!is_dir($importDir)) { + mkdir($importDir, 0775, true); + } + $destPath = $importDir . '/' . $p['image']; + copy($imagePath, $destPath); + + $product->addImageToMediaGallery( + $destPath, + ['image', 'small_image', 'thumbnail'], + false, + false + ); + echo " Image: {$p['image']}\n"; + } else { + echo " No image found at: {$imagePath}\n"; + } + + $product = $productRepository->save($product); + + // Set stock + $stockItem = $stockRegistry->getStockItemBySku($p['sku']); + $stockItem->setQty(100); + $stockItem->setIsInStock(true); + $stockRegistry->updateStockItemBySku($p['sku'], $stockItem); + + // Assign to Shop category + $categoryLinkManagement->assignProductToCategories($p['sku'], [$categoryId]); + + echo " Created: {$p['name']} — NGN " . number_format($p['price']) . "\n"; +} + +// --- 3. Update homepage CMS to show products --- + +$pageRepository = $objectManager->get(\Magento\Cms\Api\PageRepositoryInterface::class); +$searchCriteriaBuilder = $objectManager->get(\Magento\Framework\Api\SearchCriteriaBuilder::class); + +$searchCriteria = $searchCriteriaBuilder + ->addFilter('identifier', 'home') + ->create(); + +$pages = $pageRepository->getList($searchCriteria); +$items = $pages->getItems(); + +$widgetContent = '

Welcome to the Paystack Test Store

' + . '{{widget type="Magento\CatalogWidget\Block\Product\ProductsList" title="Our Products" products_count="5" template="Magento_CatalogWidget::product/widget/content/grid.phtml" conditions_encoded="^[`1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Combine`,`aggregator`:`all`,`value`:`1`,`new_child`:``^]^]"}}' + . '
'; + +if (count($items) > 0) { + $homePage = array_values($items)[0]; + $homePage->setContent($widgetContent); + $pageRepository->save($homePage); + echo "\nUpdated homepage to display products.\n"; +} else { + echo "\nNote: Could not find homepage CMS page. Navigate to /shop.html to see products.\n"; +} + +echo "\nDone! Products are available at:\n"; +echo " Homepage: http://localhost:8080\n"; +echo " Category: http://localhost:8080/shop.html\n"; diff --git a/dev/setup.sh b/dev/setup.sh new file mode 100644 index 0000000..4482fa1 --- /dev/null +++ b/dev/setup.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# +# Paystack Magento 2 — Development Environment Setup +# +# Prerequisites: +# 1. cp .env.example .env (then fill in your Paystack test keys) +# 2. docker compose up -d (wait ~3 minutes for Magento to install) +# 3. bash setup.sh +# +set -e + +# Prevent Git Bash (MSYS/MinGW) from converting Unix paths to Windows paths +export MSYS_NO_PATHCONV=1 + +source .env + +# Helper: run magento CLI as www-data to avoid root-owned file permission issues +mage() { + docker compose exec --user www-data magento php bin/magento "$@" +} + +echo "==> Waiting for Magento to be ready..." +until mage --version 2>/dev/null; do + echo " Still initializing... (this can take a few minutes on first run)" + sleep 10 +done + +echo "" +echo "==> Fixing file ownership..." +docker compose exec magento chown -R www-data:www-data \ + /var/www/html/var \ + /var/www/html/generated \ + /var/www/html/pub/static \ + /var/www/html/app/etc + +echo "" +echo "==> Configuring admin security for local development..." +mage config:set admin/security/lockout_failures 0 +mage config:set admin/security/lockout_threshold 0 +mage config:set admin/security/password_is_forced 0 +mage config:set admin/security/password_lifetime 0 + +echo "" +echo "==> Disabling 2FA for local development..." +mage module:disable Magento_AdminAdobeImsTwoFactorAuth Magento_TwoFactorAuth --clear-static-content + +echo "" +echo "==> Enabling Pstk_Paystack module..." +mage module:enable Pstk_Paystack --clear-static-content + +echo "" +echo "==> Running setup:upgrade..." +mage setup:upgrade + +echo "" +echo "==> Switching to developer mode..." +mage deploy:mode:set developer + +echo "" +echo "==> Compiling DI..." +mage setup:di:compile + +echo "" +echo "==> Configuring Paystack payment method..." +mage config:set payment/pstk_paystack/active 1 +mage config:set payment/pstk_paystack/test_mode 1 +mage config:set payment/pstk_paystack/test_public_key "$PAYSTACK_TEST_PK" +mage config:set payment/pstk_paystack/test_secret_key "$PAYSTACK_TEST_SK" +mage config:set payment/pstk_paystack/integration_type inline + +echo "" +echo "==> Setting store currency to NGN..." +mage config:set currency/options/base NGN +mage config:set currency/options/default NGN +mage config:set currency/options/allow NGN + +echo "" +echo "==> Creating test products with images..." +docker compose exec --user www-data magento php app/code/Pstk/Paystack/dev/seed-products.php + +echo "" +echo "==> Reindexing..." +mage indexer:reindex + +echo "" +echo "==> Flushing cache..." +mage cache:flush + +echo "" +echo "============================================" +echo " Setup complete!" +echo "" +echo " Storefront: http://localhost:8080" +echo " Admin panel: http://localhost:8080/admin" +echo " Admin login: admin / Admin12345!" +echo "" +echo " Test card: 4084 0840 8408 4081" +echo " Expiry: 12/30" +echo " CVV: 408" +echo " PIN: 0000" +echo " OTP: 123456" +echo "============================================" diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 1f9e92f..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: '3.7' - -services: - mysql: - image: mysql:5 - volumes: - - db_data:/var/lib/mysql - restart: always - env_file: .env - - magento2: - depends_on: - - mysql - build: - context: . - args: - - magento_username=$magento_username - - magento_password=$magento_password - image: paystack-magento2 - ports: - - "8000:80" - links: - - mysql - env_file: .env -volumes: - db_data: {} diff --git a/etc/di.xml b/etc/di.xml index b72f4b4..9eedbe6 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -3,5 +3,14 @@ - + + + + + + Pstk\Paystack\Model\CspPolicyCollector + + + + diff --git a/etc/frontend/csp_whitelist.xml b/etc/frontend/csp_whitelist.xml deleted file mode 100644 index 8397610..0000000 --- a/etc/frontend/csp_whitelist.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - js.paystack.co - api.paystack.co - - - - - - api.paystack.co - js.paystack.co - plugin-tracker.paystackintegrations.com - - - - - - standard.paystack.co - - - - diff --git a/etc/module.xml b/etc/module.xml index 9707dd5..becbee4 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - +