diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index b4344de..ec4cdbe 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -19,6 +19,9 @@ jobs: - name: Create branch for Composer and remove version from composer.json run: git checkout -b continuous-integration-test-branch && sed -i '/version/d' ./composer.json + - name: Remove PHPStan duplicate include + run: sed -i '/vendor\/bitexpert/d' ./phpstan.neon + - name: Upload our code into the docker container run: docker cp $(pwd) magento-project-community-edition:/data/extensions/ diff --git a/Api/Config/System/SettingInterface.php b/Api/Config/System/SettingInterface.php index a07b3f7..42b3761 100644 --- a/Api/Config/System/SettingInterface.php +++ b/Api/Config/System/SettingInterface.php @@ -20,6 +20,7 @@ interface SettingInterface public const XML_PATH_SEND_ORDER_EMAIL = 'payment/truelayer/send_order_email'; public const XML_PATH_SEND_INVOICE_EMAIL = 'payment/truelayer/send_invoice_email'; public const XML_PATH_BANKING_PROVIDERS = 'payment/truelayer/banking_providers'; + public const XML_PATH_RELEASE_CHANNEL = 'payment/truelayer/release_channel'; public const XML_PATH_PAYMENT_PAGE_PRIMARY_COLOR = 'payment/truelayer/payment_page_primary_color'; public const XML_PATH_PAYMENT_PAGE_SECONDARY_COLOR = 'payment/truelayer/payment_page_secondary_color'; public const XML_PATH_PAYMENT_PAGE_TERTIARY_COLOR = 'payment/truelayer/payment_page_tertiary_color'; @@ -48,6 +49,15 @@ public function getMaximumOrderTotal(): float; */ public function getBankingProviders(?int $storeId = null): array; + /** + * Get associated array of credentials + * + * @param int|null $storeId + * + * @return string + */ + public function getReleaseChannel(?int $storeId = null): string; + /** * Get payment page primary color * diff --git a/Api/Log/LogServiceInterface.php b/Api/Log/LogServiceInterface.php index 1bfe214..52f8421 100644 --- a/Api/Log/LogServiceInterface.php +++ b/Api/Log/LogServiceInterface.php @@ -19,7 +19,7 @@ interface LogServiceInterface * @param string $type * @param mixed $data */ - public function error(string $type, $data): LogServiceInterface; + public function error(string $type, $data = ''): LogServiceInterface; /** * Add record to debug log @@ -27,7 +27,7 @@ public function error(string $type, $data): LogServiceInterface; * @param string $type * @param mixed $data */ - public function debug(string $type, $data): LogServiceInterface; + public function debug(string $type, $data = ''): LogServiceInterface; /** * @param string|int $prefix diff --git a/Block/Adminhtml/System/Config/Button/Credentials.php b/Block/Adminhtml/System/Config/Button/Credentials.php index 0d13680..f8aeab4 100644 --- a/Block/Adminhtml/System/Config/Button/Credentials.php +++ b/Block/Adminhtml/System/Config/Button/Credentials.php @@ -92,10 +92,10 @@ public function getApiCheckUrl(): string public function getButtonHtml(): string { try { - return $this->getLayout() - ->createBlock(Button::class) - ->setData(['id' => 'truelayer-button_credentials', 'label' => __('Check Credentials')]) - ->toHtml(); + /** @var \Magento\Framework\View\Element\AbstractBlock $block */ + $block = $this->getLayout()->createBlock(Button::class); + $block->setData(['id' => 'truelayer-button_credentials', 'label' => __('Check Credentials')]); + return $block->toHtml(); } catch (Exception $e) { $this->logger->error('Credentials check', $e->getMessage()); return ''; diff --git a/Block/Adminhtml/System/Config/Button/DebugCheck.php b/Block/Adminhtml/System/Config/Button/DebugCheck.php index 01f8e97..206f49c 100644 --- a/Block/Adminhtml/System/Config/Button/DebugCheck.php +++ b/Block/Adminhtml/System/Config/Button/DebugCheck.php @@ -58,12 +58,13 @@ public function getDebugCheckUrl(): string public function getButtonHtml(): string { try { - return $this->getLayout() - ->createBlock(Button::class) - ->setData([ - 'id' => 'truelayer-button_debug', - 'label' => __('Check last 100 debug log records') - ])->toHtml(); + /** @var \Magento\Framework\View\Element\AbstractBlock $block */ + $block = $this->getLayout()->createBlock(Button::class); + $block->setData([ + 'id' => 'truelayer-button_debug', + 'label' => __('Check last 100 debug log records') + ]); + return $block->toHtml(); } catch (Exception $e) { return ''; } diff --git a/Block/Adminhtml/System/Config/Button/ErrorCheck.php b/Block/Adminhtml/System/Config/Button/ErrorCheck.php index 1b4427a..2e36aa8 100644 --- a/Block/Adminhtml/System/Config/Button/ErrorCheck.php +++ b/Block/Adminhtml/System/Config/Button/ErrorCheck.php @@ -58,12 +58,13 @@ public function getErrorCheckUrl(): string public function getButtonHtml(): string { try { - return $this->getLayout() - ->createBlock(Button::class) - ->setData([ - 'id' => 'truelayer-button_error', - 'label' => __('Check last 100 error log records') - ])->toHtml(); + /** @var \Magento\Framework\View\Element\AbstractBlock $block */ + $block = $this->getLayout()->createBlock(Button::class); + $block->setData([ + 'id' => 'truelayer-button_error', + 'label' => __('Check last 100 error log records') + ]); + return $block->toHtml(); } catch (Exception $e) { return ''; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ba107..ad52707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v2.3.0] - 2024-09-20 + +### Added + +- Release channel configuration option +- Fix settings visibility based on scopes + +### Changed + +- Stricter PHPStan rules + ## [v2.2.0] - 2024-09-02 ### Added diff --git a/Gateway/Command/AbstractCommand.php b/Gateway/Command/AbstractCommand.php index 732cf53..c03bb66 100644 --- a/Gateway/Command/AbstractCommand.php +++ b/Gateway/Command/AbstractCommand.php @@ -65,7 +65,6 @@ protected function getOrder(array $subject): OrderInterface /** * @param array $subject - * @return mixed */ abstract protected function executeCommand(array $subject): void; } diff --git a/Model/Config/Source/ReleaseChannel.php b/Model/Config/Source/ReleaseChannel.php new file mode 100644 index 0000000..8aee359 --- /dev/null +++ b/Model/Config/Source/ReleaseChannel.php @@ -0,0 +1,34 @@ + self::GENERAL_AVAILABILITY, 'label' => __('General Availability')], + ['value' => self::PUBLIC_BETA, 'label' => __('Public Beta')], + ['value' => self::PRIVATE_BETA, 'label' => __('Private Beta')] + ]; + } +} diff --git a/Model/Config/System/SettingsRepository.php b/Model/Config/System/SettingsRepository.php index a48d78e..72fd70a 100644 --- a/Model/Config/System/SettingsRepository.php +++ b/Model/Config/System/SettingsRepository.php @@ -8,6 +8,7 @@ namespace TrueLayer\Connect\Model\Config\System; use TrueLayer\Connect\Api\Config\System\SettingInterface; +use TrueLayer\Connect\Model\Config\Source\ReleaseChannel; /** * Debug provider class @@ -43,6 +44,14 @@ public function getBankingProviders(?int $storeId = null): array } } + /** + * @inheritDoc + */ + public function getReleaseChannel(?int $storeId = null): string + { + return $this->getStoreValue(self::XML_PATH_RELEASE_CHANNEL, $storeId) ?: ReleaseChannel::GENERAL_AVAILABILITY; + } + /** * @inheritDoc */ diff --git a/Model/Transaction/Payment/PaymentTransactionRepository.php b/Model/Transaction/Payment/PaymentTransactionRepository.php index 1314c1c..dda5a68 100644 --- a/Model/Transaction/Payment/PaymentTransactionRepository.php +++ b/Model/Transaction/Payment/PaymentTransactionRepository.php @@ -54,6 +54,7 @@ public function create(): PaymentTransactionDataInterface /** * @inheritDoc + * @return PaymentTransactionDataModel */ public function get(int $entityId): PaymentTransactionDataInterface { @@ -64,12 +65,16 @@ public function get(int $entityId): PaymentTransactionDataInterface $exceptionMsg = self::NO_SUCH_ENTITY_EXCEPTION; throw new NoSuchEntityException(__($exceptionMsg, $entityId)); } - return $this->dataFactory->create() - ->load($entityId); + + /** @var PaymentTransactionDataModel $transaction */ + $transaction = $this->dataFactory->create(); + $this->resource->load($transaction, $entityId); + return $transaction; } /** * @inheritDoc + * @return PaymentTransactionDataModel */ public function getByOrderId(int $orderId): PaymentTransactionDataInterface { @@ -79,12 +84,16 @@ public function getByOrderId(int $orderId): PaymentTransactionDataInterface } elseif (!$this->resource->isOrderIdExists($orderId)) { throw new NoSuchEntityException(__('No record found for OrderID: %1.', $orderId)); } - return $this->dataFactory->create() - ->load($orderId, 'order_id'); + + /** @var PaymentTransactionDataModel $transaction */ + $transaction = $this->dataFactory->create(); + $this->resource->load($transaction, $orderId, 'order_id'); + return $transaction; } /** * @inheritDoc + * @return PaymentTransactionDataModel */ public function getByPaymentUuid(string $uuid): PaymentTransactionDataInterface { @@ -95,12 +104,17 @@ public function getByPaymentUuid(string $uuid): PaymentTransactionDataInterface throw new NoSuchEntityException(__('No record found for uuid: %1.', $uuid)); } - return $this->dataFactory->create() - ->load($uuid, 'uuid'); + $transaction = $this->dataFactory->create(); + /** @var PaymentTransactionDataModel $transaction */ + $this->resource->load($transaction, $uuid, 'uuid'); + return $transaction; + } /** * @inheritDoc + * @param PaymentTransactionDataModel $entity + * @return PaymentTransactionDataModel */ public function save(PaymentTransactionDataInterface $entity): PaymentTransactionDataInterface { diff --git a/Model/Transaction/Refund/RefundCollection.php b/Model/Transaction/Refund/RefundCollection.php index 9a0f0d9..8e3b62d 100644 --- a/Model/Transaction/Refund/RefundCollection.php +++ b/Model/Transaction/Refund/RefundCollection.php @@ -9,6 +9,9 @@ use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +/** + * @method RefundTransactionDataModel getFirstItem() + */ class RefundCollection extends AbstractCollection { /** diff --git a/Model/Transaction/Refund/RefundTransactionRepository.php b/Model/Transaction/Refund/RefundTransactionRepository.php index 32b6623..26a4b12 100644 --- a/Model/Transaction/Refund/RefundTransactionRepository.php +++ b/Model/Transaction/Refund/RefundTransactionRepository.php @@ -100,6 +100,7 @@ public function getByCreditMemoId(int $id): RefundTransactionDataInterface /** * @inheritDoc + * @param \TrueLayer\Connect\Model\Transaction\Refund\RefundTransactionDataModel $entity */ public function save(RefundTransactionDataInterface $entity): RefundTransactionDataInterface { @@ -116,13 +117,14 @@ public function save(RefundTransactionDataInterface $entity): RefundTransactionD /** * @param string $col * @param $value - * @return RefundTransactionDataInterface + * @return RefundTransactionDataModel * @throws NoSuchEntityException */ private function getByColumn(string $col, $value): RefundTransactionDataInterface { - /** @var RefundTransactionDataInterface $transaction */ - $transaction = $this->dataFactory->create()->load($value, $col); + /** @var RefundTransactionDataModel $transaction */ + $transaction = $this->dataFactory->create(); + $this->resource->load($transaction, $value, $col); if (!$transaction->getEntityId()) { $this->logger->error('Refund transaction not found', $value); @@ -135,10 +137,11 @@ private function getByColumn(string $col, $value): RefundTransactionDataInterfac /** * @param array $cols * @param array $sort - * @return RefundTransactionDataInterface + * @return RefundTransactionDataModel */ public function getOneByColumns(array $cols, array $sort = []): RefundTransactionDataInterface { + $collection = $this->collectionFactory->create(); foreach ($cols as $col => $value) { diff --git a/Model/Ui/ConfigProvider.php b/Model/Ui/ConfigProvider.php index 8c94103..8b5ef2b 100644 --- a/Model/Ui/ConfigProvider.php +++ b/Model/Ui/ConfigProvider.php @@ -30,7 +30,7 @@ public function __construct( /** * Get config * - * @return \array[][] + * @return array{payment:array{truelayer:array{description:string|null}}} */ public function getConfig(): array { diff --git a/Model/User/ResourceModel.php b/Model/User/ResourceModel.php index ce1ee82..e2ad03e 100644 --- a/Model/User/ResourceModel.php +++ b/Model/User/ResourceModel.php @@ -55,7 +55,7 @@ public function set(string $email, string $userId): bool } /** - * @param string $uuid + * @param string $userId * @return mixed */ public function getByTruelayerId(string $userId) diff --git a/Plugin/Payment/MethodList.php b/Plugin/Payment/MethodList.php index 238e172..ccbc7e2 100644 --- a/Plugin/Payment/MethodList.php +++ b/Plugin/Payment/MethodList.php @@ -32,7 +32,7 @@ public function __construct( /** * @param Subject $subject * @param $availableMethods - * @param CartInterface|null $quote + * @param \Magento\Quote\Model\Quote|null $quote * @return mixed */ public function afterGetAvailableMethods( diff --git a/README.md b/README.md index 0f8741c..dac7dbe 100644 --- a/README.md +++ b/README.md @@ -3,56 +3,34 @@ The TrueLayer plugin makes it effortless to connect your Magento® 2 catalog with the TrueLayer Payment Services. ## Installation -To make the integration process as easy as possible for you, we have developed various plugins for your webshop software package. -This is the manual for installing the Magento® 2 Plugin. -Before you start up the installation process, we recommend that you make a backup of your webshop files, as well as the database. +Before you start the installation process, we recommend that you make a backup of your store, as well as the database. -There are 2 different methods to install the Magento® 2 extension. -1. Install by using Composer -2. Install by using the Magento® Marketplace (coming soon!) - -### Installation using Composer ### -Magento® 2 use the Composer to manage the module package and the library. Composer is a dependency manager for PHP. Composer declare the libraries your project depends on and it will manage (install/update) them for you. - -Check if your server has composer installed by running the following command: +You can use Composer to install this package. First, check if your server has Composer installed by running the following command: ``` composer –v ``` -If your server doesn’t have composer installed, you can easily install it by using this manual: https://getcomposer.org/doc/00-intro.md -Step-by-step to install the Magento® 2 extension through Composer: +If your server doesn't have composer installed, you can easily install by following the instructions here: https://getcomposer.org/doc/00-intro.md + +You can then install this Magento® 2 extension through Composer: 1. Connect to your server running Magento® 2 using SSH or other method (make sure you have access to the command line). 2. Locate your Magento® 2 project root. -3. Install the Magento® 2 extension through composer and wait till it's completed: +3. Install the extension through composer: ``` composer require truelayer/magento2 ``` -4. Once completed run the Magento® module enable command: +4. Once completed run the following commands: ``` bin/magento module:enable TrueLayer_Connect -``` -5. After that run the Magento® upgrade and clean the caches: -``` -php bin/magento setup:upgrade -php bin/magento cache:flush +bin/magento setup:upgrade +bin/magento cache:flush ``` 6. If Magento® is running in production mode you also need to redeploy the static content: ``` -php bin/magento setup:static-content:deploy +bin/magento setup:static-content:deploy ``` -7. After the installation: Go to your Magento® admin portal and open ‘Stores’ > ‘Configuration’ > ‘Payment Methods’ > ‘TrueLayer’. - -## Development by Magmodules - -We are a Dutch agency dedicated to the development of extensions for Magento and Shopware. All our extensions are coded by our own team and our support team is always there to help you out. - -[Visit Magmodules.eu](https://www.magmodules.eu/) - -## Developed for TrueLayer - -The TrueLayer plugin solves this, enabling businesses with a Magento 2 webshop a seamless way to integrate instant bank payments into their website with minimal technical and developer resources required. -[Visit Truelayer.com](https://truelayer.com/) +7. After the installation, go to your Magento® admin portal and open ‘Stores’ > ‘Configuration’ > ‘Sales’ > ‘TrueLayer’. # Local development A basic docker-compose configuration is provided to make local development easier. To start it, run the following: diff --git a/Service/Client/ClientFactory.php b/Service/Client/ClientFactory.php index 049a696..9cad06e 100644 --- a/Service/Client/ClientFactory.php +++ b/Service/Client/ClientFactory.php @@ -12,6 +12,7 @@ use TrueLayer\Client; use TrueLayer\Connect\Api\Config\RepositoryInterface as ConfigRepository; use TrueLayer\Connect\Api\Log\LogServiceInterface; +use TrueLayer\Exceptions\InvalidArgumentException; use TrueLayer\Exceptions\SignerException; use TrueLayer\Interfaces\Client\ClientInterface; use TrueLayer\Settings; @@ -27,7 +28,7 @@ class ClientFactory */ public function __construct( ConfigRepository $configProvider, - LogServiceInterface $logger + LogServiceInterface $logger, ) { $this->configProvider = $configProvider; $this->logger = $logger; @@ -54,18 +55,22 @@ public function create(int $storeId = 0, ?array $data = []): ?ClientInterface /** * @param array $credentials + * @param bool|null $forceSandbox * @return ClientInterface|null + * @throws InvalidArgumentException * @throws SignerException */ private function createClient(array $credentials, ?bool $forceSandbox = null): ?ClientInterface { Settings::tlAgent('truelayer-magento/' . $this->configProvider->getExtensionVersion()); - return Client::configure() - ->clientId($credentials['client_id']) + + $clientFactory = Client::configure(); + $clientFactory->clientId($credentials['client_id']) ->clientSecret($credentials['client_secret']) ->keyId($credentials['key_id']) ->pemFile($credentials['private_key']) - ->useProduction(is_null($forceSandbox) ? !$this->configProvider->isSandbox() : !$forceSandbox) - ->create(); + ->useProduction(is_null($forceSandbox) ? !$this->configProvider->isSandbox() : !$forceSandbox); + + return $clientFactory->create(); } } diff --git a/Service/Order/PaymentCreationService.php b/Service/Order/PaymentCreationService.php index 953a24f..cc83d90 100644 --- a/Service/Order/PaymentCreationService.php +++ b/Service/Order/PaymentCreationService.php @@ -125,7 +125,7 @@ private function createPaymentConfig( "countries" => [ $order->getBillingAddress()->getCountryId() ], - "release_channel" => "general_availability", + "release_channel" => $this->configRepository->getReleaseChannel((int) $order->getStoreId()), "customer_segments" => $this->configRepository->getBankingProviders(), "excludes" => [ "provider_ids" => [ diff --git a/Service/Order/PaymentUpdate/PaymentFailedService.php b/Service/Order/PaymentUpdate/PaymentFailedService.php index ef34edf..38a7544 100644 --- a/Service/Order/PaymentUpdate/PaymentFailedService.php +++ b/Service/Order/PaymentUpdate/PaymentFailedService.php @@ -62,6 +62,7 @@ public function handle(string $paymentId, string $failureReason): void */ private function cancelOrder(PaymentTransactionDataInterface $transaction, string $failureReason): void { + /** @var \Magento\Sales\Model\Order $order */ $order = $this->orderRepository->get($transaction->getOrderId()); if (!$order->isCanceled()) { @@ -70,6 +71,7 @@ private function cancelOrder(PaymentTransactionDataInterface $transaction, strin } // Update order payment + /** @var \Magento\Sales\Model\Order\Payment $payment */ $payment = $order->getPayment(); $payment->setLastTransId($transaction->getPaymentUuid()); $payment->cancel(); diff --git a/Service/Order/PaymentUpdate/PaymentSettledService.php b/Service/Order/PaymentUpdate/PaymentSettledService.php index 60bc525..12e7bb4 100644 --- a/Service/Order/PaymentUpdate/PaymentSettledService.php +++ b/Service/Order/PaymentUpdate/PaymentSettledService.php @@ -78,9 +78,14 @@ public function handle(string $paymentId): void $this->logger->removePrefix($prefix); } + /** + * @param \Magento\Sales\Model\Order $order + * @return void + */ private function updateOrder(OrderInterface $order, string $paymentId): void { // Update order payment + /** @var \Magento\Sales\Model\Order\Payment $payment */ $payment = $order->getPayment(); $payment->setTransactionId($paymentId); $payment->setIsTransactionClosed(true); @@ -93,7 +98,7 @@ private function updateOrder(OrderInterface $order, string $paymentId): void } /** - * @param OrderInterface $order + * @param \Magento\Sales\Model\Order $order * @return void */ private function sendOrderEmail(OrderInterface $order): void @@ -117,7 +122,7 @@ private function sendOrderEmail(OrderInterface $order): void } /** - * @param OrderInterface $order + * @param \Magento\Sales\Model\Order $order * @throws Exception */ private function sendInvoiceEmail(OrderInterface $order): void diff --git a/composer.json b/composer.json index 5ddab9b..764cb1e 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "php": ">=7.4.0" }, "require-dev": { - "phpstan/phpstan": "*" + "phpstan/phpstan": "*", + "bitexpert/phpstan-magento": "*" }, "autoload": { "files": [ @@ -25,5 +26,27 @@ "psr-4": { "TrueLayer\\Connect\\": "" } + }, + "config": { + "allow-plugins": { + "magento/composer-dependency-version-audit-plugin": true, + "magento/magento-composer-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "magento/inventory-composer-installer": true, + "php-http/discovery": true, + "magento/composer-root-update-plugin": true + } + }, + "scripts": { + "analyse": "vendor/bin/phpstan analyse --memory-limit=-1", + "checks": [ + "@analyse" + ] + }, + "repositories": { + "magento": { + "type": "composer", + "url": "https://repo.magento.com/" + } } } diff --git a/etc/adminhtml/system/general.xml b/etc/adminhtml/system/general.xml index eb110f8..d63387e 100644 --- a/etc/adminhtml/system/general.xml +++ b/etc/adminhtml/system/general.xml @@ -112,7 +112,7 @@ sandbox - +