diff --git a/.github/workflows/codesniffer.yml b/.github/workflows/codesniffer.yml deleted file mode 100644 index 675d9de..0000000 --- a/.github/workflows/codesniffer.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Codesniffer with the Magento Coding standard -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Run codesniffer - run: - docker run - --volume $(pwd)/:/app/workdir - michielgerritsen/magento-coding-standard:latest - --severity=6 diff --git a/Api/Log/RepositoryInterface.php b/Api/Log/LogServiceInterface.php similarity index 50% rename from Api/Log/RepositoryInterface.php rename to Api/Log/LogServiceInterface.php index 04f9a7d..1bfe214 100644 --- a/Api/Log/RepositoryInterface.php +++ b/Api/Log/LogServiceInterface.php @@ -11,16 +11,15 @@ * Log repository interface * @api */ -interface RepositoryInterface +interface LogServiceInterface { - /** * Add record to error log * * @param string $type * @param mixed $data */ - public function addErrorLog(string $type, $data): void; + public function error(string $type, $data): LogServiceInterface; /** * Add record to debug log @@ -28,5 +27,16 @@ public function addErrorLog(string $type, $data): void; * @param string $type * @param mixed $data */ - public function addDebugLog(string $type, $data): void; + public function debug(string $type, $data): LogServiceInterface; + + /** + * @param string|int $prefix + */ + public function addPrefix($prefix): LogServiceInterface; + + /** + * @param string|int $prefix + * @return LogServiceInterface + */ + public function removePrefix($prefix): LogServiceInterface; } diff --git a/Api/Transaction/BaseTransactionDataInterface.php b/Api/Transaction/BaseTransactionDataInterface.php new file mode 100644 index 0000000..9592da6 --- /dev/null +++ b/Api/Transaction/BaseTransactionDataInterface.php @@ -0,0 +1,108 @@ +setData(['id' => 'truelayer-button_credentials', 'label' => __('Check Credentials')]) ->toHtml(); } catch (Exception $e) { - $this->logger->addErrorLog('Credentials check', $e->getMessage()); + $this->logger->error('Credentials check', $e->getMessage()); return ''; } } diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0d46e..63a8d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ 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.0.0] - 2024-06-19 + +### Added + +- Payment metadata including store and order ID +- Support for handling failing refunds + +### Changed + +- Place orders upfront and update them throughout the payment lifecycle +- Improved logging + +### Fixed + +- Minicart cache busting +- Refund metadata being set to NULL +- Improved database indexes +- Improved idempotency for webhooks +- Payment creation failing when shipping address not required +- Issues duplicating Quotes + ## [v1.0.10] - 2024-05-07 ### Fixed @@ -37,7 +58,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [v1.0.6] - 2023-09-13 -### Changed: +### Changed - Wait for payment status updates on customer checkout - Admin panel improvements and fixes \ No newline at end of file diff --git a/Controller/Adminhtml/Credentials/Check.php b/Controller/Adminhtml/Credentials/Check.php index 699a24e..4b53bbf 100644 --- a/Controller/Adminhtml/Credentials/Check.php +++ b/Controller/Adminhtml/Credentials/Check.php @@ -16,7 +16,7 @@ use Magento\Framework\Filesystem\DirectoryList; use Magento\Framework\Filesystem\Io\File; use TrueLayer\Connect\Api\Config\RepositoryInterface as ConfigRepository; -use TrueLayer\Connect\Service\Api\GetClient; +use TrueLayer\Connect\Service\Client\ClientFactory; use TrueLayer\Interfaces\Client\ClientInterface; /** @@ -24,35 +24,19 @@ */ class Check extends Action implements HttpPostActionInterface { - private const PEM_UPLOAD_FILE = '/truelayer/temp/private_key.pem'; - /** - * @var DirectoryList - */ - private $directoryList; - /** - * @var GetClient - */ - private $getClient; - /** - * @var Json - */ - private $resultJson; - /** - * @var ConfigRepository - */ - private $configProvider; - /** - * @var File - */ - private $file; + private DirectoryList $directoryList; + private ClientFactory $clientFactory; + private Json $resultJson; + private ConfigRepository $configProvider; + private File $file; /** * Check constructor. * * @param Action\Context $context * @param JsonFactory $resultJsonFactory - * @param GetClient $getClient + * @param ClientFactory $clientFactory * @param ConfigRepository $configProvider * @param File $file * @param DirectoryList $directoryList @@ -60,12 +44,12 @@ class Check extends Action implements HttpPostActionInterface public function __construct( Action\Context $context, JsonFactory $resultJsonFactory, - GetClient $getClient, + ClientFactory $clientFactory, ConfigRepository $configProvider, File $file, DirectoryList $directoryList ) { - $this->getClient = $getClient; + $this->clientFactory = $clientFactory; $this->resultJson = $resultJsonFactory->create(); $this->configProvider = $configProvider; $this->file = $file; @@ -106,7 +90,7 @@ private function testCredentials(): ?ClientInterface throw new LocalizedException(__('No Client Secret set!')); } - $result = $this->getClient->execute( + $result = $this->clientFactory->create( (int)$config['store_id'], ['credentials' => $config['credentials']] ); diff --git a/Controller/Adminhtml/Log/Stream.php b/Controller/Adminhtml/Log/Stream.php index a66c055..3ec63a0 100644 --- a/Controller/Adminhtml/Log/Stream.php +++ b/Controller/Adminhtml/Log/Stream.php @@ -22,7 +22,6 @@ */ class Stream extends Action implements HttpPostActionInterface { - /** * Error log file path pattern */ @@ -32,22 +31,10 @@ class Stream extends Action implements HttpPostActionInterface */ public const MAX_LINES = 100; - /** - * @var JsonFactory - */ - private $resultJsonFactory; - /** - * @var DirectoryList - */ - private $dir; - /** - * @var File - */ - private $file; - /** - * @var RequestInterface - */ - private $request; + private JsonFactory $resultJsonFactory; + private DirectoryList $dir; + private File $file; + private RequestInterface $request; /** * Check constructor. diff --git a/Controller/Checkout/BaseController.php b/Controller/Checkout/BaseController.php new file mode 100644 index 0000000..479556a --- /dev/null +++ b/Controller/Checkout/BaseController.php @@ -0,0 +1,80 @@ +context = $context; + $this->logger = $logger; + $this->jsonFactory = $jsonFactory; + } + + /** + * @return ResultInterface|ResponseInterface + */ + public function execute() + { + $this->logger->debug('Execute'); + $result = $this->executeAction(); + + // Prevent caching + $result->setHeader('Cache-Control', self::CACHE_CONTROL, true); + + return $result; + } + + /** + * @return ResultInterface|ResponseInterface + */ + abstract protected function executeAction(); + + /** + * @param string $to + * @param array $arguments + * @return ResponseInterface + */ + protected function redirect(string $to, array $arguments = []): ResponseInterface + { + $response = $this->context->getResponse(); + $this->context->getRedirect()->redirect($response, $to, $arguments); + return $response; + } + + /** + * @param array $data + * @return JsonResult + */ + protected function jsonResponse(array $data): AbstractResult + { + return $this->jsonFactory->create()->setData($data); + } +} diff --git a/Controller/Checkout/Pending.php b/Controller/Checkout/Pending.php deleted file mode 100644 index 02b42b1..0000000 --- a/Controller/Checkout/Pending.php +++ /dev/null @@ -1,45 +0,0 @@ -pageFactory = $pageFactory; - parent::__construct($context); - } - - /** - * @return ResultInterface|Page - */ - public function execute() - { - return $this->pageFactory->create(); - } -} diff --git a/Controller/Checkout/Process.php b/Controller/Checkout/Process.php index ee6b9d7..45e4b78 100644 --- a/Controller/Checkout/Process.php +++ b/Controller/Checkout/Process.php @@ -7,73 +7,39 @@ namespace TrueLayer\Connect\Controller\Checkout; -use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Framework\App\Action\HttpGetActionInterface; -use Magento\Framework\Controller\Result\Redirect; -use TrueLayer\Connect\Api\Log\RepositoryInterface as LogRepository; -use TrueLayer\Connect\Service\Order\ProcessReturn; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\Controller\Result\JsonFactory; +use TrueLayer\Connect\Api\Log\LogServiceInterface as LogRepository; -/** - * Process Controller - */ -class Process extends Action implements HttpGetActionInterface +class Process extends BaseController implements HttpGetActionInterface, HttpPostActionInterface { + private PageFactory $pageFactory; /** - * @var LogRepository - */ - private $logRepository; - /** - * @var ProcessReturn - */ - private $processReturn; - - /** - * Process constructor. - * * @param Context $context - * @param ProcessReturn $processReturn - * @param LogRepository $logRepository + * @param JsonFactory $jsonFactory + * @param PageFactory $pageFactory + * @param LogRepository $logger */ public function __construct( Context $context, - ProcessReturn $processReturn, - LogRepository $logRepository + JsonFactory $jsonFactory, + PageFactory $pageFactory, + LogRepository $logger ) { - parent::__construct($context); - $this->processReturn = $processReturn; - $this->logRepository = $logRepository; + $this->pageFactory = $pageFactory; + parent::__construct($context, $jsonFactory, $logger); } /** - * @return Redirect + * @return ResultInterface */ - public function execute(): Redirect + public function executeAction(): ResultInterface { - $resultRedirect = $this->resultRedirectFactory->create(); - if (!$transactionId = $this->getRequest()->getParam('payment_id')) { - $this->messageManager->addErrorMessage(__('Error in return data from TrueLayer')); - $resultRedirect->setPath('checkout/cart/index'); - return $resultRedirect; - } - - try { - $result = $this->processReturn->execute((string)$transactionId); - if ($result['success']) { - $resultRedirect->setPath('checkout/onepage/success'); - } elseif (in_array($result['status'], ['settled', 'executed', 'authorized'])) { - $resultRedirect->setPath('truelayer/checkout/pending', ['payment_id' => $transactionId]); - } else { - $this->messageManager->addErrorMessage('Something went wrong'); - $resultRedirect->setPath('checkout/cart/index'); - } - } catch (\Exception $exception) { - $this->logRepository->addErrorLog('Checkout Process', $exception->getMessage()); - $this->messageManager->addErrorMessage('Error processing payment'); - $resultRedirect->setPath('checkout/cart/index'); - } - - return $resultRedirect; + return $this->pageFactory->create(); } } diff --git a/Controller/Checkout/Redirect.php b/Controller/Checkout/Redirect.php new file mode 100644 index 0000000..362a288 --- /dev/null +++ b/Controller/Checkout/Redirect.php @@ -0,0 +1,123 @@ +checkoutSession = $checkoutSession; + $this->orderRepository = $orderRepository; + $this->paymentCreationService = $paymentCreationService; + $this->paymentErrorMessageManager = $paymentErrorMessageManager; + $this->hppService = $hppService; + $logger = $logger->addPrefix('RedirectController'); + parent::__construct($context, $jsonFactory, $logger); + } + + /** + * @return ResponseInterface + */ + public function executeAction(): ResponseInterface + { + try { + return $this->createPaymentAndRedirect(); + } catch (Exception $e) { + $this->logger->error('Failed to create payment and redirect to HPP', $e); + $this->failOrder(); + $this->checkoutSession->restoreQuote(); + return $this->redirectToFailPage(); + } + } + + /** + * @return ResponseInterface + * @throws ApiRequestJsonSerializationException + * @throws ApiResponseUnsuccessfulException + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws LocalizedException + * @throws SignerException + * @throws InputException + * @throws NoSuchEntityException + */ + private function createPaymentAndRedirect(): ResponseInterface + { + $order = $this->checkoutSession->getLastRealOrder(); + $this->logger->addPrefix("order {$order->getEntityId()}"); + + $created = $this->paymentCreationService->createPayment($order); + $url = $this->hppService->getRedirectUrl($created); + + return $this->redirect($url); + } + + private function failOrder(): void + { + $order = $this->checkoutSession->getLastRealOrder(); + if (!$order->isCanceled()) { + $order->cancel(); + $this->logger->debug('Order cancelled, failed to create payment'); + } + $order->addStatusToHistory($order->getStatus(), 'Failed to create payment and redirect to HPP', true); + $this->orderRepository->save($order); + } + + /** + * @return ResponseInterface + */ + private function redirectToFailPage(): ResponseInterface + { + $this->paymentErrorMessageManager->addMessage('There was an issue creating your payment. Please try again.'); + return $this->redirect('checkout/cart/index'); + } +} diff --git a/Controller/Checkout/Status.php b/Controller/Checkout/Status.php new file mode 100644 index 0000000..cb1e3aa --- /dev/null +++ b/Controller/Checkout/Status.php @@ -0,0 +1,208 @@ +session = $session; + $this->orderRepository = $orderRepository; + $this->clientFactory = $clientFactory; + $this->configRepository = $configRepository; + $this->transactionRepository = $transactionRepository; + $this->paymentSettledService = $paymentSettledService; + $this->paymentFailedService = $paymentFailedService; + $this->paymentErrorMessageManager = $paymentErrorMessageManager; + $logger->addPrefix('StatusController'); + parent::__construct($context, $jsonFactory, $logger); + } + + /** + * @return ResultInterface + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function executeAction(): ResultInterface + { + $paymentId = $this->context->getRequest()->getParam('payment_id'); + + // Validate payment id + if (!ValidationHelper::isUUID($paymentId)) { + return $this->noPaymentFoundResponse(); + } + + // Check if we have a final status already in the transactions table and redirect + if ($redirect = $this->getFinalPaymentStatusResponse($paymentId)) { + return $redirect; + } + + // Looks like the webhook has not been processed yet + if ($this->shouldCheckTLApi()) { + // Check the payment and update the order + $this->checkPaymentAndUpdateOrder($paymentId); + if ($redirect = $this->getFinalPaymentStatusResponse($paymentId)) { + return $redirect; + } + } + + // No updates available on the payment, show a spinner page and start polling + return $this->pendingResponse(); + } + + /** + * @param string $paymentId + * @return ResultInterface|null + * @throws InputException + */ + private function getFinalPaymentStatusResponse(string $paymentId): ?ResultInterface + { + try { + $transaction = $this->transactionRepository->getByPaymentUuid($paymentId); + } catch (NoSuchEntityException $e) { + return $this->noPaymentFoundResponse(); + } + + if ($transaction->isPaymentSettled()) { + return $this->urlResponse('checkout/onepage/success'); + } + + if ($transaction->isPaymentFailed()) { + $this->session->restoreQuote(); + + $errorText = PaymentFailureReasonHelper::getHumanReadableLabel($transaction->getFailureReason()); + $this->paymentErrorMessageManager->addMessage($errorText . ' ' . __('Please try again.')); + + return $this->urlResponse('checkout/cart'); + } + + return null; + } + + /** + * Check payment and update order and transaction accordingly + * @param string $paymentId + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws Exception + */ + private function checkPaymentAndUpdateOrder(string $paymentId): void + { + $payment = $this->getTruelayerPayment($paymentId); + + if ($payment instanceof PaymentSettledInterface) { + $this->paymentSettledService->handle($paymentId); + } + + if ($payment instanceof PaymentFailedInterface) { + $this->paymentFailedService->handle($paymentId, $payment->getFailureReason()); + } + } + + /** + * @param string $paymentId + * @return PaymentRetrievedInterface|null + */ + private function getTruelayerPayment(string $paymentId): ?PaymentRetrievedInterface + { + try { + $transaction = $this->transactionRepository->getByPaymentUuid($paymentId); + $order = $this->orderRepository->get($transaction->getOrderId()); + $client = $this->clientFactory->create((int)$order->getStoreId()); + return $client->getPayment($paymentId); + } catch (Exception $e) { + $this->logger->error('Could not load TL payment', $e); + } + + return null; + } + + /** + * @return bool + */ + private function shouldCheckTLApi(): bool + { + $attempt = (int) $this->context->getRequest()->getParam('attempt'); + return $attempt > self::CHECK_API_AFTER_ATTEMPTS; + } + + /** + * @return ResultInterface + */ + private function noPaymentFoundResponse(): ResultInterface + { + $this->logger->error('Could not load TL payment'); + $this->context->getMessageManager()->addErrorMessage(__('No payment found')); + return $this->urlResponse('checkout/cart'); + } + + /** + * Render a loading UI + * @return ResultInterface + */ + private function pendingResponse(): ResultInterface + { + return $this->jsonResponse(['pending' => true]); + } + + /** + * @param string $to + * @return ResultInterface + */ + private function urlResponse(string $to): ResultInterface + { + return $this->jsonResponse(['redirect' => $this->configRepository->getBaseUrl() . $to]); + } +} diff --git a/Gateway/Command/AbstractCommand.php b/Gateway/Command/AbstractCommand.php new file mode 100644 index 0000000..732cf53 --- /dev/null +++ b/Gateway/Command/AbstractCommand.php @@ -0,0 +1,71 @@ +orderRepository = $orderRepository; + $this->logger = $logger; + } + + /** + * Adds logging to executeCommand + * @param array $commandSubject + * @return null + * @throws Exception + */ + public function execute(array $commandSubject) + { + $this->logger->debug('Start'); + + try { + $this->executeCommand($commandSubject); + $this->logger->debug('End'); + } catch (Exception $e) { + $this->logger->error('Failed', $e); + throw $e; + } + + return null; + } + + /** + * @param array $subject + * @return OrderInterface + * @throws Exception + */ + protected function getOrder(array $subject): OrderInterface + { + $orderId = SubjectReader::readPayment($subject)->getOrder()->getId(); + return $this->orderRepository->get($orderId); + } + + /** + * @param array $subject + * @return mixed + */ + abstract protected function executeCommand(array $subject): void; +} diff --git a/Gateway/Command/AuthorizePaymentCommand.php b/Gateway/Command/AuthorizePaymentCommand.php new file mode 100644 index 0000000..2150054 --- /dev/null +++ b/Gateway/Command/AuthorizePaymentCommand.php @@ -0,0 +1,43 @@ +addPrefix("AuthorizePaymentCommand")); + } + + /** + * @param array $subject + */ + protected function executeCommand(array $subject): void + { + /** @var Payment $payment */ + $payment = SubjectReader::readPayment($subject)->getPayment(); + + // Set the transaction to pending so that the order is created in a pending state + // The pending state set by magento is not the one we want, so we will overwrite that in OrderPlacedHandler + // This status will also help third party code that may be listening to transactions. + $payment->setIsTransactionPending(true); + + // Do not send emails when the order is placed + // We will instead send emails when the payment is settled + $payment->getOrder()->setCanSendNewEmailFlag(false); + } +} diff --git a/Gateway/Command/RefundPaymentCommand.php b/Gateway/Command/RefundPaymentCommand.php new file mode 100644 index 0000000..e887a28 --- /dev/null +++ b/Gateway/Command/RefundPaymentCommand.php @@ -0,0 +1,55 @@ +refundService = $refundService; + parent::__construct($orderRepository, $logger->addPrefix("RefundPaymentCommand")); + } + + /** + * @param array $subject + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws Exception + */ + protected function executeCommand(array $subject): void + { + /** @var Payment $payment */ + $payment = SubjectReader::readPayment($subject)->getPayment(); + $invoiceIncrementId = $payment->getCreditMemo()->getInvoice()->getIncrementId(); + + $order = $this->getOrder($subject); + $this->logger->addPrefix($order->getEntityId()); + $this->refundService->refund($order, $invoiceIncrementId, (float) SubjectReader::readAmount($subject)); + } +} diff --git a/Gateway/Http/Client/GenericClient.php b/Gateway/Http/Client/GenericClient.php deleted file mode 100644 index 9927412..0000000 --- a/Gateway/Http/Client/GenericClient.php +++ /dev/null @@ -1,23 +0,0 @@ -transferBuilder = $transferBuilder; - } - - /** - * Builds gateway transfer object - * - * @param array $request - * @return TransferInterface - */ - public function create(array $request): TransferInterface - { - return $this->transferBuilder - ->setBody($request) - ->build(); - } -} diff --git a/Gateway/Request/CancelRequest.php b/Gateway/Request/CancelRequest.php deleted file mode 100644 index 197274b..0000000 --- a/Gateway/Request/CancelRequest.php +++ /dev/null @@ -1,40 +0,0 @@ -getPayment(); - - if (!$payment instanceof OrderPaymentInterface) { - throw new \LogicException('Order payment should be provided.'); - } - - return []; - } -} diff --git a/Gateway/Request/RefundRequest.php b/Gateway/Request/RefundRequest.php deleted file mode 100644 index d45a058..0000000 --- a/Gateway/Request/RefundRequest.php +++ /dev/null @@ -1,74 +0,0 @@ -refundOrder = $refundOrder; - $this->orderRepository = $orderRepository; - } - - /** - * Builds ENV request - * - * @param array $buildSubject - * @return array - * @throws AuthenticationException - * @throws LocalizedException - */ - public function build(array $buildSubject): array - { - if (!isset($buildSubject['payment']) - || !$buildSubject['payment'] instanceof PaymentDataObjectInterface - ) { - throw new \InvalidArgumentException('Payment data object should be provided'); - } - - $paymentDO = SubjectReader::readPayment($buildSubject); - $amount = (float)SubjectReader::readAmount($buildSubject); - $payment = $paymentDO->getPayment(); - - if (!$payment instanceof OrderPaymentInterface) { - throw new \LogicException('Order payment should be provided.'); - } - - $order = $this->orderRepository->get($paymentDO->getOrder()->getId()); - $this->refundOrder->execute($order, $amount); - - return []; - } -} diff --git a/Gateway/Validator/CountryValidator.php b/Gateway/Validator/CountryValidator.php index e4cfc32..0885d10 100644 --- a/Gateway/Validator/CountryValidator.php +++ b/Gateway/Validator/CountryValidator.php @@ -17,11 +17,7 @@ */ class CountryValidator extends AbstractValidator { - - /** - * @var ConfigInterface - */ - private $config; + private ConfigInterface $config; /** * @param ResultInterfaceFactory $resultFactory diff --git a/Gateway/Validator/CurrencyValidator.php b/Gateway/Validator/CurrencyValidator.php index 51fff66..96cad02 100644 --- a/Gateway/Validator/CurrencyValidator.php +++ b/Gateway/Validator/CurrencyValidator.php @@ -17,11 +17,7 @@ */ class CurrencyValidator extends AbstractValidator { - - /** - * @var ConfigInterface - */ - private $config; + private ConfigInterface $config; /** * @param ResultInterfaceFactory $resultFactory diff --git a/Helper/AmountHelper.php b/Helper/AmountHelper.php new file mode 100644 index 0000000..b03744c --- /dev/null +++ b/Helper/AmountHelper.php @@ -0,0 +1,16 @@ +json = $json; - $this->configProvider = $configProvider; - parent::__construct($name, $handlers, $processors); - } - - /** - * Add debug data to truelayer Log - * - * @param string $type - * @param mixed $data - */ - public function addLog(string $type, $data): void - { - if (!$this->configProvider->isDebugLoggingEnabled()) { - return; - } - - if (is_array($data) || is_object($data)) { - $this->addRecord(static::INFO, $type . ': ' . $this->json->serialize($data)); - } else { - $this->addRecord(static::INFO, $type . ': ' . $data); - } - } -} diff --git a/Logger/ErrorLogger.php b/Logger/ErrorLogger.php deleted file mode 100644 index 0fa8a8b..0000000 --- a/Logger/ErrorLogger.php +++ /dev/null @@ -1,57 +0,0 @@ -json = $json; - parent::__construct($name, $handlers, $processors); - } - - /** - * Add error data to truelayer Log - * - * @param string $type - * @param mixed $data - * - */ - public function addLog(string $type, $data): void - { - if (is_array($data) || is_object($data)) { - $this->addRecord(static::ERROR, $type . ': ' . $this->json->serialize($data)); - } else { - $this->addRecord(static::ERROR, $type . ': ' . $data); - } - } -} diff --git a/Model/Config/System/BaseRepository.php b/Model/Config/System/BaseRepository.php index 641c509..408272a 100644 --- a/Model/Config/System/BaseRepository.php +++ b/Model/Config/System/BaseRepository.php @@ -17,7 +17,7 @@ use Magento\Store\Model\StoreManagerInterface; /** - * Base Repository provider class + * Base PaymentTransactionRepository provider class */ class BaseRepository { diff --git a/Model/Log/Repository.php b/Model/Log/Repository.php deleted file mode 100644 index bd075f0..0000000 --- a/Model/Log/Repository.php +++ /dev/null @@ -1,58 +0,0 @@ -debugLogger = $debugLogger; - $this->errorLogger = $errorLogger; - } - - /** - * @inheritDoc - */ - public function addErrorLog(string $type, $data): void - { - $this->errorLogger->addLog($type, $data); - } - - /** - * @inheritDoc - */ - public function addDebugLog(string $type, $data): void - { - $this->debugLogger->addLog($type, $data); - } -} diff --git a/Model/Transaction/DataModel.php b/Model/Transaction/Payment/PaymentTransactionDataModel.php similarity index 52% rename from Model/Transaction/DataModel.php rename to Model/Transaction/Payment/PaymentTransactionDataModel.php index a373ffe..e00fc43 100644 --- a/Model/Transaction/DataModel.php +++ b/Model/Transaction/Payment/PaymentTransactionDataModel.php @@ -5,16 +5,17 @@ */ declare(strict_types=1); -namespace TrueLayer\Connect\Model\Transaction; +namespace TrueLayer\Connect\Model\Transaction\Payment; use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Framework\Model\AbstractModel; -use TrueLayer\Connect\Api\Transaction\Data\DataInterface; +use TrueLayer\Connect\Api\Transaction\Payment\PaymentTransactionDataInterface; /** - * Transaction DataModel + * Transaction PaymentTransactionDataModel */ -class DataModel extends AbstractModel implements ExtensibleDataInterface, DataInterface +class PaymentTransactionDataModel extends AbstractModel + implements ExtensibleDataInterface, PaymentTransactionDataInterface { /** @@ -22,7 +23,7 @@ class DataModel extends AbstractModel implements ExtensibleDataInterface, DataIn */ public function _construct() { - $this->_init(ResourceModel::class); + $this->_init(PaymentTransactionResourceModel::class); } /** @@ -46,7 +47,7 @@ public function getQuoteId(): ?int /** * @inheritDoc */ - public function setQuoteId(int $quoteId): DataInterface + public function setQuoteId(int $quoteId): PaymentTransactionDataInterface { return $this->setData(self::QUOTE_ID, $quoteId); } @@ -64,7 +65,7 @@ public function getOrderId(): ?int /** * @inheritDoc */ - public function setOrderId(int $orderId): DataInterface + public function setOrderId(int $orderId): PaymentTransactionDataInterface { return $this->setData(self::ORDER_ID, $orderId); } @@ -82,7 +83,7 @@ public function getToken(): ?string /** * @inheritDoc */ - public function setToken(string $value): DataInterface + public function setToken(string $value): PaymentTransactionDataInterface { return $this->setData(self::TOKEN, $value); } @@ -90,7 +91,7 @@ public function setToken(string $value): DataInterface /** * @inheritDoc */ - public function getUuid(): ?string + public function getPaymentUuid(): ?string { return $this->getData(self::UUID) ? (string)$this->getData(self::UUID) @@ -100,7 +101,7 @@ public function getUuid(): ?string /** * @inheritDoc */ - public function setUuid(string $value): DataInterface + public function setPaymentUuid(string $value): PaymentTransactionDataInterface { return $this->setData(self::UUID, $value); } @@ -118,7 +119,7 @@ public function getStatus(): ?string /** * @inheritDoc */ - public function setStatus(string $status): DataInterface + public function setStatus(string $status): PaymentTransactionDataInterface { return $this->setData(self::STATUS, $status); } @@ -126,54 +127,66 @@ public function setStatus(string $status): DataInterface /** * @inheritDoc */ - public function getInvoiceUuid(): ?string + public function getFailureReason(): ?string { - return $this->getData(self::INVOICE_UUID) - ? (string)$this->getData(self::INVOICE_UUID) + return $this->getData(self::FAILURE_REASON) + ? (string)$this->getData(self::FAILURE_REASON) : null; } /** * @inheritDoc */ - public function setInvoiceUuid(string $invoiceUuid): DataInterface + public function setFailureReason(string $failureReason): PaymentTransactionDataInterface { - return $this->setData(self::INVOICE_UUID, $invoiceUuid); + return $this->setData(self::FAILURE_REASON, $failureReason); } /** * @inheritDoc */ - public function getPaymentUrl(): ?string + public function getIsLocked(): bool { - return $this->getData(self::PAYMENT_URL) - ? (string)$this->getData(self::PAYMENT_URL) - : null; + return (bool) $this->getData(self::IS_LOCKED); } /** * @inheritDoc */ - public function setPaymentUrl(string $paymentUrl): DataInterface + public function setIsLocked(bool $isLocked): PaymentTransactionDataInterface { - return $this->setData(self::PAYMENT_URL, $paymentUrl); + return $this->setData(self::IS_LOCKED, $isLocked ? 1 : 0); } /** * @inheritDoc */ - public function getIsLocked(): ?int + public function setPaymentFailed(): PaymentTransactionDataInterface { - return $this->getData(self::IS_LOCKED) - ? (int)$this->getData(self::IS_LOCKED) - : null; + return $this->setStatus(PaymentTransactionDataInterface::PAYMENT_FAILED); + } + + /** + * @inheritDoc + */ + public function isPaymentFailed(): bool + { + return $this->getStatus() === PaymentTransactionDataInterface::PAYMENT_FAILED; + } + + /** + * @inheritDoc + */ + public function setPaymentSettled(): PaymentTransactionDataInterface + { + return $this->setStatus(PaymentTransactionDataInterface::PAYMENT_SETTLED); } /** * @inheritDoc */ - public function setIsLocked(int $isLocked): DataInterface + public function isPaymentSettled(): bool { - return $this->setData(self::IS_LOCKED, $isLocked); + return $this->getStatus() === PaymentTransactionDataInterface::PAYMENT_SETTLED; } } diff --git a/Model/Transaction/Payment/PaymentTransactionRepository.php b/Model/Transaction/Payment/PaymentTransactionRepository.php new file mode 100644 index 0000000..1314c1c --- /dev/null +++ b/Model/Transaction/Payment/PaymentTransactionRepository.php @@ -0,0 +1,119 @@ +resource = $resource; + $this->dataFactory = $dataFactory; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function create(): PaymentTransactionDataInterface + { + return $this->dataFactory->create(); + } + + /** + * @inheritDoc + */ + public function get(int $entityId): PaymentTransactionDataInterface + { + if (!$entityId) { + $errorMsg = static::INPUT_EXCEPTION; + throw new InputException(__($errorMsg, 'EntityId')); + } elseif (!$this->resource->isExists($entityId)) { + $exceptionMsg = self::NO_SUCH_ENTITY_EXCEPTION; + throw new NoSuchEntityException(__($exceptionMsg, $entityId)); + } + return $this->dataFactory->create() + ->load($entityId); + } + + /** + * @inheritDoc + */ + public function getByOrderId(int $orderId): PaymentTransactionDataInterface + { + if (!$orderId) { + $errorMsg = static::INPUT_EXCEPTION; + throw new InputException(__($errorMsg, 'OrderID')); + } elseif (!$this->resource->isOrderIdExists($orderId)) { + throw new NoSuchEntityException(__('No record found for OrderID: %1.', $orderId)); + } + return $this->dataFactory->create() + ->load($orderId, 'order_id'); + } + + /** + * @inheritDoc + */ + public function getByPaymentUuid(string $uuid): PaymentTransactionDataInterface + { + if (!$uuid) { + $errorMsg = static::INPUT_EXCEPTION; + throw new InputException(__($errorMsg, 'Uuid')); + } elseif (!$this->resource->isUuidExists($uuid)) { + throw new NoSuchEntityException(__('No record found for uuid: %1.', $uuid)); + } + + return $this->dataFactory->create() + ->load($uuid, 'uuid'); + } + + /** + * @inheritDoc + */ + public function save(PaymentTransactionDataInterface $entity): PaymentTransactionDataInterface + { + try { + $this->resource->save($entity); + } catch (\Exception $exception) { + $this->logger->error('Quote repository', $exception->getMessage()); + $exceptionMsg = self::COULD_NOT_SAVE_EXCEPTION; + throw new CouldNotSaveException(__( + $exceptionMsg, + $exception->getMessage() + )); + } + return $entity; + } +} diff --git a/Model/Transaction/Payment/PaymentTransactionResourceModel.php b/Model/Transaction/Payment/PaymentTransactionResourceModel.php new file mode 100644 index 0000000..9a31d3e --- /dev/null +++ b/Model/Transaction/Payment/PaymentTransactionResourceModel.php @@ -0,0 +1,71 @@ +_init('truelayer_transaction', 'entity_id'); + } + + /** + * Check is entity exists + * + * @param int $entityId + * @return bool + */ + public function isExists(int $entityId): bool + { + $connection = $this->getConnection(); + $select = $connection->select() + ->from($this->getTable('truelayer_transaction'), 'entity_id') + ->where('entity_id = :entity_id'); + $bind = [':entity_id' => $entityId]; + return (bool)$connection->fetchOne($select, $bind); + } + + /** + * Check is entity exists + * + * @param int $orderId + * @return bool + */ + public function isOrderIdExists(int $orderId): bool + { + $connection = $this->getConnection(); + $select = $connection->select() + ->from($this->getTable('truelayer_transaction'), 'order_id') + ->where('order_id = :order_id'); + $bind = [':order_id' => $orderId]; + return (bool)$connection->fetchOne($select, $bind); + } + + /** + * Check is entity exists + * + * @param string $uuid + * @return bool + */ + public function isUuidExists(string $uuid): bool + { + $connection = $this->getConnection(); + $select = $connection->select() + ->from($this->getTable('truelayer_transaction'), 'uuid') + ->where('uuid = :uuid'); + $bind = [':uuid' => $uuid]; + return (bool)$connection->fetchOne($select, $bind); + } +} diff --git a/Model/Transaction/Collection.php b/Model/Transaction/Refund/RefundCollection.php similarity index 56% rename from Model/Transaction/Collection.php rename to Model/Transaction/Refund/RefundCollection.php index 85bfe26..9a0f0d9 100644 --- a/Model/Transaction/Collection.php +++ b/Model/Transaction/Refund/RefundCollection.php @@ -5,17 +5,12 @@ */ declare(strict_types=1); -namespace TrueLayer\Connect\Model\Transaction; +namespace TrueLayer\Connect\Model\Transaction\Refund; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; -use TrueLayer\Connect\Model\Transaction\DataModel as Model; -/** - * Transaction Resource Collection - */ -class Collection extends AbstractCollection +class RefundCollection extends AbstractCollection { - /** * Define resource model * @@ -23,6 +18,6 @@ class Collection extends AbstractCollection */ protected function _construct() { - $this->_init(Model::class, ResourceModel::class); + $this->_init(RefundTransactionDataModel::class, RefundTransactionResourceModel::class); } } diff --git a/Model/Transaction/Refund/RefundTransactionDataModel.php b/Model/Transaction/Refund/RefundTransactionDataModel.php new file mode 100644 index 0000000..3532d73 --- /dev/null +++ b/Model/Transaction/Refund/RefundTransactionDataModel.php @@ -0,0 +1,176 @@ +_init(RefundTransactionResourceModel::class); + } + + /** + * @inheritDoc + */ + public function getEntityId(): int + { + return (int) $this->getData(self::ENTITY_ID); + } + + /** + * @inheritDoc + */ + public function getRefundUuid(): ?string + { + return $this->getData(self::REFUND_UUID); + } + + /** + * @inheritDoc + */ + public function setRefundUuid(string $value): RefundTransactionDataInterface + { + return $this->setData(self::REFUND_UUID, $value); + } + + /** + * @inheritDoc + */ + public function getAmount(): ?int + { + return (int) $this->getData(self::AMOUNT); + } + + /** + * @inheritDoc + */ + public function setAmount(int $value): RefundTransactionDataInterface + { + return $this->setData(self::AMOUNT, $value); + } + + /** + * @inheritDoc + */ + public function getOrderId(): ?int + { + return (int) $this->getData(self::ORDER_ID); + } + + /** + * @inheritDoc + */ + public function setOrderId(int $orderId): RefundTransactionDataInterface + { + return $this->setData(self::ORDER_ID, $orderId); + } + + /** + * @inheritDoc + */ + public function getPaymentUuid(): ?string + { + return $this->getData(self::PAYMENT_UUID); + } + + /** + * @inheritDoc + */ + public function setPaymentUuid(string $value): RefundTransactionDataInterface + { + return $this->setData(self::PAYMENT_UUID, $value); + } + + /** + * @inheritDoc + */ + public function getStatus(): ?string + { + return $this->getData(self::STATUS); + } + + /** + * @inheritDoc + */ + public function setStatus(string $status): RefundTransactionDataInterface + { + return $this->setData(self::STATUS, $status); + } + + /** + * @inheritDoc + */ + public function getFailureReason(): ?string + { + return $this->getData(self::FAILURE_REASON); + } + + /** + * @inheritDoc + */ + public function setFailureReason(string $failureReason): RefundTransactionDataInterface + { + return $this->setData(self::FAILURE_REASON, $failureReason); + } + + /** + * @inheritDoc + */ + public function getIsLocked(): bool + { + return (bool) $this->getData(self::IS_LOCKED); + } + + /** + * @inheritDoc + */ + public function setIsLocked(bool $isLocked): RefundTransactionDataInterface + { + return $this->setData(self::IS_LOCKED, $isLocked ? 1 : 0); + } + + /** + * @inheritDoc + */ + public function getCreditMemoId(): ?int + { + return (int) $this->getData(self::CREDITMEMO_ID); + } + + /** + * @inheritDoc + */ + public function setCreditMemoId(int $creditMemoId): RefundTransactionDataInterface + { + return $this->setData(self::CREDITMEMO_ID, $creditMemoId); + } + + /** + * @inheritDoc + */ + public function setRefundFailed(): RefundTransactionDataInterface + { + return $this->setStatus(RefundTransactionDataInterface::REFUND_FAILED); + } + + /** + * @inheritDoc + */ + public function isRefundFailed(): bool + { + return $this->getStatus() === RefundTransactionDataInterface::REFUND_FAILED; + } +} diff --git a/Model/Transaction/Refund/RefundTransactionRepository.php b/Model/Transaction/Refund/RefundTransactionRepository.php new file mode 100644 index 0000000..32b6623 --- /dev/null +++ b/Model/Transaction/Refund/RefundTransactionRepository.php @@ -0,0 +1,154 @@ +resource = $resource; + $this->dataFactory = $dataFactory; + $this->collectionFactory = $collectionFactory; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function create(): RefundTransactionDataInterface + { + return $this->dataFactory->create(); + } + + /** + * @inheritDoc + */ + public function get(int $entityId): RefundTransactionDataInterface + { + return $this->getByColumn(RefundTransactionDataInterface::ENTITY_ID, $entityId); + } + + /** + * @inheritDoc + */ + public function getByOrderId(int $orderId): RefundTransactionDataInterface + { + return $this->getByColumn(RefundTransactionDataInterface::ORDER_ID, $orderId); + } + + /** + * @inheritDoc + */ + public function getByPaymentUuid(string $uuid): RefundTransactionDataInterface + { + return $this->getByColumn(RefundTransactionDataInterface::PAYMENT_UUID, $uuid); + } + + /** + * @inheritDoc + */ + public function getByRefundUuid(string $uuid): RefundTransactionDataInterface + { + return $this->getByColumn(RefundTransactionDataInterface::REFUND_UUID, $uuid); + } + + /** + * @inheritDoc + */ + public function getByCreditMemoId(int $id): RefundTransactionDataInterface + { + return $this->getByColumn(RefundTransactionDataInterface::CREDITMEMO_ID, $id); + } + + /** + * @inheritDoc + */ + public function save(RefundTransactionDataInterface $entity): RefundTransactionDataInterface + { + try { + $this->resource->save($entity); + return $entity; + } catch (\Exception $exception) { + $this->logger->error('Could not save refund transaction', $exception); + $msg = self::COULD_NOT_SAVE_EXCEPTION; + throw new CouldNotSaveException(__($msg, $exception->getMessage())); + } + } + + /** + * @param string $col + * @param $value + * @return RefundTransactionDataInterface + * @throws NoSuchEntityException + */ + private function getByColumn(string $col, $value): RefundTransactionDataInterface + { + /** @var RefundTransactionDataInterface $transaction */ + $transaction = $this->dataFactory->create()->load($value, $col); + + if (!$transaction->getEntityId()) { + $this->logger->error('Refund transaction not found', $value); + throw new NoSuchEntityException(__('No record found for %1: %2.', $col, $value)); + } + + return $transaction; + } + + /** + * @param array $cols + * @param array $sort + * @return RefundTransactionDataInterface + */ + public function getOneByColumns(array $cols, array $sort = []): RefundTransactionDataInterface + { + $collection = $this->collectionFactory->create(); + + foreach ($cols as $col => $value) { + $collection->addFieldToFilter($col, $value); + } + + foreach ($sort as $col => $dir) { + $collection->setOrder($col, $dir); + } + + return $collection->getFirstItem(); + } +} diff --git a/Model/Transaction/Refund/RefundTransactionResourceModel.php b/Model/Transaction/Refund/RefundTransactionResourceModel.php new file mode 100644 index 0000000..4a7dcc3 --- /dev/null +++ b/Model/Transaction/Refund/RefundTransactionResourceModel.php @@ -0,0 +1,23 @@ +_init('truelayer_refund_transaction', 'entity_id'); + } +} diff --git a/Model/Transaction/Repository.php b/Model/Transaction/Repository.php deleted file mode 100644 index 580d20c..0000000 --- a/Model/Transaction/Repository.php +++ /dev/null @@ -1,279 +0,0 @@ -searchResultFactory = $searchResultFactory; - $this->collectionFactory = $collectionFactory; - $this->resource = $resource; - $this->dataFactory = $dataFactory; - $this->logger = $logger; - } - - /** - * @inheritDoc - */ - public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface - { - /* @var Collection $collection */ - $collection = $this->collectionFactory->create(); - return $this->searchResultFactory->create() - ->setSearchCriteria($searchCriteria) - ->setItems($collection->getItems()) - ->setTotalCount($collection->getSize()); - } - - /** - * @inheritDoc - */ - public function create(): DataInterface - { - return $this->dataFactory->create(); - } - - /** - * @inheritDoc - */ - public function deleteById(int $entityId): bool - { - $entity = $this->get((int)$entityId); - return $this->delete($entity); - } - - /** - * @inheritDoc - */ - public function get(int $entityId): DataInterface - { - if (!$entityId) { - $errorMsg = static::INPUT_EXCEPTION; - throw new InputException(__($errorMsg, 'EntityId')); - } elseif (!$this->resource->isExists($entityId)) { - $exceptionMsg = self::NO_SUCH_ENTITY_EXCEPTION; - throw new NoSuchEntityException(__($exceptionMsg, $entityId)); - } - return $this->dataFactory->create() - ->load($entityId); - } - - /** - * @inheritDoc - */ - public function delete(DataInterface $entity): bool - { - try { - $this->resource->delete($entity); - } catch (\Exception $exception) { - $this->logger->addErrorLog('Quote repository', $exception->getMessage()); - $exceptionMsg = self::COULD_NOT_DELETE_EXCEPTION; - throw new CouldNotDeleteException(__( - $exceptionMsg, - $exception->getMessage() - )); - } - return true; - } - - /** - * @inheritDoc - */ - public function getByQuoteId(int $quoteId, bool $uuidCheck = false): DataInterface - { - if (!$quoteId) { - $errorMsg = static::INPUT_EXCEPTION; - throw new InputException(__($errorMsg, 'QuoteId')); - } elseif (!$this->resource->isQuoteIdExists($quoteId)) { - throw new NoSuchEntityException(__('No record found for QuoteId: %1.', $quoteId)); - } - - if (!$uuidCheck) { - return $this->dataFactory->create()->load($quoteId, 'quote_id'); - } - $transaction = $this->getByDataSet( - [ - 'quote_id' => $quoteId, - 'uuid' => ['empty' => true] - ], - true - ); - if ($transaction->getData() == null) { - throw new NoSuchEntityException(__('No record found for QuoteId: %1. with empty uuid', $quoteId)); - } - return $transaction; - } - - /** - * @inheritDoc - */ - public function getByDataSet(array $dataSet, bool $getFirst = false) - { - $collection = $this->collectionFactory->create(); - foreach ($dataSet as $attribute => $value) { - if (is_array($value)) { - $collection->addFieldToFilter($attribute, ['in' => $value]); - } else { - $collection->addFieldToFilter($attribute, $value); - } - } - if ($getFirst) { - return $collection->getFirstItem(); - } else { - return $collection; - } - } - - /** - * @inheritDoc - */ - public function getByOrderId(int $orderId): DataInterface - { - if (!$orderId) { - $errorMsg = static::INPUT_EXCEPTION; - throw new InputException(__($errorMsg, 'OrderID')); - } elseif (!$this->resource->isOrderIdExists($orderId)) { - throw new NoSuchEntityException(__('No record found for OrderID: %1.', $orderId)); - } - return $this->dataFactory->create() - ->load($orderId, 'order_id'); - } - - /** - * @inheritDoc - */ - public function getByUuid(string $uuid): DataInterface - { - if (!$uuid) { - $errorMsg = static::INPUT_EXCEPTION; - throw new InputException(__($errorMsg, 'Uuid')); - } elseif (!$this->resource->isUuidExists($uuid)) { - throw new NoSuchEntityException(__('No record found for uuid: %1.', $uuid)); - } - return $this->dataFactory->create() - ->load($uuid, 'uuid'); - } - - /** - * @inheritDoc - */ - public function getByToken(string $token): DataInterface - { - if (!$token) { - $errorMsg = static::INPUT_EXCEPTION; - throw new InputException(__($errorMsg, 'Token')); - } elseif (!$this->resource->isTokenExist($token)) { - throw new NoSuchEntityException(__('No record found for token: %1.', $token)); - } - return $this->dataFactory->create() - ->load($token, 'token'); - } - - /** - * @inheritDoc - */ - public function lock(DataInterface $entity): bool - { - return $this->resource->lockTransaction($entity); - } - - /** - * @inheritDoc - */ - public function unlock(DataInterface $entity): DataInterface - { - $entity->setIsLocked(0); - return $this->save($entity); - } - - /** - * @inheritDoc - */ - public function save(DataInterface $entity): DataInterface - { - try { - $this->resource->save($entity); - } catch (\Exception $exception) { - $this->logger->addErrorLog('Quote repository', $exception->getMessage()); - $exceptionMsg = self::COULD_NOT_SAVE_EXCEPTION; - throw new CouldNotSaveException(__( - $exceptionMsg, - $exception->getMessage() - )); - } - return $entity; - } - - /** - * @inheritDoc - */ - public function isLocked(DataInterface $entity): bool - { - return $this->resource->isLocked($entity); - } - - /** - * @inheritDoc - */ - public function checkOrderIsPlaced(DataInterface $entity): bool - { - return $this->resource->isOrderPlaced($entity); - } -} diff --git a/Model/Transaction/ResourceModel.php b/Model/Transaction/ResourceModel.php deleted file mode 100644 index 41690d4..0000000 --- a/Model/Transaction/ResourceModel.php +++ /dev/null @@ -1,146 +0,0 @@ -getConnection(); - $select = $connection->select() - ->from($this->getTable('truelayer_transaction'), 'entity_id') - ->where('entity_id = :entity_id'); - $bind = [':entity_id' => $entityId]; - return (bool)$connection->fetchOne($select, $bind); - } - - /** - * Check is entity exists - * - * @param int $quoteId - * @return bool - */ - public function isQuoteIdExists(int $quoteId): bool - { - $connection = $this->getConnection(); - $select = $connection->select() - ->from($this->getTable('truelayer_transaction'), 'quote_id') - ->where('quote_id = :quote_id'); - $bind = [':quote_id' => $quoteId]; - return (bool)$connection->fetchOne($select, $bind); - } - - /** - * Check is entity exists - * - * @param int $orderId - * @return bool - */ - public function isOrderIdExists(int $orderId): bool - { - $connection = $this->getConnection(); - $select = $connection->select() - ->from($this->getTable('truelayer_transaction'), 'order_id') - ->where('order_id = :order_id'); - $bind = [':order_id' => $orderId]; - return (bool)$connection->fetchOne($select, $bind); - } - - /** - * Check is entity exists - * - * @param string $uuid - * @return bool - */ - public function isUuidExists(string $uuid): bool - { - $connection = $this->getConnection(); - $select = $connection->select() - ->from($this->getTable('truelayer_transaction'), 'uuid') - ->where('uuid = :uuid'); - $bind = [':uuid' => $uuid]; - return (bool)$connection->fetchOne($select, $bind); - } - - /** - * Check is entity exists - * - * @param string $token - * @return bool - */ - public function isTokenExist(string $token): bool - { - $connection = $this->getConnection(); - $select = $connection->select() - ->from($this->getTable('truelayer_transaction'), 'token') - ->where('token = :token'); - $bind = [':token' => $token]; - return (bool)$connection->fetchOne($select, $bind); - } - - /** - * @param $transaction - * @return bool - */ - public function lockTransaction($transaction) - { - $connection = $this->getConnection(); - return (bool)$connection->update( - $this->getTable('truelayer_transaction'), - ['is_locked' => 1], - $connection->quoteInto('entity_id = ?', $transaction->getEntityId()) - ); - } - - /** - * @param $transaction - * @return bool - */ - public function isLocked($transaction): bool - { - $connection = $this->getConnection(); - $select = $connection->select() - ->from($this->getTable('truelayer_transaction'), 'is_locked') - ->where('entity_id = :entity_id'); - $bind = [':entity_id' => $transaction->getEntityId()]; - return (bool)$connection->fetchOne($select, $bind); - } - - /** - * @param $transaction - * @return bool - */ - public function isOrderPlaced($transaction): bool - { - $connection = $this->getConnection(); - $select = $connection->select() - ->from($this->getTable('truelayer_transaction'), 'order_id') - ->where('entity_id = :entity_id'); - $bind = [':entity_id' => $transaction->getEntityId()]; - return (bool)$connection->fetchOne($select, $bind); - } - - /** - * Resource model - * - * @return void - */ - protected function _construct() - { - $this->_init('truelayer_transaction', 'entity_id'); - } -} diff --git a/Model/User/Repository.php b/Model/User/Repository.php index 89cd2cc..3ba0a10 100644 --- a/Model/User/Repository.php +++ b/Model/User/Repository.php @@ -10,7 +10,7 @@ use TrueLayer\Connect\Api\User\RepositoryInterface; /** - * User Repository class + * User PaymentTransactionRepository class */ class Repository implements RepositoryInterface { @@ -20,7 +20,7 @@ class Repository implements RepositoryInterface private $resource; /** - * Repository constructor. + * PaymentTransactionRepository constructor. * * @param ResourceModel $resource */ diff --git a/Model/Webapi/Checkout.php b/Model/Webapi/Checkout.php deleted file mode 100644 index c29473c..0000000 --- a/Model/Webapi/Checkout.php +++ /dev/null @@ -1,102 +0,0 @@ -orderRequest = $orderRequest; - $this->logRepository = $logRepository; - $this->checkoutSession = $checkoutSession; - $this->quoteIdMaskFactory = $quoteIdMaskFactory; - $this->generateTokenService = $generateToken; - } - - /** - * @inheritDoc - */ - public function orderRequest(bool $isLoggedIn, string $cartId) - { - $token = $this->getToken($isLoggedIn, $cartId); - //web api can't return first level associative array - $return = []; - try { - $paymentUrl = $this->orderRequest->execute($token); - $return['response'] = ['success' => true, 'payment_page_url' => $paymentUrl]; - return $return; - } catch (\Exception $exception) { - $this->logRepository->addErrorLog('Checkout endpoint', $exception->getMessage()); - $return['response'] = ['success' => false, 'message' => $exception->getMessage()]; - return $return; - } - } - - /** - * @param bool $isLoggedIn - * @param string $cartId - * @return string|null - */ - private function getToken(bool $isLoggedIn, string $cartId): ?string - { - try { - if ($isLoggedIn) { - $quote = $this->checkoutSession->getQuote(); - return $this->generateTokenService->execute((int)$quote->getId()); - } else { - $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); - return $this->generateTokenService->execute((int)$quoteIdMask->getQuoteId()); - } - } catch (\Exception $e) { - return ''; - } - } -} diff --git a/Model/Webapi/Pending.php b/Model/Webapi/Pending.php deleted file mode 100644 index 9dc0f0e..0000000 --- a/Model/Webapi/Pending.php +++ /dev/null @@ -1,45 +0,0 @@ -transactionRepository = $transactionRepository; - } - - /** - * @inheritDoc - */ - public function checkOrderPlaced(string $token): bool - { - try { - $transaction = $this->transactionRepository->getByUuid($token); - return (bool)$transaction->getOrderId(); - } catch (InputException|NoSuchEntityException $e) { - return false; - } - } -} diff --git a/Model/Webapi/Webhook.php b/Model/Webapi/Webhook.php index 89e6a67..b9b4be1 100644 --- a/Model/Webapi/Webhook.php +++ b/Model/Webapi/Webhook.php @@ -7,17 +7,29 @@ namespace TrueLayer\Connect\Model\Webapi; -use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem\Driver\File; use Magento\Framework\Serialize\Serializer\Json as JsonSerializer; use Magento\Quote\Api\CartRepositoryInterface; +use ReflectionException; use TrueLayer\Connect\Api\Config\RepositoryInterface as ConfigRepository; -use TrueLayer\Connect\Api\Log\RepositoryInterface as LogRepository; -use TrueLayer\Connect\Api\Transaction\RepositoryInterface as TransactionRepository; +use TrueLayer\Connect\Api\Log\LogServiceInterface as LogRepository; +use TrueLayer\Connect\Api\Transaction\Payment\PaymentTransactionRepositoryInterface as TransactionRepository; use TrueLayer\Connect\Api\Webapi\WebhookInterface; -use TrueLayer\Connect\Service\Order\ProcessWebhook; +use TrueLayer\Connect\Helper\ValidationHelper; +use TrueLayer\Connect\Service\Order\PaymentUpdate\PaymentFailedService; +use TrueLayer\Connect\Service\Order\PaymentUpdate\PaymentSettledService; +use TrueLayer\Connect\Service\Order\RefundUpdate\RefundFailedService; use TrueLayer\Exceptions\Exception; +use TrueLayer\Exceptions\InvalidArgumentException; +use TrueLayer\Exceptions\SignerException; +use TrueLayer\Exceptions\WebhookHandlerException; +use TrueLayer\Exceptions\WebhookHandlerInvalidArgumentException; +use TrueLayer\Exceptions\WebhookVerificationFailedException; use TrueLayer\Interfaces\Webhook as TrueLayerWebhookInterface; +use TrueLayer\Settings; use TrueLayer\Webhook as TrueLayerWebhook; /** @@ -25,89 +37,85 @@ */ class Webhook implements WebhookInterface { + private PaymentSettledService $paymentSettledService; + private PaymentFailedService $paymentFailedService; + private RefundFailedService $refundFailedService; + private ConfigRepository $configProvider; + private JsonSerializer $jsonSerializer; + private File $file; + private TransactionRepository $transactionRepository; + private CartRepositoryInterface $quoteRepository; + private LogRepository $logger; /** - * @var LogRepository - */ - private $logRepository; - /** - * @var ProcessWebhook - */ - private $processWebhook; - /** - * @var ConfigRepository - */ - private $configProvider; - /** - * @var JsonSerializer - */ - private $jsonSerializer; - /** - * @var File - */ - private $file; - /** - * @var TransactionRepository - */ - private $transactionRepository; - /** - * @var CartRepositoryInterface - */ - private $quoteRepository; - - /** - * Webhook constructor. - * - * @param LogRepository $logRepository - * @param ProcessWebhook $processWebhook + * @param PaymentSettledService $paymentSettledService + * @param PaymentFailedService $paymentFailedService + * @param RefundFailedService $refundFailedService * @param ConfigRepository $configProvider * @param JsonSerializer $jsonSerializer * @param File $file * @param TransactionRepository $transactionRepository * @param CartRepositoryInterface $quoteRepository + * @param LogRepository $logger */ public function __construct( - LogRepository $logRepository, - ProcessWebhook $processWebhook, - ConfigRepository $configProvider, - JsonSerializer $jsonSerializer, - File $file, - TransactionRepository $transactionRepository, - CartRepositoryInterface $quoteRepository + PaymentSettledService $paymentSettledService, + PaymentFailedService $paymentFailedService, + RefundFailedService $refundFailedService, + ConfigRepository $configProvider, + JsonSerializer $jsonSerializer, + File $file, + TransactionRepository $transactionRepository, + CartRepositoryInterface $quoteRepository, + LogRepository $logger ) { - $this->logRepository = $logRepository; - $this->processWebhook = $processWebhook; + $this->paymentSettledService = $paymentSettledService; + $this->paymentFailedService = $paymentFailedService; + $this->refundFailedService = $refundFailedService; $this->configProvider = $configProvider; $this->jsonSerializer = $jsonSerializer; $this->file = $file; $this->transactionRepository = $transactionRepository; $this->quoteRepository = $quoteRepository; + $this->logger = $logger->addPrefix('Webhook'); } /** - * @inheritDoc + * @throws AuthorizationException + * @throws Exception + * @throws InvalidArgumentException + * @throws ReflectionException + * @throws SignerException + * @throws WebhookHandlerException + * @throws WebhookHandlerInvalidArgumentException */ public function processTransfer() { - \TrueLayer\Settings::tlAgent('truelayer-magento/' . $this->configProvider->getExtensionVersion()); + Settings::tlAgent('truelayer-magento/' . $this->configProvider->getExtensionVersion()); + $webhook = TrueLayerWebhook::configure() ->useProduction(!$this->configProvider->isSandbox($this->getStoreId())) - ->create(); + ->create() + ->handler(function (TrueLayerWebhookInterface\EventInterface $event) { + $this->logger->debug('Body', $event->getBody()); + }) + ->handler(function (TrueLayerWebhookInterface\PaymentSettledEventInterface $event) { + $this->paymentSettledService->handle($event->getPaymentId()); + }) + ->handler(function (TrueLayerWebhookInterface\PaymentFailedEventInterface $event) { + $this->paymentFailedService->handle($event->getPaymentId(), $event->getFailureReason()); + }) + ->handler(function (TrueLayerWebhookInterface\RefundFailedEventInterface $event) { + $this->refundFailedService->handle($event->getRefundId(), $event->getFailureReason()); + }); - $webhook->handler(function (TrueLayerWebhookInterface\EventInterface $event) { - $this->logRepository->addDebugLog('Webhook', $event->getBody()); - })->handler(function (TrueLayerWebhookInterface\PaymentSettledEventInterface $event) { - try { - $this->processWebhook->execute($event->getBody()['payment_id'], $event->getBody()['user_id']); - } catch (\Exception $exception) { - $this->logRepository->addErrorLog('Webhook processTransfer', $exception->getMessage()); - throw new LocalizedException(__($exception->getMessage())); - } - }); try { $webhook->execute(); - } catch (Exception $e) { - $this->logRepository->addErrorLog('Webhook', $e->getMessage()); + } catch (WebhookVerificationFailedException $e) { + throw new AuthorizationException(__('Invalid signature')); // 401 + } catch (NoSuchEntityException $e) { + // We intentionally do not surface a 404 status code + $this->logger->error('Aborting webhook, payment or refund not found'); } } @@ -117,13 +125,12 @@ public function processTransfer() private function getStoreId(): int { try { - $post = $this->file->fileGetContents('php://input'); - $postArray = $this->jsonSerializer->unserialize($post); - if (!isset($postArray['payment_id']) || !$this->isValidUuid((string)$postArray['payment_id'])) { + $paymentId = $this->getPaymentId(); + if (!$paymentId) { return 0; } - $transaction = $this->transactionRepository->getByUuid($postArray['payment_id']); + $transaction = $this->transactionRepository->getByPaymentUuid($paymentId); if (!$quoteId = $transaction->getQuoteId()) { return 0; } @@ -131,20 +138,24 @@ private function getStoreId(): int $quote = $this->quoteRepository->get($quoteId); return $quote->getStoreId(); } catch (\Exception $exception) { - $this->logRepository->addErrorLog('Webhook processTransfer postData', $exception->getMessage()); + $this->logger->error('Unable to get store id', $exception); return 0; } } /** - * Check if string is valid Uuid - * - * @param string $paymentId - * @return bool + * @return string|null + * @throws FileSystemException */ - private function isValidUuid(string $paymentId): bool + private function getPaymentId(): ?string { - $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i'; - return preg_match($pattern, $paymentId) === 1; + $post = $this->file->fileGetContents('php://input'); + $postArray = $this->jsonSerializer->unserialize($post); + + if (!isset($postArray['payment_id']) || !ValidationHelper::isUUID((string) $postArray['payment_id'])) { + return null; + } + + return $postArray['payment_id']; } } diff --git a/Observer/CreditMemoObserver.php b/Observer/CreditMemoObserver.php new file mode 100644 index 0000000..730dfdb --- /dev/null +++ b/Observer/CreditMemoObserver.php @@ -0,0 +1,94 @@ +refundTransactionRepository = $refundTransactionRepository; + $this->logger = $logger->addPrefix('CreditMemoObserver'); + } + + /** + * @param Observer $observer + * @throws LocalizedException + * @throws Exception + */ + public function execute(Observer $observer) + { + /** @var Order\Creditmemo $creditMemo */ + $creditMemo = $observer->getData('creditmemo'); + $order = $creditMemo->getOrder(); + + if ($order->getPayment()->getMethod() !== 'truelayer') { + return; + } + + $this->logger->debug('Get transaction'); + + try { + // Find matching transaction with missing creditmemo id + $refundTransaction = $this->refundTransactionRepository->getOneByColumns([ + RefundTransactionDataInterface::AMOUNT => AmountHelper::toMinor($creditMemo->getGrandTotal()), + RefundTransactionDataInterface::ORDER_ID => $order->getEntityId(), + RefundTransactionDataInterface::CREDITMEMO_ID => ['null' => true], + ], [RefundTransactionDataInterface::ENTITY_ID => 'DESC']); + } catch (Exception $e) { + $this->logger->error('Failed loading transaction', $e); + throw $e; + } + + if (!$refundTransaction || !$refundTransaction->getEntityId()) { + if ($this->findByCreditMemo($creditMemo)) { + return; // We already have a credit memo id, we can abort. + } + $this->logger->error('Transaction not found'); + throw new LocalizedException( + __('Something has gone wrong. Please check the refund status in your TrueLayer Console account.') + ); + } + + $refundTransaction->setCreditMemoId((int) $creditMemo->getEntityId()); + $this->refundTransactionRepository->save($refundTransaction); + $this->logger->debug('Transaction updated'); + } + + /** + * @param Order\Creditmemo $creditMemo + * @return RefundTransactionDataInterface|null + */ + private function findByCreditMemo(Order\Creditmemo $creditMemo): ?RefundTransactionDataInterface + { + try { + $creditMemoId = (int) $creditMemo->getEntityId(); + return $this->refundTransactionRepository->getByCreditMemoId($creditMemoId); + } catch (NoSuchEntityException $e) { + $this->logger->debug('No transaction found with creditmemo id', $e); + return null; + } + } +} diff --git a/Observer/OrderPlacedObserver.php b/Observer/OrderPlacedObserver.php new file mode 100644 index 0000000..40fe40f --- /dev/null +++ b/Observer/OrderPlacedObserver.php @@ -0,0 +1,54 @@ +orderRepository = $orderRepository; + $this->logger = $logger->addPrefix('OrderPlacedObserver'); + } + + /** + * @param Observer $observer + */ + public function execute(Observer $observer) + { + $order = $observer->getEvent()->getOrder(); + + if ($order->getPayment()->getMethod() !== 'truelayer') { + return; + } + + $this->logger->debug('Start'); + + // Set order status to pending payment + $order + ->setState(Order::STATE_PENDING_PAYMENT) + ->setStatus(Order::STATE_PENDING_PAYMENT); + + $this->orderRepository->save($order); + $this->logger->debug('End'); + } +} diff --git a/Plugin/Payment/MethodList.php b/Plugin/Payment/MethodList.php index 402c508..238e172 100644 --- a/Plugin/Payment/MethodList.php +++ b/Plugin/Payment/MethodList.php @@ -17,10 +17,7 @@ */ class MethodList { - /** - * @var ConfigRepository - */ - private $configRepository; + private ConfigRepository $configRepository; /** * MethodList constructor. diff --git a/Plugin/Quote/ChangeQuoteControl.php b/Plugin/Quote/ChangeQuoteControl.php deleted file mode 100644 index ac735ea..0000000 --- a/Plugin/Quote/ChangeQuoteControl.php +++ /dev/null @@ -1,36 +0,0 @@ -getPayment()->getMethod() == 'truelayer') { - return true; - } - return $result; - } -} diff --git a/Service/Api/GetClient.php b/Service/Api/GetClient.php deleted file mode 100644 index 3f2fdef..0000000 --- a/Service/Api/GetClient.php +++ /dev/null @@ -1,89 +0,0 @@ -configProvider = $configProvider; - $this->logRepository = $logRepository; - } - - /** - * @param int $storeId - * @param array|null $data - * @return ClientInterface|null - */ - public function execute(int $storeId = 0, ?array $data = []): ?ClientInterface - { - $this->storeId = $storeId; - if (isset($data['credentials'])) { - $this->credentials = $data['credentials']; - } else { - $this->credentials = $this->configProvider->getCredentials((int)$storeId); - } - - return $this->getClient(); - } - - /** - * @return ClientInterface|null - */ - private function getClient(): ?ClientInterface - { - try { - \TrueLayer\Settings::tlAgent('truelayer-magento/' . $this->configProvider->getExtensionVersion()); - $client = Client::configure() - ->clientId($this->credentials['client_id']) - ->clientSecret($this->credentials['client_secret']) - ->keyId($this->credentials['key_id']) - ->pemFile($this->credentials['private_key']) - ->useProduction(!$this->configProvider->isSandbox()); - - return $client->create(); - } catch (\Exception $e) { - $this->logRepository->addDebugLog('Get Client', $e->getMessage()); - return null; - } - } -} diff --git a/Service/Client/ClientFactory.php b/Service/Client/ClientFactory.php new file mode 100644 index 0000000..ed4cde7 --- /dev/null +++ b/Service/Client/ClientFactory.php @@ -0,0 +1,70 @@ +configProvider = $configProvider; + $this->logger = $logger; + } + + /** + * @param int $storeId + * @param array|null $data + * @return ClientInterface|null + * @throws SignerException + */ + public function create(int $storeId = 0, ?array $data = []): ?ClientInterface + { + $credentials = $data['credentials'] ?? $this->configProvider->getCredentials($storeId); + + try { + return $this->createClient($credentials); + } catch (Exception $e) { + $this->logger->debug('Client Creation Failed', $e->getMessage()); + throw $e; + } + } + + /** + * @param array $credentials + * @return ClientInterface|null + * @throws SignerException + */ + private function createClient(array $credentials): ?ClientInterface + { + Settings::tlAgent('truelayer-magento/' . $this->configProvider->getExtensionVersion()); + return Client::configure() + ->clientId($credentials['client_id']) + ->clientSecret($credentials['client_secret']) + ->keyId($credentials['key_id']) + ->pemFile($credentials['private_key']) + ->useProduction(!$this->configProvider->isSandbox()) + ->create(); + } +} diff --git a/Logger/Handler/Debug.php b/Service/Log/DebugHandler.php similarity index 84% rename from Logger/Handler/Debug.php rename to Service/Log/DebugHandler.php index 3f92b5f..c6492f3 100644 --- a/Logger/Handler/Debug.php +++ b/Service/Log/DebugHandler.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace TrueLayer\Connect\Logger\Handler; +namespace TrueLayer\Connect\Service\Log; use Magento\Framework\Logger\Handler\Base; use Monolog\Logger; @@ -13,7 +13,7 @@ /** * Debug logger handler class */ -class Debug extends Base +class DebugHandler extends Base { /** diff --git a/Logger/Handler/Error.php b/Service/Log/ErrorHandler.php similarity index 84% rename from Logger/Handler/Error.php rename to Service/Log/ErrorHandler.php index 9e8d8f7..742292e 100644 --- a/Logger/Handler/Error.php +++ b/Service/Log/ErrorHandler.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace TrueLayer\Connect\Logger\Handler; +namespace TrueLayer\Connect\Service\Log; use Magento\Framework\Logger\Handler\Base; use Monolog\Logger; @@ -13,9 +13,8 @@ /** * Error logger handler class */ -class Error extends Base +class ErrorHandler extends Base { - /** * @var int */ diff --git a/Service/Log/LogService.php b/Service/Log/LogService.php new file mode 100644 index 0000000..5db9a23 --- /dev/null +++ b/Service/Log/LogService.php @@ -0,0 +1,125 @@ +configProvider = $configProvider; + $this->debugLogger = $debugLogger; + $this->errorLogger = $errorLogger; + } + + /** + * @inheritDoc + */ + public function error(string $type, $data = ''): self + { + $this->errorLogger->addRecord(Logger::ERROR, $this->buildMessage($type, $data)); + + return $this; + } + + /** + * @inheritDoc + */ + public function debug(string $type, $data = ''): self + { + if ($this->configProvider->isDebugLoggingEnabled()) { + $this->debugLogger->addRecord(Logger::INFO, $this->buildMessage($type, $data)); + } + + return $this; + } + + /** + * @inheriDoc + */ + public function addPrefix($prefix): self + { + $this->prefixes[] = $prefix; + return $this; + } + + /** + * @param int|string $prefix + * @return $this + */ + public function removePrefix($prefix): LogServiceInterface + { + foreach ($this->prefixes as $key => $value) { + if ($value ===$prefix) { + unset($this->prefixes[$key]); + return $this; + } + } + return $this; + } + + /** + * @param string $msg + * @param mixed $data + * @return string + */ + private function buildMessage(string $msg, $data = ''): string + { + $parts = $this->prefixes; + $parts[] = $msg; + + if ($serialisedData = $this->convertDataToString($data)) { + $parts[] = $serialisedData; + } + + return join(' > ', $parts); + } + + /** + * @param $data + * @return string + */ + private function convertDataToString($data): string + { + if ($data instanceof Exception) { + return $data->getMessage() . " " . $data->getTraceAsString(); + } + + if (empty($data)) { + return ''; + } + + if (is_array($data) || is_object($data)) { + if ($result = json_encode($data)) { + return $result; + } + } + + return "$data"; + } +} diff --git a/Service/Order/BaseTransactionService.php b/Service/Order/BaseTransactionService.php new file mode 100644 index 0000000..d7f77ff --- /dev/null +++ b/Service/Order/BaseTransactionService.php @@ -0,0 +1,90 @@ +logger = $logger; + } + + /** + * @param callable $fn + * @throws Exception + */ + public function execute(callable $fn): void + { + $this->logger->addPrefix('Transaction')->debug('Start'); + + $transaction = $this->getTransaction(); + $this->validateTransaction($transaction); + + if ($transaction->getIsLocked()) { + $this->logger->debug('Aborting, locked'); + return; + } + + if (!in_array($transaction->getStatus(), ['NULL', null])) { + $this->logger->debug('Aborting, already completed'); + return; + } + + $this->logger->debug('Locking'); + $transaction->setIsLocked(true); + $this->saveTransaction($transaction); + + try { + $this->logger->debug('Execute logic'); + $fn($transaction); + } catch (Exception $e) { + $this->logger->error('Exception in transaction', $e); + throw $e; + } finally { + $transaction->setIsLocked(false); + $this->saveTransaction($transaction); + $this->logger->debug('Unlocked transaction'); + $this->logger->removePrefix('Transaction'); + } + } + + /** + * @return BaseTransactionDataInterface + */ + abstract protected function getTransaction(): BaseTransactionDataInterface; + + /** + * @param BaseTransactionDataInterface $transaction + */ + abstract protected function saveTransaction(BaseTransactionDataInterface $transaction): void; + + /** + * @throws Exception + */ + private function validateTransaction(BaseTransactionDataInterface $transaction): void + { + $this->logger->debug("Details", [ + 'transaction id' => $transaction->getEntityId(), + 'order id' => $transaction->getOrderId(), + ]); + + if (!$transaction->getOrderId()) { + $this->logger->error('Transaction with missing order found'); + throw new Exception('Transaction with missing order found'); + } + } +} diff --git a/Service/Order/HPPService.php b/Service/Order/HPPService.php new file mode 100644 index 0000000..ab5d253 --- /dev/null +++ b/Service/Order/HPPService.php @@ -0,0 +1,38 @@ +configRepository = $configRepository; + } + + /** + * @param PaymentCreatedInterface $paymentCreated + * @return string + */ + public function getRedirectUrl(PaymentCreatedInterface $paymentCreated): string + { + return $paymentCreated->hostedPaymentsPage() + ->returnUri($this->configRepository->getBaseUrl() . 'truelayer/checkout/process/') + ->primaryColour($this->configRepository->getPaymentPagePrimaryColor()) + ->secondaryColour($this->configRepository->getPaymentPageSecondaryColor()) + ->tertiaryColour($this->configRepository->getPaymentPageTertiaryColor()) + ->toUrl(); + } +} diff --git a/Service/Order/MakeRequest.php b/Service/Order/MakeRequest.php deleted file mode 100644 index bb13f1b..0000000 --- a/Service/Order/MakeRequest.php +++ /dev/null @@ -1,324 +0,0 @@ -configProvider = $configProvider; - $this->checkoutSession = $checkoutSession; - $this->getClient = $getClient; - $this->quoteRepository = $quoteRepository; - $this->cartManagement = $cartManagement; - $this->transactionRepository = $transactionRepository; - $this->quoteAddressFactory = $quoteAddressFactory; - $this->dataObjectProcessor = $dataObjectProcessor; - $this->dataObjectHelper = $dataObjectHelper; - $this->userRepository = $userRepository; - $this->logRepository = $logRepository; - } - - /** - * Executes TrueLayer Api for Order Request and returns redirect to platform Url - * - * @param string $token - * @return string - * @throws AuthenticationException - * @throws CouldNotSaveException - * @throws InputException - * @throws LocalizedException - * @throws NoSuchEntityException - * @throws \TrueLayer\Exceptions\InvalidArgumentException - * @throws \TrueLayer\Exceptions\ValidationException - */ - public function execute(string $token): string - { - $transaction = $this->transactionRepository->getByToken($token); - $quote = $this->quoteRepository->get($transaction->getQuoteId()); - $quote->collectTotals(); - - if (!$quote->getReservedOrderId()) { - $quote->reserveOrderId(); - $this->quoteRepository->save($quote); - } - - $client = $this->getClient->execute($quote->getStoreId()); - if (!$client) { - throw new AuthenticationException( - __('Credentials are not correct') - ); - } - - $merchantAccountId = $this->getMerchantAccountId($client, $quote); - - $paymentData = $this->prepareData($quote, $merchantAccountId); - $payment = $client->payment()->fill($paymentData)->create(); - if (!$this->truelayerUser) { - $this->userRepository->set( - $quote->getBillingAddress()->getEmail() ?: $quote->getCustomerEmail(), - $payment->getUserId() - ); - } - - if ($payment->getId()) { - $transaction->setUuid($payment->getId()); - $this->transactionRepository->save($transaction); - $this->duplicateCurrentQuote($quote); - return $payment->hostedPaymentsPage() - ->returnUri($this->getReturnUrl()) - ->primaryColour($this->configProvider->getPaymentPagePrimaryColor()) - ->secondaryColour($this->configProvider->getPaymentPageSecondaryColor()) - ->tertiaryColour($this->configProvider->getPaymentPageTertiaryColor()) - ->toUrl(); - } - - $msg = self::REQUEST_EXCEPTION; - throw new LocalizedException(__($msg)); - } - - /** - * @param ClientInterface $client - * @param Quote $quote - * @return string|null - * @throws LocalizedException - * @throws \TrueLayer\Exceptions\ApiRequestJsonSerializationException - * @throws \TrueLayer\Exceptions\ApiResponseUnsuccessfulException - * @throws \TrueLayer\Exceptions\InvalidArgumentException - * @throws \TrueLayer\Exceptions\SignerException - * @throws \TrueLayer\Exceptions\ValidationException - */ - private function getMerchantAccountId(ClientInterface $client, Quote $quote) - { - $merchantAccounts = $client->getMerchantAccounts(); - foreach ($merchantAccounts as $merchantAccount) { - if ($merchantAccount->getCurrency() == $quote->getBaseCurrencyCode()) { - return $merchantAccount->getId(); - } - } - throw new LocalizedException(__('No merchant account found')); - } - - /** - * @param Quote $quote - * @param string $merchantAccountId - * @return array - */ - private function prepareData(Quote $quote, string $merchantAccountId): array - { - $customerEmail = $quote->getBillingAddress()->getEmail() ?: $quote->getCustomerEmail(); - - $data = [ - "amount_in_minor" => (int) round($quote->getBaseGrandTotal() * 100, 0, PHP_ROUND_HALF_UP), - "currency" => $quote->getBaseCurrencyCode(), - "payment_method" => [ - "provider_selection" => [ - "filter" => [ - "countries" => [ - $quote->getShippingAddress()->getCountryId() - ], - "release_channel" => "general_availability", - "customer_segments" => $this->configProvider->getBankingProviders(), - "excludes" => [ - "provider_ids" => [ - "ob-exclude-this-bank" - ] - ] - ], - "type" => "user_selected" - ], - "type" => "bank_transfer", - "beneficiary" => [ - "type" => "merchant_account", - "name" => $this->configProvider->getMerchantAccountName(), - "merchant_account_id" => $merchantAccountId - ] - ], - "user" => [ - "name" => trim($quote->getBillingAddress()->getFirstname()) . - ' ' . - trim($quote->getBillingAddress()->getLastname()), - "email" => $customerEmail - ] - ]; - - $this->truelayerUser = $this->userRepository->get($customerEmail); - if ($this->truelayerUser) { - $data['user']['id'] = $this->truelayerUser['truelayer_id']; - } - - $this->logRepository->addDebugLog('order request', $data); - - return $data; - } - - /** - * Duplicate current quote and set this as active session. - * This prevents quotes to change during checkout process - * - * @param Quote $quote - * @throws NoSuchEntityException - * @throws CouldNotSaveException - */ - private function duplicateCurrentQuote(Quote $quote) - { - $quote->setIsActive(false); - $this->quoteRepository->save($quote); - if ($customerId = $quote->getCustomerId()) { - $cartId = $this->cartManagement->createEmptyCartForCustomer($customerId); - } else { - $cartId = $this->cartManagement->createEmptyCart(); - } - $newQuote = $this->quoteRepository->get($cartId); - $newQuote->merge($quote); - - $newQuote->removeAllAddresses(); - if (!$quote->getIsVirtual()) { - $addressData = $this->dataObjectProcessor->buildOutputDataArray( - $quote->getShippingAddress(), - AddressInterface::class - ); - unset($addressData['id']); - $shippingAddress = $this->quoteAddressFactory->create(); - $this->dataObjectHelper->populateWithArray( - $shippingAddress, - $addressData, - AddressInterface::class - ); - $newQuote->setShippingAddress( - $shippingAddress - ); - } - - $addressData = $this->dataObjectProcessor->buildOutputDataArray( - $quote->getBillingAddress(), - AddressInterface::class - ); - unset($addressData['id']); - $billingAddress = $this->quoteAddressFactory->create(); - $this->dataObjectHelper->populateWithArray( - $billingAddress, - $addressData, - AddressInterface::class - ); - $newQuote->setBillingAddress( - $billingAddress - ); - - $newQuote->setTotalsCollectedFlag(false)->collectTotals(); - $this->quoteRepository->save($newQuote); - - $this->checkoutSession->replaceQuote($newQuote); - } - - /** - * Get return url - * - * @return string - */ - private function getReturnUrl(): string - { - return $this->configProvider->getBaseUrl() . 'truelayer/checkout/process/'; - } -} diff --git a/Service/Order/OrderCommentHistory.php b/Service/Order/OrderCommentHistory.php deleted file mode 100644 index 7f37348..0000000 --- a/Service/Order/OrderCommentHistory.php +++ /dev/null @@ -1,70 +0,0 @@ -historyFactory = $historyFactory; - $this->historyRepository = $historyRepository; - } - - /** - * Add comment to order - * - * @param OrderInterface $order - * @param Phrase $message - * @param bool $isCustomerNotified - * @throws CouldNotSaveException - */ - public function add(OrderInterface $order, Phrase $message, bool $isCustomerNotified = false) - { - if (!$message->getText()) { - return; - } - - if (!$order->canComment()) { - return; - } - - /** @var OrderStatusHistoryInterface $history */ - $history = $this->historyFactory->create(); - $history->setParentId($order->getEntityId()) - ->setComment($message) - ->setStatus($order->getStatus()) - ->setIsCustomerNotified($isCustomerNotified) - ->setEntityName('order'); - $this->historyRepository->save($history); - } -} diff --git a/Service/Order/PaymentCreationService.php b/Service/Order/PaymentCreationService.php new file mode 100644 index 0000000..953a24f --- /dev/null +++ b/Service/Order/PaymentCreationService.php @@ -0,0 +1,202 @@ +clientFactory = $clientFactory; + $this->configRepository = $configRepository; + $this->transactionRepository = $transactionRepository; + $this->userRepository = $userRepository; + $this->mathRandom = $mathRandom; + $this->logger = $logger; + } + + /** + * @param OrderInterface $order + * @return PaymentCreatedInterface + * @throws AuthenticationException + * @throws ApiRequestJsonSerializationException + * @throws ApiResponseUnsuccessfulException + * @throws InvalidArgumentException + * @throws SignerException + * @throws LocalizedException + */ + public function createPayment(OrderInterface $order): PaymentCreatedInterface + { + $this->logger->addPrefix('PaymentCreationService')->debug('Start'); + + // Get the TL user id if we recognise the email address + $customerEmail = $order->getBillingAddress()->getEmail() ?: $order->getCustomerEmail(); + $existingUser = $this->userRepository->get($customerEmail); + $existingUserId = $existingUser["truelayer_id"] ?? null; + + // Create the TL payment + $client = $this->clientFactory->create((int) $order->getStoreId()); + $this->logger->debug('Create client'); + + $merchantAccountId = $this->getMerchantAccountId($client, $order); + $this->logger->debug('Merchant account', $merchantAccountId); + + $paymentConfig = $this->createPaymentConfig($order, $merchantAccountId, $customerEmail, $existingUserId); + $payment = $client->payment()->fill($paymentConfig)->create(); + $this->logger->debug('Created payment', $payment->getId()); + + // If new user, we save it + if (!$existingUser) { + $this->userRepository->set($customerEmail, $payment->getUserId()); + $this->logger->debug('Saved new user'); + } + + // Link the quote id to the payment id in the transaction table + $transaction = $this->getTransaction($order)->setPaymentUuid($payment->getId()); + $this->transactionRepository->save($transaction); + $this->logger->debug('Payment transaction created', $transaction->getEntityId()); + + return $payment; + } + + /** + * @param OrderInterface $order + * @param string $merchantAccId + * @param string $customerEmail + * @param string|null $existingUserId + * @return array + */ + private function createPaymentConfig( + OrderInterface $order, + string $merchantAccId, + string $customerEmail, + string $existingUserId = null + ): array { + $config = [ + "amount_in_minor" => AmountHelper::toMinor($order->getBaseGrandTotal()), + "currency" => $order->getBaseCurrencyCode(), + "payment_method" => [ + "provider_selection" => [ + "filter" => [ + "countries" => [ + $order->getBillingAddress()->getCountryId() + ], + "release_channel" => "general_availability", + "customer_segments" => $this->configRepository->getBankingProviders(), + "excludes" => [ + "provider_ids" => [ + "ob-exclude-this-bank" + ] + ] + ], + "type" => "user_selected" + ], + "type" => "bank_transfer", + "beneficiary" => [ + "type" => "merchant_account", + "name" => $this->configRepository->getMerchantAccountName(), + "merchant_account_id" => $merchantAccId + ] + ], + "user" => [ + "id" => $existingUserId, + "name" => trim($order->getBillingAddress()->getFirstname()) . + ' ' . + trim($order->getBillingAddress()->getLastname()), + "email" => $customerEmail + ], + "metadata" => [ + "Magento Order ID" => $order->getEntityId(), + "Magento Store ID" => $order->getStoreId(), + ] + ]; + + $this->logger->debug('Payment config', $config); + + return $config; + } + + /** + * @param ClientInterface $client + * @param OrderInterface $order + * @return string + * @throws ApiRequestJsonSerializationException + * @throws ApiResponseUnsuccessfulException + * @throws InvalidArgumentException + * @throws SignerException + * @throws Exception + */ + private function getMerchantAccountId(ClientInterface $client, OrderInterface $order): string + { + foreach ($client->getMerchantAccounts() as $merchantAccount) { + if ($merchantAccount->getCurrency() == $order->getBaseCurrencyCode()) { + return $merchantAccount->getId(); + } + } + + throw new Exception('No merchant account found'); + } + + /** + * @param OrderInterface $order + * @return PaymentTransactionDataInterface + * @throws LocalizedException + */ + private function getTransaction(OrderInterface $order): PaymentTransactionDataInterface + { + try { + return $this->transactionRepository->getByOrderId((int) $order->getEntityId()); + } catch (Exception $exception) { + $transaction = $this->transactionRepository->create() + ->setOrderId((int) $order->getEntityId()) + ->setQuoteId((int) $order->getQuoteId()) + ->setToken($this->mathRandom->getUniqueHash('trl')); + + return $this->transactionRepository->save($transaction); + } + } +} diff --git a/Service/Order/PaymentErrorMessageManager.php b/Service/Order/PaymentErrorMessageManager.php new file mode 100644 index 0000000..12bde08 --- /dev/null +++ b/Service/Order/PaymentErrorMessageManager.php @@ -0,0 +1,38 @@ +messageManager = $messageManager; + } + + /** + * Add a unique message using our custom truelayer payment error template + * This will trigger a cart and checkout-data refresh on the frontend + * @param string $text + */ + public function addMessage(string $text): void + { + $message = $this->messageManager + ->createMessage(MessageInterface::TYPE_ERROR, 'truelayer_payment_error') + ->setData(['text' => $text]); + + $this->messageManager->addUniqueMessages([ $message ]); + } +} diff --git a/Service/Order/PaymentUpdate/PaymentFailedService.php b/Service/Order/PaymentUpdate/PaymentFailedService.php new file mode 100644 index 0000000..adfdce2 --- /dev/null +++ b/Service/Order/PaymentUpdate/PaymentFailedService.php @@ -0,0 +1,77 @@ +orderRepository = $orderRepository; + $this->transactionService = $transactionService; + $this->logger = $logger; + } + + /** + * @param string $paymentId + * @param string $failureReason + * @throws Exception + */ + public function handle(string $paymentId, string $failureReason): void + { + $prefix = "PaymentFailedService $paymentId"; + $this->logger->addPrefix($prefix); + + $this->transactionService + ->paymentId($paymentId) + ->execute(fn($transaction) => $this->cancelOrder($transaction, $failureReason)); + + $this->logger->removePrefix($prefix); + } + + /** + * @param PaymentTransactionDataInterface $transaction + * @param string $failureReason + */ + private function cancelOrder(PaymentTransactionDataInterface $transaction, string $failureReason): void + { + $order = $this->orderRepository->get($transaction->getOrderId()); + + if (!$order->isCanceled()) { + $order->cancel(); + $this->logger->debug('Order cancelled'); + } + + $niceMessage = PaymentFailureReasonHelper::getHumanReadableLabel($failureReason); + $orderComment = "Order cancelled. $niceMessage ($failureReason)"; + $order->addStatusToHistory($order->getStatus(), $orderComment, true); + $this->orderRepository->save($order); + $this->logger->debug('Order comment added'); + + $transaction->setPaymentFailed(); + $transaction->setFailureReason($failureReason); + $this->logger->debug('Payment transaction updated'); + } +} diff --git a/Service/Order/PaymentUpdate/PaymentSettledService.php b/Service/Order/PaymentUpdate/PaymentSettledService.php new file mode 100644 index 0000000..60bc525 --- /dev/null +++ b/Service/Order/PaymentUpdate/PaymentSettledService.php @@ -0,0 +1,151 @@ +orderRepository = $orderRepository; + $this->orderSender = $orderSender; + $this->invoiceSender = $invoiceSender; + $this->configRepository = $configRepository; + $this->transactionService = $transactionService; + $this->logger = $logger; + } + + /** + * @param string $paymentId + * @throws InputException + * @throws NoSuchEntityException + * @throws LocalizedException + * @throws Exception + */ + public function handle(string $paymentId): void + { + $prefix = "PaymentSettledService $paymentId"; + $this->logger->addPrefix($prefix); + + $this->transactionService + ->paymentId($paymentId) + ->execute(function (PaymentTransactionDataInterface $transaction) use ($paymentId) { + $order = $this->orderRepository->get($transaction->getOrderId()); + $this->updateOrder($order, $paymentId); + $transaction->setPaymentSettled(); + $this->sendOrderEmail($order); + $this->sendInvoiceEmail($order); + }); + + $this->logger->removePrefix($prefix); + } + + private function updateOrder(OrderInterface $order, string $paymentId): void + { + // Update order payment + $payment = $order->getPayment(); + $payment->setTransactionId($paymentId); + $payment->setIsTransactionClosed(true); + $payment->registerCaptureNotification($order->getGrandTotal(), true); + + // Update order state & status + $order->setState(Order::STATE_PROCESSING)->setStatus(Order::STATE_PROCESSING); + $this->orderRepository->save($order); + $this->logger->debug('Payment and order statuses updated'); + } + + /** + * @param OrderInterface $order + * @return void + */ + private function sendOrderEmail(OrderInterface $order): void + { + if ($order->getEmailSent()) { + $this->logger->debug('Order email already sent'); + return; + } + + if (!$this->configRepository->sendOrderEmail()) { + $this->logger->debug('Order email not enabled'); + return; + } + + $this->orderSender->send($order); + $this->logger->debug('Order email sent'); + + $order->addStatusToHistory($order->getStatus(), __('New order email sent'), true); + $this->orderRepository->save($order); + $this->logger->debug('Order note added'); + } + + /** + * @param OrderInterface $order + * @throws Exception + */ + private function sendInvoiceEmail(OrderInterface $order): void + { + /** @var Order\Invoice $invoice */ + $invoice = $order->getInvoiceCollection()->getFirstItem(); + + if (!$invoice) { + $this->logger->debug('Invoice not found'); + return; + } + + if ($invoice->getEmailSent()) { + $this->logger->debug('Invoice email already sent'); + return; + } + + if (!$this->configRepository->sendInvoiceEmail()) { + $this->logger->debug('Invoice email not enabled'); + return; + } + + $this->invoiceSender->send($invoice); + $this->logger->debug('Invoice email sent'); + + $message = __('Notified customer about invoice #%1', $invoice->getIncrementId()); + $order->addStatusToHistory($order->getStatus(), $message, true); + $this->orderRepository->save($order); + $this->logger->debug('Order note added'); + } +} diff --git a/Service/Order/PaymentUpdate/PaymentTransactionService.php b/Service/Order/PaymentUpdate/PaymentTransactionService.php new file mode 100644 index 0000000..5232cca --- /dev/null +++ b/Service/Order/PaymentUpdate/PaymentTransactionService.php @@ -0,0 +1,61 @@ +transactionRepository = $transactionRepository; + parent::__construct($logger); + } + + /** + * @param string $paymentId + * @return $this + */ + public function paymentId(string $paymentId): self + { + $this->paymentId = $paymentId; + return $this; + } + + /** + * @return BaseTransactionDataInterface + * @throws InputException + * @throws NoSuchEntityException + */ + protected function getTransaction(): BaseTransactionDataInterface + { + return $this->transactionRepository->getByPaymentUuid($this->paymentId); + } + + /** + * @param BaseTransactionDataInterface $transaction + * @throws LocalizedException + */ + protected function saveTransaction(BaseTransactionDataInterface $transaction): void + { + $this->transactionRepository->save($transaction); + } +} diff --git a/Service/Order/ProcessReturn.php b/Service/Order/ProcessReturn.php deleted file mode 100644 index c66d927..0000000 --- a/Service/Order/ProcessReturn.php +++ /dev/null @@ -1,171 +0,0 @@ -checkoutSession = $checkoutSession; - $this->getClient = $getClient; - $this->quoteRepository = $quoteRepository; - $this->orderInterface = $orderInterface; - $this->orderRepository = $orderRepository; - $this->cartManagement = $cartManagement; - $this->transactionRepository = $transactionRepository; - $this->logger = $logger; - } - - /** - * @param string $transactionId - * @return array - * @throws InputException - * @throws NoSuchEntityException - * @throws \TrueLayer\Exceptions\ApiRequestJsonSerializationException - * @throws \TrueLayer\Exceptions\ApiResponseUnsuccessfulException - * @throws \TrueLayer\Exceptions\SignerException - * @throws \TrueLayer\Exceptions\ValidationException - */ - public function execute(string $transactionId): array - { - $transaction = $this->transactionRepository->getByUuid($transactionId); - $quote = $this->quoteRepository->get($transaction->getQuoteId()); - $this->checkoutSession->setLoadInactive(true)->replaceQuote($quote); - - $order = $this->orderInterface->loadByAttribute('quote_id', $quote->getId()); - - $client = $this->getClient->execute($quote->getStoreId()); - $payment = $client->getPayment($transactionId); - $transactionStatus = $payment->getStatus(); - - if (!$order->getEntityId()) { - if ($transactionStatus == 'settled' || $transactionStatus == 'executed') { - return ['success' => false, 'status' => $transactionStatus]; - } - } - - switch ($transactionStatus) { - case 'executed': - case 'settled': - $this->updateCheckoutSession($quote, $order); - return ['success' => true, 'status' => $transactionStatus]; - case 'cancelled': - $message = (string)self::CANCELLED_MSG; - return ['success' => false, 'status' => $transactionStatus, 'msg' => __($message)]; - case 'failed': - $message = (string)self::FAILED_MSG; - return ['success' => false, 'status' => $transactionStatus, 'msg' => __($message)]; - case 'rejected': - $message = (string)self::REJECTED_MSG; - return ['success' => false, 'status' => $transactionStatus, 'msg' => __($message)]; - default: - $message = (string)self::UNKNOWN_MSG; - return ['success' => false, 'status' => $transactionStatus, 'msg' => __($message)]; - } - } - - /** - * @param CartInterface $quote - * @param Order $order - * @throws \Magento\Framework\Exception\CouldNotSaveException - */ - private function updateCheckoutSession(CartInterface $quote, Order $order): void - { - $this->orderRepository->save($order); - - // Remove additional quote for customer - if ($customerId = $quote->getCustomer()->getId()) { - try { - $activeQuote = $this->quoteRepository->getActiveForCustomer($customerId); - $this->quoteRepository->delete($activeQuote); - $this->cartManagement->createEmptyCartForCustomer($customerId); - } catch (NoSuchEntityException $e) { - $this->logger->addErrorLog('Remove customer quote', $e->getMessage()); - } - } - - $this->checkoutSession->setLastQuoteId($quote->getEntityId()) - ->setLastSuccessQuoteId($quote->getEntityId()) - ->setLastRealOrderId($order->getIncrementId()) - ->setLastOrderId($order->getId()); - } -} diff --git a/Service/Order/ProcessWebhook.php b/Service/Order/ProcessWebhook.php deleted file mode 100644 index 3a01e08..0000000 --- a/Service/Order/ProcessWebhook.php +++ /dev/null @@ -1,236 +0,0 @@ -quoteRepository = $quoteRepository; - $this->transactionRepository = $transactionRepository; - $this->cartManagement = $cartManagement; - $this->orderRepository = $orderRepository; - $this->orderSender = $orderSender; - $this->invoiceSender = $invoiceSender; - $this->configRepository = $configRepository; - $this->logRepository = $logRepository; - $this->checkoutSession = $checkoutSession; - $this->userRepository = $userRepository; - } - - /** - * Place order via webhook - * - * @param string $uuid - * @param string $userId - */ - public function execute(string $uuid, string $userId) - { - $this->logRepository->addDebugLog('webhook payload uuid', $uuid); - - try { - $transaction = $this->transactionRepository->getByUuid($uuid); - - $this->logRepository->addDebugLog( - 'webhook transaction id', - $transaction->getEntityId() . ' quote_id = ' . $transaction->getQuoteId() - ); - - if (!$quoteId = $transaction->getQuoteId()) { - $this->logRepository->addDebugLog('webhook', 'no quote id found in transaction'); - return; - } - - $quote = $this->quoteRepository->get($quoteId); - $this->checkoutSession->setQuoteId($quoteId); - - if (!$this->transactionRepository->isLocked($transaction)) { - $this->logRepository->addDebugLog('webhook', 'start processing accepted transaction'); - $this->transactionRepository->lock($transaction); - - if (!$this->transactionRepository->checkOrderIsPlaced($transaction)) { - $orderId = $this->placeOrder($quote, $uuid, $userId); - $transaction->setOrderId((int)$orderId)->setStatus('payment_settled'); - $this->transactionRepository->save($transaction); - $this->logRepository->addDebugLog('webhook', 'Order placed. Order id = ' . $orderId); - } - - $this->transactionRepository->unlock($transaction); - $this->logRepository->addDebugLog('webhook', 'end processing accepted transaction'); - } - } catch (Exception $e) { - $this->logRepository->addDebugLog('webhook exception', $e->getMessage()); - } - } - - /** - * @param CartInterface $quote - * @param $uuid - * @param $userId - * @return false|int|null - */ - private function placeOrder(CartInterface $quote, $uuid, $userId) - { - try { - $quote = $this->prepareQuote($quote, $userId); - $orderId = $this->cartManagement->placeOrder($quote->getId()); - $order = $this->orderRepository->get($orderId); - $this->sendOrderEmail($order); - - $payment = $order->getPayment(); - $payment->setTransactionId($uuid); - $payment->setIsTransactionClosed(true); - $payment->registerCaptureNotification($order->getGrandTotal(), true); - $order->setState(Order::STATE_PROCESSING)->setStatus(Order::STATE_PROCESSING); - $this->orderRepository->save($order); - $this->sendInvoiceEmail($order); - } catch (Exception $e) { - $this->logRepository->addDebugLog('place order', $e->getMessage()); - return false; - } - - return $order->getEntityId(); - } - - /** - * Make sure the quote is valid for order placement. - * - * Force setCustomerIsGuest; see issue: https://github.com/magento/magento2/issues/23908 - * - * @param CartInterface $quote - * - * @return CartInterface - */ - private function prepareQuote(CartInterface $quote, string $userId): CartInterface - { - if ($quote->getCustomerEmail() == null) { - $user = $this->userRepository->getByTruelayerId($userId); - $quote->setCustomerEmail($user['magento_email']); - } - - $quote->setCustomerIsGuest($quote->getCustomerId() == null); - $quote->setIsActive(true); - $quote->getShippingAddress()->setCollectShippingRates(false); - $this->quoteRepository->save($quote); - return $quote; - } - - /** - * @param OrderInterface $order - * @return void - */ - private function sendOrderEmail(OrderInterface $order): void - { - if (!$order->getEmailSent() && $this->configRepository->sendOrderEmail()) { - $this->orderSender->send($order); - $message = __('New order email sent'); - $order->addStatusToHistory($order->getStatus(), $message, true); - } - } - - /** - * @param OrderInterface $order - * @return void - * @throws Exception - */ - private function sendInvoiceEmail(OrderInterface $order): void - { - /** @var Order\Invoice $invoice */ - $invoice = $order->getInvoiceCollection()->getFirstItem(); - if ($invoice && !$invoice->getEmailSent() && $this->configRepository->sendInvoiceEmail()) { - $this->invoiceSender->send($invoice); - $message = __('Notified customer about invoice #%1', $invoice->getIncrementId()); - $order->addStatusToHistory($order->getStatus(), $message, true); - } - } -} diff --git a/Service/Order/RefundOrder.php b/Service/Order/RefundOrder.php deleted file mode 100644 index bd8df5e..0000000 --- a/Service/Order/RefundOrder.php +++ /dev/null @@ -1,77 +0,0 @@ -getClient = $getClient; - $this->transactionRepository = $transactionRepository; - } - - /** - * Executes TrueLayer Api for Order Refund - * - * @param OrderInterface $order - * @param float $amount - * @return array - * @throws InputException - * @throws LocalizedException - * @throws NoSuchEntityException - * @throws \TrueLayer\Exceptions\InvalidArgumentException - * @throws \TrueLayer\Exceptions\ValidationException - */ - public function execute(OrderInterface $order, float $amount): array - { - $transaction = $this->transactionRepository->getByOrderId((int)$order->getId()); - - if ($amount != 0) { - $client = $this->getClient->execute((int)$order->getStoreId()); - $refundId = $client->refund() - ->payment($transaction->getUuid()) - ->amountInMinor((int)bcmul((string)$amount, '100')) - ->reference($transaction->getInvoiceUuid()) - ->create() - ->getId(); - if (!$refundId) { - $exceptionMsg = (string)self::EXCEPTION_MSG; - throw new LocalizedException(__($exceptionMsg, $order->getIncrementId())); - } - } - - return []; - } -} diff --git a/Service/Order/RefundService.php b/Service/Order/RefundService.php new file mode 100644 index 0000000..b55dab5 --- /dev/null +++ b/Service/Order/RefundService.php @@ -0,0 +1,132 @@ +clientFactory = $clientFactory; + $this->paymentTransactionRepository = $paymentTransactionRepository; + $this->refundTransactionRepository = $refundTransactionRepository; + $this->logger = $logger; + } + + /** + * @param OrderInterface $order + * @param string $invoiceIncrementId + * @param float $amount + * @return string|null + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws SignerException + */ + public function refund(OrderInterface $order, string $invoiceIncrementId, float $amount): ?string + { + $this->logger->addPrefix('RefundService')->debug('Start'); + + if ($amount == 0) { + return null; + } + + $transaction = $this->paymentTransactionRepository->getByOrderId((int) $order->getEntityId()); + $paymentId = $transaction->getPaymentUuid(); + + $amountInMinor = AmountHelper::toMinor($amount); + $refundId = $this->createRefund($order, $amountInMinor, $transaction, $invoiceIncrementId); + $this->logger->debug('Created refund', $refundId); + + $refundTransaction = $this->refundTransactionRepository->create() + ->setOrderId((int) $order->getEntityId()) + ->setPaymentUuid($paymentId) + ->setRefundUuid($refundId) + ->setAmount($amountInMinor); + + $this->refundTransactionRepository->save($refundTransaction); + $this->logger->debug('Refund transaction created', $refundTransaction->getEntityId()); + + $this->logger->removePrefix('RefundService'); + + return $refundId; + } + + /** + * @param OrderInterface $order + * @param int $amount + * @param TransactionInterface $transaction + * @param string $invoiceIncrementId + * @return string + * @throws LocalizedException + * @throws SignerException + */ + private function createRefund( + OrderInterface $order, + int $amount, + TransactionInterface $transaction, + string $invoiceIncrementId + ): string { + $client = $this->clientFactory->create((int) $order->getStoreId()); + + try { + $refundId = $client->refund() + ->reference($invoiceIncrementId) + ->payment($transaction->getPaymentUuid()) + ->amountInMinor($amount) + ->create() + ->getId(); + } catch (ApiResponseUnsuccessfulException $e) { + $this->logger->error('Refund invalid input', $e->getDetail()); + $msg = self::EXCEPTION_MSG . $e->getDetail(); + throw new LocalizedException(__($msg, $order->getIncrementId())); + } catch (Exception $e) { + $this->logger->error('Refund failed', $e); + $msg = self::EXCEPTION_MSG; + throw new LocalizedException(__($msg, $order->getIncrementId())); + } + + if (!$refundId) { + $this->logger->error('No refund ID'); + $msg = self::EXCEPTION_MSG; + throw new LocalizedException(__($msg, $order->getIncrementId())); + } + + return $refundId; + } +} diff --git a/Service/Order/RefundUpdate/RefundFailedService.php b/Service/Order/RefundUpdate/RefundFailedService.php new file mode 100644 index 0000000..17dee61 --- /dev/null +++ b/Service/Order/RefundUpdate/RefundFailedService.php @@ -0,0 +1,125 @@ +orderRepository = $orderRepository; + $this->creditmemoRepository = $creditmemoRepository; + $this->transactionService = $transactionService; + $this->logger = $logger; + } + + /** + * @param string $refundId + * @param string $failureReason + * @throws InputException + * @throws NoSuchEntityException + * @throws Exception + */ + public function handle(string $refundId, string $failureReason) + { + $prefix = "RefundFailedService $refundId"; + $this->logger->addPrefix($prefix)->debug('Start'); + + $this->transactionService + ->refundId($refundId) + ->execute(function (RefundTransactionDataInterface $transaction) use ($failureReason) { + $order = $this->orderRepository->get($transaction->getOrderId()); + $creditMemo = $this->getCreditMemo($transaction); + $this->refundOrder($order, $creditMemo); + $this->markCreditMemoRefunded($creditMemo, $failureReason); + $transaction->setRefundFailed(); + }); + + $this->logger->removePrefix($prefix); + } + + /** + * @param RefundTransactionDataInterface $transaction + * @return Creditmemo|null + */ + private function getCreditMemo(RefundTransactionDataInterface $transaction): ?Creditmemo + { + if (!$transaction->getCreditMemoId()) { + return null; + } + + $creditMemo = $this->creditmemoRepository->get($transaction->getCreditMemoId()); + + if (!$creditMemo || !$creditMemo->getEntityId()) { + return null; + } + + $this->logger->debug('Creditmemo found', $creditMemo->getEntityId()); + + return $creditMemo; + } + + /** + * @param Creditmemo $creditMemo + * @param string $failureReason + */ + private function markCreditMemoRefunded(Creditmemo $creditMemo, string $failureReason): void + { + $amount = "{$creditMemo->getBaseCurrencyCode()}{$creditMemo->getBaseGrandTotal()}"; + $creditMemo->addComment("Refund of $amount failed ($failureReason)"); + $creditMemo->setGrandTotal(0); + $creditMemo->setBaseGrandTotal(0); + $creditMemo->setState(Creditmemo::STATE_CANCELED); + + $this->creditmemoRepository->save($creditMemo); + $this->logger->debug('Creditmemo updated'); + } + + /** + * @param OrderInterface $order + * @param Creditmemo $creditMemo + */ + private function refundOrder(OrderInterface $order, Creditmemo $creditMemo): void + { + $totalRefundedOriginal = $order->getBaseTotalRefunded(); + $totalRefunded = $totalRefundedOriginal - $creditMemo->getBaseGrandTotal(); + $order->setBaseTotalRefunded($totalRefunded); + + $order->setTotalRefunded($order->getTotalRefunded() - $creditMemo->getGrandTotal()); + + $this->orderRepository->save($order); + $this->logger->debug('Order refund reversed', [ + 'refund_total_original' => $totalRefundedOriginal, + 'refund_total_new' => $totalRefunded + ]); + } +} diff --git a/Service/Order/RefundUpdate/RefundTransactionService.php b/Service/Order/RefundUpdate/RefundTransactionService.php new file mode 100644 index 0000000..e1cf297 --- /dev/null +++ b/Service/Order/RefundUpdate/RefundTransactionService.php @@ -0,0 +1,61 @@ +transactionRepository = $transactionRepository; + parent::__construct($logger); + } + + /** + * @param string $refundId + * @return $this + */ + public function refundId(string $refundId): self + { + $this->refundId = $refundId; + return $this; + } + + /** + * @return BaseTransactionDataInterface + * @throws InputException + * @throws NoSuchEntityException + */ + protected function getTransaction(): BaseTransactionDataInterface + { + return $this->transactionRepository->getByRefundUuid($this->refundId); + } + + /** + * @param BaseTransactionDataInterface $transaction + * @throws LocalizedException + */ + protected function saveTransaction(BaseTransactionDataInterface $transaction): void + { + $this->transactionRepository->save($transaction); + } +} diff --git a/Service/Transaction/GenerateToken.php b/Service/Transaction/GenerateToken.php deleted file mode 100644 index 63dde87..0000000 --- a/Service/Transaction/GenerateToken.php +++ /dev/null @@ -1,62 +0,0 @@ -transactionRepository = $transactionRepository; - $this->mathRandom = $mathRandom; - } - - /** - * @param int $quoteId - * - * @return string|null - * - * @throws LocalizedException - */ - public function execute(int $quoteId): ?string - { - try { - $transaction = $this->transactionRepository->getByQuoteId($quoteId, true); - return $transaction->getToken(); - } catch (\Exception $exception) { - $token = $this->mathRandom->getUniqueHash('trl'); - $transaction = $this->transactionRepository->create(); - $transaction->setQuoteId($quoteId)->setToken($token); - $this->transactionRepository->save($transaction); - return $token; - } - } -} diff --git a/ViewModel/Checkout/Pending.php b/ViewModel/Checkout/Pending.php deleted file mode 100644 index df545d6..0000000 --- a/ViewModel/Checkout/Pending.php +++ /dev/null @@ -1,63 +0,0 @@ -request = $request; - $this->configRepository = $configRepository; - } - - /** - * Get refresh url - * - * @return string - */ - public function getRefreshUrl(): string - { - $paymentId = $this->request->getParam('payment_id'); - return $this->configRepository->getBaseUrl() . "truelayer/checkout/process/payment_id/{$paymentId}/"; - } - - /** - * Get check url - * - * @return string - */ - public function getCheckUrl(): string - { - $token = $this->request->getParam('payment_id'); - return $this->configRepository->getBaseUrl() . "rest/V1/truelayer/check-order-placed/{$token}/"; - } -} diff --git a/composer.json b/composer.json index 66d9831..3eb3376 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "truelayer/magento2", "description": "TrueLayer extension for Magento 2", "type": "magento2-module", - "version": "1.0.11", + "version": "2.0.0", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/etc/config.xml b/etc/config.xml index f710a39..ef4ae4e 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -10,7 +10,7 @@ - 1.0.11 + 2.0.0 TrueLayerFacade TrueLayer Pay using TrueLayer diff --git a/etc/db_schema.xml b/etc/db_schema.xml index 7cbd0df..68a4132 100644 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -15,6 +15,7 @@ + @@ -26,6 +27,12 @@ referenceTable="quote" referenceColumn="entity_id" onDelete="CASCADE" /> + + + + + + @@ -36,4 +43,33 @@
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json new file mode 100644 index 0000000..2eaa929 --- /dev/null +++ b/etc/db_schema_whitelist.json @@ -0,0 +1,32 @@ +{ + "truelayer_transaction": { + "column": { + "entity_id": true, + "quote_id": true, + "order_id": true, + "token": true, + "uuid": true, + "status": true, + "invoice_uuid": true, + "payment_url": true, + "is_locked": true, + "failure_reason": true, + "created_at": true, + "updated_at": true + }, + "constraint": { + "PRIMARY": true, + "TRUELAYER_TRANSACTION_QUOTE_ID_QUOTE_ENTITY_ID": true + } + }, + "truelayer_user": { + "column": { + "entity_id": true, + "magento_email": true, + "truelayer_id": true + }, + "constraint": { + "PRIMARY": true + } + } +} \ No newline at end of file diff --git a/etc/di.xml b/etc/di.xml index 9115b26..c17cb31 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -7,41 +7,40 @@ --> - - - - - - - - - - + + + + + + + + + + - TrueLayerError - - TrueLayer\Connect\Logger\Handler\Error - + TruelayerDebugLogger + TruelayerErrorLogger - + + TrueLayerDebug - TrueLayer\Connect\Logger\Handler\Debug + TrueLayer\Connect\Service\Log\DebugHandler - + + + + + TrueLayerError + + TrueLayer\Connect\Service\Log\ErrorHandler + + + @@ -57,39 +56,14 @@ - TrueLayerInitializeCommand - TrueLayerRefundCommand - TrueLayerCancelCommand - TrueLayerCancelCommand + TrueLayerAuthorizePaymentCommand + TrueLayerRefundPaymentCommand - - - TrueLayerInitializeRequestBuilder - TrueLayer\Connect\Gateway\Http\TransferFactory - TrueLayer\Connect\Gateway\Http\Client\GenericClient - - - - - - TrueLayer\Connect\Gateway\Request\RefundRequest - TrueLayer\Connect\Gateway\Http\TransferFactory - TrueLayer\Connect\Gateway\Http\Client\GenericClient - - - - - - TrueLayer\Connect\Gateway\Request\CancelRequest - TrueLayer\Connect\Gateway\Http\TransferFactory - TrueLayer\Connect\Gateway\Http\Client\GenericClient - - - - + + @@ -131,9 +105,6 @@ - - - diff --git a/etc/events.xml b/etc/events.xml new file mode 100644 index 0000000..e456c96 --- /dev/null +++ b/etc/events.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml new file mode 100644 index 0000000..c65c376 --- /dev/null +++ b/etc/frontend/di.xml @@ -0,0 +1,15 @@ + + + + + + + \Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE + + TrueLayer_Connect::checkout/payment_error_message.phtml + + + + + + \ No newline at end of file diff --git a/etc/frontend/routes.xml b/etc/frontend/routes.xml index 27feef6..bdd87f3 100644 --- a/etc/frontend/routes.xml +++ b/etc/frontend/routes.xml @@ -11,4 +11,4 @@ - + \ No newline at end of file diff --git a/etc/webapi.xml b/etc/webapi.xml index c8076dc..ea15065 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -6,22 +6,10 @@ */ --> - - - - - - - - - - - - diff --git a/view/frontend/layout/truelayer_checkout_pending.xml b/view/frontend/layout/truelayer_checkout_process.xml similarity index 51% rename from view/frontend/layout/truelayer_checkout_pending.xml rename to view/frontend/layout/truelayer_checkout_process.xml index a34ed5f..4a4d394 100644 --- a/view/frontend/layout/truelayer_checkout_pending.xml +++ b/view/frontend/layout/truelayer_checkout_process.xml @@ -1,13 +1,9 @@ - + - - - TrueLayer\Connect\ViewModel\Checkout\Pending - - + diff --git a/view/frontend/templates/checkout/payment_error_message.phtml b/view/frontend/templates/checkout/payment_error_message.phtml new file mode 100644 index 0000000..089d3a2 --- /dev/null +++ b/view/frontend/templates/checkout/payment_error_message.phtml @@ -0,0 +1,26 @@ + + + + escapeHtml(__($block->getData('text')));?> + + + \ No newline at end of file diff --git a/view/frontend/templates/checkout/pending.phtml b/view/frontend/templates/checkout/pending.phtml deleted file mode 100644 index fc46efc..0000000 --- a/view/frontend/templates/checkout/pending.phtml +++ /dev/null @@ -1,51 +0,0 @@ - -getData('view_model'); -?> -
-

- escapeHtml(__('Your payment has been accepted by your bank.')) ?> -
- escapeHtml(__( - 'We are redirecting you to the order confirmation page but it might take an extra second.' - )) ?> -

-
-
-
-
-
-
- -
- - -
-
- - diff --git a/view/frontend/templates/checkout/process.phtml b/view/frontend/templates/checkout/process.phtml new file mode 100644 index 0000000..d30fe58 --- /dev/null +++ b/view/frontend/templates/checkout/process.phtml @@ -0,0 +1,44 @@ + + +
+

+ escapeHtml(__('Please wait, we are processing your payment'))?> +

+ +
+
+
+
+
+
+ + +
+ + + \ No newline at end of file diff --git a/view/frontend/web/css/pending.css b/view/frontend/web/css/pending.css deleted file mode 100644 index 5b8da7c..0000000 --- a/view/frontend/web/css/pending.css +++ /dev/null @@ -1,162 +0,0 @@ -.truelayer-checkout-pending .checkout-pending { - margin-top: 40px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; -} - -.truelayer-checkout-pending .checkout-pending h2 { - line-height: 1.4; - text-align: center; -} - -.truelayer-loading .loading-container, -.truelayer-loading .loading { - position: relative; - height: 130px; - width: 130px; - border-radius: 100%; -} - -.truelayer-loading .loading-container { - margin: 60px auto; -} - -.truelayer-loading .loading { - border: 3px solid transparent; - border-color: transparent #3665ab transparent #3665ab; - animation: spin 1.5s linear 0s infinite normal; - transform-origin: 50% 50%; -} - -.truelayer-loading .progress { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - font-size: 28px; - text-align: center; -} - -.truelayer-loading .progress::before { - content: '100%'; - animation: progress 9s linear; -} - -.truelayer-result { - margin: 60px 0; - text-align: center; - font-size: 18px; - letter-spacing: .5px; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg);} -} - -@keyframes progress { - 0% { content: '0%'; } - 1% { content: '1%'; } - 2% { content: '2%'; } - 3% { content: '3%'; } - 4% { content: '4%'; } - 5% { content: '5%'; } - 6% { content: '6%'; } - 7% { content: '7%'; } - 8% { content: '8%'; } - 9% { content: '9%'; } - 10% { content: '10%'; } - 11% { content: '11%'; } - 12% { content: '12%'; } - 13% { content: '13%'; } - 14% { content: '14%'; } - 15% { content: '15%'; } - 16% { content: '16%'; } - 17% { content: '17%'; } - 18% { content: '18%'; } - 19% { content: '19%'; } - 20% { content: '20%'; } - 21% { content: '21%'; } - 22% { content: '22%'; } - 23% { content: '23%'; } - 24% { content: '24%'; } - 25% { content: '25%'; } - 26% { content: '26%'; } - 27% { content: '27%'; } - 28% { content: '28%'; } - 29% { content: '29%'; } - 30% { content: '30%'; } - 31% { content: '31%'; } - 32% { content: '32%'; } - 33% { content: '33%'; } - 34% { content: '34%'; } - 35% { content: '35%'; } - 36% { content: '36%'; } - 37% { content: '37%'; } - 38% { content: '38%'; } - 39% { content: '39%'; } - 40% { content: '40%'; } - 41% { content: '41%'; } - 42% { content: '42%'; } - 43% { content: '43%'; } - 44% { content: '44%'; } - 45% { content: '45%'; } - 46% { content: '46%'; } - 47% { content: '47%'; } - 48% { content: '48%'; } - 49% { content: '49%'; } - 50% { content: '50%'; } - 51% { content: '51%'; } - 52% { content: '52%'; } - 53% { content: '53%'; } - 54% { content: '54%'; } - 55% { content: '55%'; } - 56% { content: '56%'; } - 57% { content: '57%'; } - 58% { content: '58%'; } - 59% { content: '59%'; } - 60% { content: '60%'; } - 61% { content: '61%'; } - 62% { content: '62%'; } - 63% { content: '63%'; } - 64% { content: '64%'; } - 65% { content: '65%'; } - 66% { content: '66%'; } - 67% { content: '67%'; } - 68% { content: '68%'; } - 69% { content: '69%'; } - 70% { content: '70%'; } - 71% { content: '71%'; } - 72% { content: '72%'; } - 73% { content: '73%'; } - 74% { content: '74%'; } - 75% { content: '75%'; } - 76% { content: '76%'; } - 77% { content: '77%'; } - 78% { content: '78%'; } - 79% { content: '79%'; } - 80% { content: '80%'; } - 81% { content: '81%'; } - 82% { content: '82%'; } - 83% { content: '83%'; } - 84% { content: '84%'; } - 85% { content: '85%'; } - 86% { content: '86%'; } - 87% { content: '87%'; } - 88% { content: '88%'; } - 89% { content: '89%'; } - 90% { content: '90%'; } - 91% { content: '91%'; } - 92% { content: '92%'; } - 93% { content: '93%'; } - 94% { content: '94%'; } - 95% { content: '95%'; } - 96% { content: '96%'; } - 97% { content: '97%'; } - 98% { content: '98%'; } - 99% { content: '99%'; } - 100% { content: '100%'; } -} \ No newline at end of file diff --git a/view/frontend/web/css/process.css b/view/frontend/web/css/process.css new file mode 100644 index 0000000..bbce5c0 --- /dev/null +++ b/view/frontend/web/css/process.css @@ -0,0 +1,53 @@ +.truelayer-checkout-pending { + margin-top: 50px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; +} + +.truelayer-checkout-pending h2 { + line-height: 1.4; + text-align: center; +} + +.truelayer-loading .loading-container, +.truelayer-loading .loading { + position: relative; + height: 100px; + width: 100px; + border-radius: 100%; +} + +.truelayer-loading .loading-container { + margin: 60px auto; +} + +.truelayer-loading .loading { + border: 3px solid transparent; + border-color: transparent #3665ab transparent #3665ab; + animation: truelayer-spin 1.5s linear 0s infinite normal; + transform-origin: 50% 50%; +} + +.truelayer-loading .progress { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + font-size: 28px; + text-align: center; +} + +.truelayer-result { + margin: 60px 0; + text-align: center; + font-size: 18px; + letter-spacing: .5px; +} + +@keyframes truelayer-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg);} +} \ No newline at end of file diff --git a/view/frontend/web/js/cart_refresh.js b/view/frontend/web/js/cart_refresh.js new file mode 100644 index 0000000..93ed8cc --- /dev/null +++ b/view/frontend/web/js/cart_refresh.js @@ -0,0 +1,18 @@ +/** + * Copyright © TrueLayer Ltd, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define(['Magento_Customer/js/customer-data', 'uiComponent'], function (customerData, Component) { + 'use strict'; + + return Component.extend({ + initialize() { + this._super(); + + var sections = ['cart', 'checkout-data']; + customerData.invalidate(sections); + customerData.reload(sections, true); + }, + }); +}); \ No newline at end of file diff --git a/view/frontend/web/js/pending.js b/view/frontend/web/js/pending.js index c6c3f36..94c0632 100644 --- a/view/frontend/web/js/pending.js +++ b/view/frontend/web/js/pending.js @@ -3,34 +3,50 @@ * See COPYING.txt for license details. */ -define([], function () { +define(['jquery', 'mage/url', 'ko', 'uiComponent'], function ($, url, ko, Component) { 'use strict'; - return function (data) { - let success = document.querySelector('[data-success]'), - error = document.querySelector('[data-error]'), - loader = document.querySelector('[data-loader]'), - count = 0, - interval = setInterval(() => { getRequest() }, 2500); + return Component.extend({ + defaults: { + isLoading: ko.observable(true), + isError: ko.observable(false), + requestCount: ko.observable(0), + maxRequestCount: 15, + statusUrl: url.build('/truelayer/checkout/status'), + isRedirecting: false, + }, - function getRequest() { - fetch(data.checkUrl) - .then((res) => { - count++; - if (!res.ok) throw new Error(); - displayRequstResult(success); - window.location.replace(data.refreshUrl); - }) - .catch(() => { - if (count === 3) displayRequstResult(error); - }); - } + initialize() { + this._super(); + this.checkStatus(); + }, + + checkStatus() { + if (this.requestCount() >= this.maxRequestCount) { + this.isError(true); + this.isLoading(false); + return; + } + + this.requestCount(this.requestCount() + 1); - // Element - HTML element - function displayRequstResult(element) { - clearInterval(interval); - loader.setAttribute('style', 'display: none;'); - element.removeAttribute('style'); + $.ajax({ + url: this.statusUrl + window.location.search + '&attempt=' + this.requestCount(), + type: 'POST', + dataType: 'json', + contentType: 'application/json', + success: (data) => { + if (data && data.redirect) { + this.isRedirecting = true; + window.location.replace(data.redirect); + } + }, + complete: () => { + if (!this.isRedirecting) { + setTimeout(this.checkStatus.bind(this), this.requestCount() * 2000); + } + } + }) } - }; -}); + }); +}); \ No newline at end of file diff --git a/view/frontend/web/js/view/payment/method-renderer/truelayer.js b/view/frontend/web/js/view/payment/method-renderer/truelayer.js index 71b9164..ae27c0e 100644 --- a/view/frontend/web/js/view/payment/method-renderer/truelayer.js +++ b/view/frontend/web/js/view/payment/method-renderer/truelayer.js @@ -6,24 +6,16 @@ /*global define*/ define( [ - 'jquery', 'Magento_Checkout/js/view/payment/default', - 'Magento_Checkout/js/model/error-processor', - 'Magento_Checkout/js/model/quote', - 'Magento_Customer/js/model/customer', - 'Magento_Checkout/js/model/url-builder', - 'Magento_Checkout/js/model/full-screen-loader', - 'mage/storage', - 'Magento_Ui/js/model/messageList', - 'Magento_Checkout/js/model/payment/additional-validators', - 'uiRegistry' + 'mage/url', + 'Magento_Checkout/js/action/redirect-on-success', ], - function ($, Component, errorProcessor, quote, customer, urlBuilder, fullScreenLoader, storage, messageList, additionalValidators, uiRegistry) { + function (Component, url, redirectOnSuccess) { 'use strict'; - var payload = ''; - return Component.extend({ + redirectAfterPlaceOrder: true, + defaults: { template: 'TrueLayer_Connect/payment/truelayer' }, @@ -32,81 +24,8 @@ define( return 'truelayer'; }, - placeOrder: function (data, event) { - if (event) { - event.preventDefault(); - } - - this.isPlaceOrderActionAllowed(false); - var _this = this; - - if (additionalValidators.validate()) { - fullScreenLoader.startLoader(); - _this._placeOrder(); - } - }, - - _placeOrder: function () { - return this.setPaymentInformation().done(function () { - this.orderRequest(customer.isLoggedIn(), quote.getQuoteId()); - }.bind(this)); - }, - - setPaymentInformation: function() { - var serviceUrl, payload; - - payload = { - cartId: quote.getQuoteId(), - billingAddress: quote.billingAddress(), - paymentMethod: this.getData() - }; - - if (customer.isLoggedIn()) { - serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); - } else { - payload.email = quote.guestEmail; - serviceUrl = urlBuilder.createUrl('/guest-carts/:quoteId/set-payment-information', { - quoteId: quote.getQuoteId() - }); - } - - return storage.post( - serviceUrl, JSON.stringify(payload) - ); - }, - - orderRequest: function(isLoggedIn, cartId) { - var url = 'rest/V1/truelayer/order-request'; - - payload = { - isLoggedIn: isLoggedIn, - cartId: cartId, - paymentMethod: this.getData() - }; - - storage.post( - url, - JSON.stringify(payload) - ).done(function (response) { - if (response[0].success) { - fullScreenLoader.stopLoader(); - window.location.replace(response[0].payment_page_url); - } else { - fullScreenLoader.stopLoader(); - this.addError(response[0].message); - } - }.bind(this)); - }, - - /** - * Adds error message - * - * @param {String} message - */ - addError: function (message) { - messageList.addErrorMessage({ - message: message - }); + afterPlaceOrder: function() { + redirectOnSuccess.redirectUrl = url.build('truelayer/checkout/redirect'); }, }); }