From 40da1dde6f1080e4f74862da24bf6ae8aa70b634 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Fri, 20 Mar 2026 10:00:18 +0100 Subject: [PATCH 01/18] wip --- Block/Customer/GooglePay/PaymentInfo.php | 28 +++ Block/GooglePay/Payment.php | 8 + Controller/GooglePay/Cancel.php | 119 +++++++++++++ Controller/GooglePay/Form/Data.php | 162 ++++++++++++++++++ Controller/GooglePay/Payment.php | 21 +++ Controller/GooglePay/Success.php | 145 ++++++++++++++++ .../GooglePay/CreateRequestCommand.php | 61 +++++++ .../Command/GooglePay/InitializeCommand.php | 78 +++++++++ Gateway/Config/GooglePay.php | 50 ++++++ Gateway/Http/Client.php | 4 +- .../PaymentCreateTransferFactory.php | 2 +- .../Request/GooglePay/OrderDetailsBuilder.php | 111 ++++++++++++ .../GooglePay/PaymentCreateHandler.php | 42 +++++ Model/Crypto/Components/Digest.php | 2 +- ViewModel/GooglePayConfig.php | 66 +++++++ etc/adminhtml/system.xml | 65 +++++++ etc/config.xml | 24 +++ etc/di.xml | 105 ++++++++++++ view/frontend/layout/checkout_index_index.xml | 8 + .../monripayments_googlepay_payment.xml | 20 +++ .../templates/googlepay/payment.phtml | 16 ++ .../web/js/view/init/googlepay-init.js | 81 +++++++++ .../view/method-renderer/monri_google_pay.js | 55 ++++++ view/frontend/web/js/view/monri_google_pay.js | 28 +++ view/frontend/web/template/google_pay.html | 42 +++++ 25 files changed, 1340 insertions(+), 3 deletions(-) create mode 100644 Block/Customer/GooglePay/PaymentInfo.php create mode 100644 Block/GooglePay/Payment.php create mode 100644 Controller/GooglePay/Cancel.php create mode 100644 Controller/GooglePay/Form/Data.php create mode 100644 Controller/GooglePay/Payment.php create mode 100644 Controller/GooglePay/Success.php create mode 100644 Gateway/Command/GooglePay/CreateRequestCommand.php create mode 100644 Gateway/Command/GooglePay/InitializeCommand.php create mode 100644 Gateway/Config/GooglePay.php create mode 100644 Gateway/Request/GooglePay/OrderDetailsBuilder.php create mode 100644 Gateway/Response/GooglePay/PaymentCreateHandler.php create mode 100644 ViewModel/GooglePayConfig.php create mode 100644 view/frontend/layout/monripayments_googlepay_payment.xml create mode 100644 view/frontend/templates/googlepay/payment.phtml create mode 100644 view/frontend/web/js/view/init/googlepay-init.js create mode 100644 view/frontend/web/js/view/method-renderer/monri_google_pay.js create mode 100644 view/frontend/web/js/view/monri_google_pay.js create mode 100644 view/frontend/web/template/google_pay.html diff --git a/Block/Customer/GooglePay/PaymentInfo.php b/Block/Customer/GooglePay/PaymentInfo.php new file mode 100644 index 0000000..8f22d0a --- /dev/null +++ b/Block/Customer/GooglePay/PaymentInfo.php @@ -0,0 +1,28 @@ +checkoutSession->getData('last_order_id'); + + if (!$orderId) { + $log['errors'][] = 'Missing order ID field.'; + throw new InputException(__('Missing fields.')); + } + + $order = $this->orderRepository->get($orderId); + + $payment = $order->getPayment(); + + $this->commandManager->executeByCode('create_request', $payment); + + return $payment->getAdditionalInformation( + PaymentCreateHandler::INITIAL_DATA + ); + } +} diff --git a/Block/GooglePay/Payment.php b/Block/GooglePay/Payment.php new file mode 100644 index 0000000..d226d4d --- /dev/null +++ b/Block/GooglePay/Payment.php @@ -0,0 +1,8 @@ + + */ + +namespace Monri\Payments\Controller\GooglePay; + +use Exception; +use Magento\Checkout\Model\Session; +use Magento\Framework\App\Action\Context; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Payment\Gateway\Command\CommandManagerInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\Method\Logger; +use Magento\Sales\Model\OrderRepository; +use Monri\Payments\Controller\AbstractGatewayResponse; +use Monri\Payments\Model\GetOrderIdByIncrement; + +/** + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ +class Cancel extends AbstractGatewayResponse +{ + /** + * @var Session + */ + private $checkoutSession; + /** + * @var Logger + */ + private $logger; + + /** + * Cancel constructor. + * + * @param Context $context + * @param OrderRepository $orderRepository + * @param CommandManagerInterface $commandManager + * @param GetOrderIdByIncrement $getOrderIdByIncrement + * @param Session $checkoutSession + * @param Logger $logger + */ + public function __construct( + Context $context, + OrderRepository $orderRepository, + CommandManagerInterface $commandManager, + GetOrderIdByIncrement $getOrderIdByIncrement, + Session $checkoutSession, + Logger $logger + ) { + parent::__construct($context, $orderRepository, $commandManager, $getOrderIdByIncrement); + + $this->checkoutSession = $checkoutSession; + $this->logger = $logger; + } + + /** + * Cancels an order. + * + * @return Redirect|void + */ + public function execute() + { + $log = [ + 'location' => __METHOD__, + 'errors' => [], + 'success' => true, + ]; + + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + + try { + $order = $this->getOrderById( + $this->checkoutSession->getData('last_order_id') + ); + + $gatewayResponse = $this->getRequest()->getParams(); + $log['payload'] = $gatewayResponse; + + + /** @var InfoInterface $payment */ + $payment = $order->getPayment(); + + $gatewayResponse['status'] = 'declined'; + + $result = $this->processGatewayResponse($gatewayResponse, $payment, ['disabled' => true]); + + if (isset($result['message'])) { + $log['errors'][] = 'Error processing payment: ' . $result['message']; + $this->messageManager->addNoticeMessage(__('The payment has been denied: %1', $result['message'])); + } else { + $log['errors'][] = 'Error processing payment.'; + $this->messageManager->addNoticeMessage(__('The payment has been denied.')); + } + } catch (InputException | NoSuchEntityException | NotFoundException $e) { + $log['errors'][] = 'Caught exception: ' . $e->getMessage(); + $log['success'] = false; + $this->messageManager->addNoticeMessage(__('Order not found.')); + } catch (Exception $e) { + $log['errors'][] = 'Caught unexpected exception: ' . $e->getMessage(); + $log['success'] = false; + $this->messageManager->addNoticeMessage(__('Error processing payment, please try again later.')); + } finally { + $this->checkoutSession->restoreQuote(); + $this->logger->debug($log); + } + + return $resultRedirect->setPath('checkout/cart'); + } +} diff --git a/Controller/GooglePay/Form/Data.php b/Controller/GooglePay/Form/Data.php new file mode 100644 index 0000000..fd59921 --- /dev/null +++ b/Controller/GooglePay/Form/Data.php @@ -0,0 +1,162 @@ + + */ + +namespace Monri\Payments\Controller\GooglePay\Form; + +use Exception; +use Magento\Checkout\Model\Session; +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Payment\Gateway\Command\CommandException; +use Magento\Payment\Gateway\Command\CommandManagerInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\Method\Logger; +use Magento\Sales\Model\OrderRepository; +use Monri\Payments\Gateway\Config\GooglePay as Config; +use Monri\Payments\Gateway\Response\Components\PaymentCreateHandler; + +/** + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ +class Data extends Action +{ + /** + * @var Session + */ + private $checkoutSession; + + /** + * @var OrderRepository + */ + private $orderRepository; + + /** + * @var CommandManagerInterface + */ + private $commandManager; + + /** + * @var Config + */ + private $config; + + /** + * @var Logger + */ + private $logger; + + /** + * Data constructor. + * + * @param Context $context + * @param Session $checkoutSession + * @param OrderRepository $orderRepository + * @param Config $config + * @param CommandManagerInterface $commandManager + * @param Logger $logger + */ + public function __construct( + Context $context, + Session $checkoutSession, + OrderRepository $orderRepository, + Config $config, + CommandManagerInterface $commandManager, + Logger $logger + ) { + $this->commandManager = $commandManager; + $this->checkoutSession = $checkoutSession; + $this->orderRepository = $orderRepository; + $this->config = $config; + $this->logger = $logger; + + parent::__construct($context); + } + + /** + * Generate form data that will be used for the redirect to the gateway + * + * @return ResultInterface|ResponseInterface + */ + public function execute() + { + $log = [ + 'location' => __METHOD__, + 'errors' => [], + 'success' => true, + 'payload' => [] + ]; + + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + + try { + $orderId = $this->checkoutSession->getData('last_order_id'); + + if (!$orderId) { + $log['errors'][] = 'Missing order ID field.'; + throw new InputException(__('Missing fields.')); + } + + $order = $this->orderRepository->get($orderId); + + /** @var InfoInterface $payment */ + $payment = $order->getPayment(); + + $this->commandManager->executeByCode('create_request', $payment); + + $result = $payment->getAdditionalInformation(PaymentCreateHandler::INITIAL_DATA); + $storeId = $order->getStoreId(); + + $resultJson->setData([ + 'payload' => $result, + 'url' => $this->config->getGatewayPaymentCreateURL($storeId), + 'componentsJsUrl' => $this->config->getComponentsJsURL($storeId), + 'isTest' => $this->config->getIsSandboxMode($storeId), + 'error' => null, + 'authenticityToken' => $this->config->getClientAuthenticityToken($storeId), + ]); + + $log['payload'] = $result; + } catch (InputException | NoSuchEntityException | CommandException $e) { + $resultJson->setData([ + 'payload' => [], + 'url' => '', + 'error' => __('Error processing payment, please try again later.') + ]); + + $log['errors'][] = 'Exception caught: ' . $e->getMessage(); + $log['success'] = false; + + $resultJson->setHttpResponseCode(400); + return $resultJson; + } catch (Exception $e) { + $resultJson->setData([ + 'payload' => [], + 'url' => '', + 'error' => __('Error processing payment, please try again later.') + ]); + + $log['errors'][] = 'Unexpected exception caught: ' . $e->getMessage(); + $log['success'] = false; + + $resultJson->setHttpResponseCode(500); + return $resultJson; + } finally { + $this->logger->debug($log); + } + + return $resultJson; + } +} diff --git a/Controller/GooglePay/Payment.php b/Controller/GooglePay/Payment.php new file mode 100644 index 0000000..ad6a546 --- /dev/null +++ b/Controller/GooglePay/Payment.php @@ -0,0 +1,21 @@ +resultPageFactory = $resultPageFactory; + } + + public function execute() + { + return $this->resultPageFactory->create(); + } +} diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php new file mode 100644 index 0000000..0b49295 --- /dev/null +++ b/Controller/GooglePay/Success.php @@ -0,0 +1,145 @@ + + */ + +namespace Monri\Payments\Controller\GooglePay; + +use Exception; +use Magento\Checkout\Model\Session; +use Magento\Framework\App\Action\Context; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Payment\Gateway\Command\CommandManagerInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\Method\Logger; +use Magento\Sales\Model\OrderRepository; +use Monri\Payments\Controller\AbstractGatewayResponse; +use Monri\Payments\Gateway\Exception\TransactionAlreadyProcessedException; +use Monri\Payments\Model\GetOrderIdByIncrement; + +/** + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ +class Success extends AbstractGatewayResponse +{ + + /** + * @var Session + */ + private $checkoutSession; + /** + * @var Logger + */ + private $logger; + + /** + * Success constructor. + * + * @param Context $context + * @param OrderRepository $orderRepository + * @param CommandManagerInterface $commandManager + * @param GetOrderIdByIncrement $getOrderIdByIncrement + * @param Session $checkoutSession + * @param Logger $logger + */ + public function __construct( + Context $context, + OrderRepository $orderRepository, + CommandManagerInterface $commandManager, + GetOrderIdByIncrement $getOrderIdByIncrement, + Session $checkoutSession, + Logger $logger + ) { + parent::__construct($context, $orderRepository, $commandManager, $getOrderIdByIncrement); + + $this->checkoutSession = $checkoutSession; + $this->logger = $logger; + } + + /** + * Updates the status of an order. + * + * @return Redirect + */ + public function execute() + { + $log = [ + 'location' => __METHOD__, + 'errors' => [], + 'success' => true, + ]; + + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + + try { + $order = $this->getOrderById( + $this->checkoutSession->getData('last_order_id') + ); + + /** @var InfoInterface $payment */ + $payment = $order->getPayment(); + + $gatewayResponse = $this->getRequest()->getParams(); + $log['payload'] = $gatewayResponse; + $gatewayResponse['status'] = 'approved'; + + $digestData = $this->getDigestData(); + + $result = $this->processGatewayResponse($gatewayResponse, $payment, $digestData); + + if (isset($result['response_code_message'])) { + $this->messageManager->addNoticeMessage( + __('The payment has been accepted: %1', $result['response_code_message']) + ); + } else { + $this->messageManager->addNoticeMessage(__('The payment has been accepted.')); + } + } catch (TransactionAlreadyProcessedException | AlreadyExistsException $e) { + $log['errors'][] = 'Already processed: ' . $e->getMessage(); + $log['success'] = true; + } catch (InputException | NoSuchEntityException $e) { + $log['errors'][] = 'Exception caught: ' . $e->getMessage(); + $log['success'] = false; + $this->messageManager->addNoticeMessage(__('Order not found.')); + + return $resultRedirect->setPath('checkout/cart'); + } catch (Exception $e) { + $log['errors'][] = 'Unexpected exception caught: ' . $e->getMessage(); + $log['success'] = false; + $this->messageManager->addNoticeMessage(__('Error processing payment, please try again later.')); + + return $resultRedirect->setPath('checkout/cart'); + } finally { + $this->logger->debug($log); + } + + return $resultRedirect->setPath('checkout/onepage/success'); + } + + /** + * Resolve digest data from url + * + * @return array + */ + protected function getDigestData() + { + $digest = $this->getRequest()->getParam('digest'); + $url = $this->_url->getCurrentUrl(); + + $data = str_replace('&digest=' . $digest, '', $url); + + return [ + 'digest' => $digest, + 'digest_data' => $data, + ]; + } +} diff --git a/Gateway/Command/GooglePay/CreateRequestCommand.php b/Gateway/Command/GooglePay/CreateRequestCommand.php new file mode 100644 index 0000000..12febc8 --- /dev/null +++ b/Gateway/Command/GooglePay/CreateRequestCommand.php @@ -0,0 +1,61 @@ + + */ + +namespace Monri\Payments\Gateway\Command\GooglePay; + +use Magento\Payment\Gateway\Command; +use Magento\Payment\Gateway\Command\Result\ArrayResultFactory; +use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Request\BuilderInterface; + +class CreateRequestCommand implements CommandInterface +{ + /** + * @var BuilderInterface + */ + private $requestBuilder; + + /** + * @method Command\Result\ArrayResult create(array $params) + * @var ArrayResultFactory + */ + private $resultFactory; + + /** + * CreateRequestCommand constructor. + * + * @param BuilderInterface $builder + * @param ArrayResultFactory $resultFactory + */ + public function __construct( + BuilderInterface $builder, + ArrayResultFactory $resultFactory + ) { + $this->requestBuilder = $builder; + $this->resultFactory = $resultFactory; + } + + /** + * Builds the data object for redirect. + * + * @param array $commandSubject + * @return null|Command\ResultInterface + */ + public function execute(array $commandSubject) + { + $requestData = $this->requestBuilder->build($commandSubject); + + /** @var Command\Result\ArrayResult $result */ + $result = $this->resultFactory->create([ + 'array' => $requestData + ]); + + return $result; + } +} diff --git a/Gateway/Command/GooglePay/InitializeCommand.php b/Gateway/Command/GooglePay/InitializeCommand.php new file mode 100644 index 0000000..a51f14e --- /dev/null +++ b/Gateway/Command/GooglePay/InitializeCommand.php @@ -0,0 +1,78 @@ + + */ + +namespace Monri\Payments\Gateway\Command\GooglePay; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Helper\ContextHelper; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Model\Method\Logger; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Monri\Payments\Gateway\Config\GooglePay as Config; + +class InitializeCommand implements CommandInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @var Logger + */ + private $logger; + + /** + * InitializeCommand constructor. + * + * @param Config $config + * @param Logger $logger + */ + public function __construct( + Config $config, + Logger $logger + ) { + $this->config = $config; + $this->logger = $logger; + } + + /** + * Initializes the payment. + * + * @param array $commandSubject + */ + public function execute(array $commandSubject) + { + $stateObject = SubjectReader::readStateObject($commandSubject); + $paymentDataObject = SubjectReader::readPayment($commandSubject); + + /** @var Payment $payment */ + $payment = $paymentDataObject->getPayment(); + ContextHelper::assertOrderPayment($payment); + + $payment->setAmountOrdered($payment->getOrder()->getTotalDue()); + $payment->setBaseAmountOrdered($payment->getOrder()->getBaseTotalDue()); + $payment->getOrder()->setCanSendNewEmailFlag(false); + + try { + $payment->setAdditionalInformation( + 'transaction_type', + 'purchase' + ); + } catch (LocalizedException $e) { + $this->logger->debug(['Failed to set transaction type for payment: ' . $e->getMessage()]); + } + + $stateObject->setData(OrderInterface::STATE, Order::STATE_PENDING_PAYMENT); + $stateObject->setData(OrderInterface::STATUS, Order::STATE_PENDING_PAYMENT); + } +} diff --git a/Gateway/Config/GooglePay.php b/Gateway/Config/GooglePay.php new file mode 100644 index 0000000..611e28a --- /dev/null +++ b/Gateway/Config/GooglePay.php @@ -0,0 +1,50 @@ +getGatewayResourceURL('v2/payment/new', $storeId); + } + + /** + * Get components javascript url + * + * @param null|int $storeId + * @return string + */ + public function getComponentsJsURL($storeId = null) + { + return $this->getGatewayResourceURL('dist/components.js', $storeId); + } + + /** + * Get configured payment action + * + * @param int|null $storeId + * @return string + */ + public function getPaymentAction($storeId = null) + { + return $this->getValue(self::PAYMENT_ACTION, $storeId); + } + + public function getIsTestMode($storeId = null) { + return $this->getIsSandboxMode( $storeId ) === true ? 'test' : 'prod'; + } +} diff --git a/Gateway/Http/Client.php b/Gateway/Http/Client.php index 71effb3..ffda018 100644 --- a/Gateway/Http/Client.php +++ b/Gateway/Http/Client.php @@ -117,7 +117,9 @@ public function placeRequest(TransferInterface $transferObject) $log['errors'][] = 'Exception caught: ' . $e->getMessage(); $log['success'] = false; $this->logger->debug($log); - throw $e; + throw new \Magento\Framework\Exception\LocalizedException( + __('Payment initialization failed. Please try again.') + ); } $this->logger->debug($log); diff --git a/Gateway/Http/Components/PaymentCreateTransferFactory.php b/Gateway/Http/Components/PaymentCreateTransferFactory.php index b603768..ea5493a 100644 --- a/Gateway/Http/Components/PaymentCreateTransferFactory.php +++ b/Gateway/Http/Components/PaymentCreateTransferFactory.php @@ -14,7 +14,7 @@ use Magento\Payment\Gateway\Http\TransferFactoryInterface; use Magento\Payment\Gateway\Http\TransferInterface; use Monri\Payments\Model\Crypto\Components\Digest; -use Monri\Payments\Gateway\Config\Components as Config; +use Monri\Payments\Gateway\Config\GooglePay as Config; class PaymentCreateTransferFactory implements TransferFactoryInterface { diff --git a/Gateway/Request/GooglePay/OrderDetailsBuilder.php b/Gateway/Request/GooglePay/OrderDetailsBuilder.php new file mode 100644 index 0000000..e5ce74c --- /dev/null +++ b/Gateway/Request/GooglePay/OrderDetailsBuilder.php @@ -0,0 +1,111 @@ + + */ + +namespace Monri\Payments\Gateway\Request\GooglePay; + +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Event\ManagerInterface; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Monri\Payments\Helper\Formatter; +use Magento\Framework\UrlInterface; + +class OrderDetailsBuilder implements BuilderInterface +{ + public const ORDER_INFO_FIELD = 'order_info'; + + public const ORDER_NUMBER_FIELD = 'order_number'; + + public const AMOUNT_FIELD = 'amount'; + + public const CURRENCY_FIELD = 'currency'; + + public const TRANSACTION_TYPE_FIELD = 'transaction_type'; + + public const IP_ADDRESS_FIELD = 'ip'; + + /** + * OrderDetailsBuilder constructor. + * + * @param Formatter $formatter + * @param ManagerInterface $eventManager + * @param DataObjectFactory $dataObjectFactory + * @param UrlInterface $urlBuilder + */ + public function __construct( + private Formatter $formatter, + private ManagerInterface $eventManager, + private DataObjectFactory $dataObjectFactory, + ) {} + + /** + * Builds the order details object + * + * @param array $buildSubject + * @return array + */ + public function build(array $buildSubject) + { + $paymentDataObject = SubjectReader::readPayment($buildSubject); + + /** @var \Magento\Payment\Gateway\Data\Quote\QuoteAdapter $order */ + $order = $paymentDataObject->getOrder(); + + $orderNumber = $this->formatter->formatText($order->getOrderIncrementId() . uniqid('-'), 40); + + $orderIpAddress = $paymentDataObject->getPayment()->getOrder()->getRemoteIp(); + + $paymentDataObject->getPayment()->setAdditionalInformation(self::ORDER_NUMBER_FIELD, $orderNumber); + + $orderInfo = __('Order: %1', $order->getOrderIncrementId())->render(); + + $transportObject = $this->dataObjectFactory->create([ + 'data' => [ + 'description' => $orderInfo + ] + ]); + + // For custom order descriptions + $this->eventManager->dispatch('monri_payments_order_description_after', [ + 'order' => $order, + 'payment' => $paymentDataObject->getPayment(), + 'transportObject' => $transportObject + ]); + + $orderInfo = $this->formatter->formatText($transportObject->getData('description'), 100); + + /* + Added in 2.4.8, because \PayPal\Braintree\Gateway\Data\Order\OrderAdapter puts themselves as preference for + \Magento\Payment\Gateway\Data\Order\OrderAdapter. It declares strict types, but getGrandTotalAmount returns + string instead of float, causing it to break execution. + */ + try { + $orderAmount = $this->formatter->formatPrice( + $order->getGrandTotalAmount() + ); + } catch (\TypeError $e) { + $payment = $paymentDataObject->getPayment(); + $orderObject = $payment->getOrder(); + $orderAmount = $this->formatter->formatPrice( + $orderObject->getBaseGrandTotal() + ); + } + + $currencyCode = $order->getCurrencyCode(); + + return [ + self::ORDER_INFO_FIELD => $orderInfo, + self::ORDER_NUMBER_FIELD => $orderNumber, + self::AMOUNT_FIELD => $orderAmount, + self::CURRENCY_FIELD => $currencyCode, + self::IP_ADDRESS_FIELD => $orderIpAddress, + self::TRANSACTION_TYPE_FIELD => 'purchase', + ]; + } +} diff --git a/Gateway/Response/GooglePay/PaymentCreateHandler.php b/Gateway/Response/GooglePay/PaymentCreateHandler.php new file mode 100644 index 0000000..f659003 --- /dev/null +++ b/Gateway/Response/GooglePay/PaymentCreateHandler.php @@ -0,0 +1,42 @@ + + */ + +namespace Monri\Payments\Gateway\Response\GooglePay; + +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Quote\Model\Quote\Payment; + +class PaymentCreateHandler implements HandlerInterface +{ + public const INITIAL_DATA = 'initial_payment_data'; + + /** + * @inheritDoc + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = SubjectReader::readPayment($handlingSubject); + + /** @var Payment $payment */ + $payment = $paymentDO->getPayment(); + $order = $payment->getOrder(); + $billing = $order->getBillingAddress(); + + $response['ch_full_name'] = trim($billing->getFirstname() . ' ' . $billing->getLastname()); + $response['ch_address'] = $billing->getStreetLine(1) ?? ''; + $response['ch_city'] = $billing->getCity() ?? ''; + $response['ch_zip'] = $billing->getPostcode() ?? ''; + $response['ch_country'] = $billing->getCountryId() ?? ''; + $response['ch_phone'] = $billing->getTelephone() ?? ''; + $response['ch_email'] = $order->getCustomerEmail() ?? ''; + $response['orderInfo'] = 'Magento Order'; + $payment->setAdditionalInformation(self::INITIAL_DATA, $response); + } +} diff --git a/Model/Crypto/Components/Digest.php b/Model/Crypto/Components/Digest.php index 75e7ed9..8c47edb 100644 --- a/Model/Crypto/Components/Digest.php +++ b/Model/Crypto/Components/Digest.php @@ -9,7 +9,7 @@ namespace Monri\Payments\Model\Crypto\Components; -use Monri\Payments\Gateway\Config\Components as Config; +use Monri\Payments\Gateway\Config; class Digest { diff --git a/ViewModel/GooglePayConfig.php b/ViewModel/GooglePayConfig.php new file mode 100644 index 0000000..61b9a26 --- /dev/null +++ b/ViewModel/GooglePayConfig.php @@ -0,0 +1,66 @@ +checkoutSession->getData('last_order_id'); + if (!$orderId) { + throw new InputException(__('Missing fields.')); + } + + $order = $this->orderRepository->get($orderId); + /** @var InfoInterface $payment */ + $payment = $order->getPayment(); + + // direct execution, no pool needed + $this->googlePayCommand->execute(['payment' => $this->paymentDataObjectFactory->create($payment)]); + + $payload = $payment->getAdditionalInformation( + PaymentCreateHandler::INITIAL_DATA + ); + + $storeId = $order->getStoreId(); + + return [ + 'payload' => $payload, + 'gatewayUrl' => $this->config->getGatewayPaymentCreateURL($storeId), + 'componentsJsUrl' => $this->config->getComponentsJsURL($storeId), + 'authenticityToken' => $this->config->getClientAuthenticityToken($storeId), + 'isTest' => $this->config->getIsSandboxMode($storeId), + ]; + } catch (Exception $e) { + $this->logger->debug(['GooglePay initialization failed: ' . $e->getMessage()]); + + return [ + 'error' => true, + 'message' => __('Google Pay is currently unavailable.') + ]; + } + + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 5bec32f..7e101a0 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -173,6 +173,71 @@ validate-number + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + + + Magento\Config\Model\Config\Backend\Encrypted + + + + + Magento\Config\Model\Config\Backend\Encrypted + + + + + \Monri\Payments\Block\Adminhtml\Config\Source\Components\Languages + + + + Magento\Config\Model\Config\Source\Yesno + + + + Magento\Config\Model\Config\Source\Yesno + + + Monri\Payments\Block\Adminhtml\Config\DownloadLog + + 1 + + + + + Magento\Payment\Model\Config\Source\Allspecificcountries + + + + Magento\Directory\Model\Config\Source\Country + 1 + + + + validate-number + + diff --git a/etc/config.xml b/etc/config.xml index d292527..0eda835 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -72,6 +72,30 @@ + + + 0 + Monri Google Pay + authorize + + + + + + + MonriGooglePayFacade + 0 + 1 + 1 + 0 + 1 + 1 + 1 + 1 + 1 + + + 0 diff --git a/etc/di.xml b/etc/di.xml index ba9ff5d..20046b0 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -678,6 +678,111 @@ + + + Monri\Payments\Gateway\Config\GooglePay::CODE + Magento\Payment\Block\Form + Magento\Payment\Block\Info + MonriGooglePayValueHandlerPool + MonriGooglePayValidatorPool + MonriGooglePayCommandPool + + + + + + + MonriGooglePayConfigHandler + + + + + + + Monri\Payments\Gateway\Config\GooglePay + + + + + + Monri\Payments\Gateway\Config\GooglePay::CODE + + + + + + + MonriGooglePayCountryValidator + Monri\Payments\Gateway\Validator\CurrencyValidator + + + + + + + Monri\Payments\Gateway\Config\GooglePay + + + + + + + MonriGooglePayCreateRequestCommand + Monri\Payments\Gateway\Command\GooglePay\InitializeCommand + MonriPaymentsGatewayResponseCommand + + + + + + + MonriGooglePayCommandPool + + + + + + MonriGooglePayCommandManager + + + + + + MonriGooglePayCommandManager + MonriPaymentsLogger + + + + + + + Monri\Payments\Gateway\Config\GooglePay + + + + + + + Monri\Payments\Gateway\Config\GooglePay + MonriGooglePayDigest + + + + + + Monri\Payments\Gateway\Request\GooglePay\OrderDetailsBuilder + MonriGooglePayPaymentCreateTransferFactory + MonriComponentsJsonClient + Monri\Payments\Gateway\Response\GooglePay\PaymentCreateHandler + MonriPaymentsVirtualLogger + + + + + + MonriGooglePayCreateRequestCommand + + diff --git a/view/frontend/layout/checkout_index_index.xml b/view/frontend/layout/checkout_index_index.xml index 0b335fd..60ec776 100644 --- a/view/frontend/layout/checkout_index_index.xml +++ b/view/frontend/layout/checkout_index_index.xml @@ -44,6 +44,14 @@ + + Monri_Payments/js/view/monri_google_pay + + + true + + + Monri_Payments/js/view/monri_wspay diff --git a/view/frontend/layout/monripayments_googlepay_payment.xml b/view/frontend/layout/monripayments_googlepay_payment.xml new file mode 100644 index 0000000..13caea0 --- /dev/null +++ b/view/frontend/layout/monripayments_googlepay_payment.xml @@ -0,0 +1,20 @@ + + + + + + + + + Monri\Payments\ViewModel\GooglePayConfig + + + + + + + diff --git a/view/frontend/templates/googlepay/payment.phtml b/view/frontend/templates/googlepay/payment.phtml new file mode 100644 index 0000000..b793693 --- /dev/null +++ b/view/frontend/templates/googlepay/payment.phtml @@ -0,0 +1,16 @@ +getData( 'view_model' )->getConfig(); +?> + +
+ + + + diff --git a/view/frontend/web/js/view/init/googlepay-init.js b/view/frontend/web/js/view/init/googlepay-init.js new file mode 100644 index 0000000..c4208f6 --- /dev/null +++ b/view/frontend/web/js/view/init/googlepay-init.js @@ -0,0 +1,81 @@ +define([ + 'jquery', + 'uiComponent', + 'mage/translate', + 'mage/url', + 'Magento_Customer/js/customer-data' +], function ($, Component, $t, urlBuilder, customerData) { + 'use strict'; + + return Component.extend({ + initialize: function (config) { + this._super(); + + var self = this; + + if (config.error) { + $('#monri-error').text(config.message || $t('Google Pay is unavailable.')); + return; + } + + function monriAddScriptTag(url) { + var deferred = $.Deferred(); + var script = document.createElement('script'); + script.src = url; + script.onload = deferred.resolve; + script.onerror = deferred.reject; + document.head.appendChild(script); + return deferred.promise(); + } + + function monriCreatePayment() { + var transaction = { + ch_full_name: config.payload.ch_full_name, + ch_address: config.payload.ch_address, + ch_city: config.payload.ch_city, + ch_zip: config.payload.ch_zip, + ch_phone: config.payload.ch_phone, + ch_country: config.payload.ch_country, + ch_email: config.payload.ch_email, + ch_language: config.payload.locale + }; + + var monri = Monri(config.authenticityToken, {locale: config.payload.locale}); + var components = monri.components({clientSecret: config.payload.client_secret}); + + var googlePay = components.create('google-pay', { + style: { invalid: { color: 'red' } }, + trx_token: config.payload.client_secret, + environment: config.isTest ? 'test' : 'production', + transaction: transaction + }); + + googlePay.mount('google-pay-element'); + + window.addEventListener('message', (event) => { + if (event.data?.sentinel === '__ACTIVITIES__' && event.data?.cmd === 'check') { + customerData.invalidate(['cart', 'checkout-data']); + window.location.href = urlBuilder.build('monripayments/googlepay/cancel'); + } + + if (event.data.type === 'PAYMENT_RESULT') { + const {transaction} = event.data; + if (transaction.status === 'approved') { + console.log('success') + } else { + console.log('error') + } + } + }); + } + + function monriFailed() { + $('#monri-error').text($t('Monri failed to initialize.')); + } + + $.when(monriAddScriptTag(config.componentsJsUrl)) + .then(monriCreatePayment) + .fail(monriFailed); + } + }); +}); diff --git a/view/frontend/web/js/view/method-renderer/monri_google_pay.js b/view/frontend/web/js/view/method-renderer/monri_google_pay.js new file mode 100644 index 0000000..68aefb0 --- /dev/null +++ b/view/frontend/web/js/view/method-renderer/monri_google_pay.js @@ -0,0 +1,55 @@ +/** + * This file is part of the Monri Payments module + * + * (c) Monri Payments d.o.o. + * + * @author Favicode + */ + +define( + [ + 'Magento_Checkout/js/view/payment/default', + 'Magento_Vault/js/view/payment/vault-enabler', + 'jquery', + 'underscore', + 'mage/template', + 'Magento_Checkout/js/model/error-processor', + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Customer/js/customer-data', + 'mage/url' + ], + function (Component, VaultEnabler, $, _, mageTemplate, errorProcessor, fullScreenLoader, customerData, urlBuilder) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Monri_Payments/google_pay' + }, + redirectAfterPlaceOrder: false, + + getCode: function () { + return 'monri_google_pay'; + }, + + initialize: function () { + this._super(); + return this; + }, + + getData: function () { + var data = { + 'method': this.getCode(), + 'additional_data': {} + }; + console.log('data: ', data) + return data; + }, + + afterPlaceOrder: function() { + fullScreenLoader.startLoader(); + customerData.invalidate(['cart', 'checkout-data']); + window.location.href = urlBuilder.build('monripayments/googlepay/payment'); + } + }); + } +); diff --git a/view/frontend/web/js/view/monri_google_pay.js b/view/frontend/web/js/view/monri_google_pay.js new file mode 100644 index 0000000..8f0ce60 --- /dev/null +++ b/view/frontend/web/js/view/monri_google_pay.js @@ -0,0 +1,28 @@ +/** + * This file is part of the Monri Payments module + * + * (c) Monri Payments d.o.o. + * + * @author Favicode + * @version 1.7.0 + */ + +define( + [ + 'uiComponent', + 'Magento_Checkout/js/model/payment/renderer-list' + ], + function ( + Component, + rendererList + ) { + 'use strict'; + rendererList.push( + { + type: 'monri_google_pay', + component: 'Monri_Payments/js/view/method-renderer/monri_google_pay' + } + ); + return Component.extend({}); + } +); diff --git a/view/frontend/web/template/google_pay.html b/view/frontend/web/template/google_pay.html new file mode 100644 index 0000000..89e7f08 --- /dev/null +++ b/view/frontend/web/template/google_pay.html @@ -0,0 +1,42 @@ +
+
+ + +
+
+ + + +
+
+ + + +
+
+ +
+ + + +
+ +
+ +
+
+ +
+
+
+
From 0fe9e275f302f23dfaee5e92dfc87b77bd7c8069 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Tue, 24 Mar 2026 13:39:53 +0100 Subject: [PATCH 02/18] wip, first fully working version of GooglePay integration --- Controller/GooglePay/Success.php | 29 +- .../GooglePay/GatewayResponseCommand.php | 155 ++++++++++ .../Command/GooglePay/InitializeCommand.php | 17 +- Gateway/Http/StatusClient.php | 156 ++++++++++ .../Request/GooglePay/OrderDetailsBuilder.php | 6 +- Gateway/Request/StatusRequestBuilder.php | 61 ++++ .../Response/GooglePay/OrderUpdateHandler.php | 275 ++++++++++++++++++ Gateway/Response/GooglePay/StatusHandler.php | 23 ++ Model/Crypto/Components/OrderStatusDigest.php | 26 ++ composer.json | 3 +- etc/di.xml | 45 ++- .../web/js/view/init/googlepay-init.js | 2 +- 12 files changed, 769 insertions(+), 29 deletions(-) create mode 100644 Gateway/Command/GooglePay/GatewayResponseCommand.php create mode 100644 Gateway/Http/StatusClient.php create mode 100644 Gateway/Request/StatusRequestBuilder.php create mode 100644 Gateway/Response/GooglePay/OrderUpdateHandler.php create mode 100644 Gateway/Response/GooglePay/StatusHandler.php create mode 100644 Model/Crypto/Components/OrderStatusDigest.php diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php index 0b49295..18215a2 100644 --- a/Controller/GooglePay/Success.php +++ b/Controller/GooglePay/Success.php @@ -17,6 +17,8 @@ use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Payment\Gateway\Command\CommandException; use Magento\Payment\Gateway\Command\CommandManagerInterface; use Magento\Payment\Model\InfoInterface; use Magento\Payment\Model\Method\Logger; @@ -92,13 +94,14 @@ public function execute() $log['payload'] = $gatewayResponse; $gatewayResponse['status'] = 'approved'; - $digestData = $this->getDigestData(); + //Google pay has no digest in success url. Instead, we get order status using API and save it in payment + $this->commandManager->executeByCode('check_status', $payment); - $result = $this->processGatewayResponse($gatewayResponse, $payment, $digestData); + $result = $this->commandManager->executeByCode('gateway_response', $payment);; - if (isset($result['response_code_message'])) { + if ( $result->get( 'response_code_message' ) !== null ) { $this->messageManager->addNoticeMessage( - __('The payment has been accepted: %1', $result['response_code_message']) + __('The payment has been accepted: %1', $result->get( 'response_code_message' )) ); } else { $this->messageManager->addNoticeMessage(__('The payment has been accepted.')); @@ -125,21 +128,5 @@ public function execute() return $resultRedirect->setPath('checkout/onepage/success'); } - /** - * Resolve digest data from url - * - * @return array - */ - protected function getDigestData() - { - $digest = $this->getRequest()->getParam('digest'); - $url = $this->_url->getCurrentUrl(); - - $data = str_replace('&digest=' . $digest, '', $url); - - return [ - 'digest' => $digest, - 'digest_data' => $data, - ]; - } } + diff --git a/Gateway/Command/GooglePay/GatewayResponseCommand.php b/Gateway/Command/GooglePay/GatewayResponseCommand.php new file mode 100644 index 0000000..c55ad24 --- /dev/null +++ b/Gateway/Command/GooglePay/GatewayResponseCommand.php @@ -0,0 +1,155 @@ + + */ + +namespace Monri\Payments\Gateway\Command\GooglePay; + +use Magento\Payment\Gateway\Command; +use Magento\Payment\Gateway\Command\CommandException; +use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\ErrorMapper\ErrorMessageMapperInterface; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ValidatorInterface; + +class GatewayResponseCommand implements CommandInterface +{ + public const STATUS_FIELD = 'status'; + + public const STATUS_APPROVED = 'approved'; + + /** + * @var Command\Result\ArrayResultFactory + */ + protected $arrayResultFactory; + + /** + * @var HandlerInterface + */ + protected $orderUpdateHandler; + + /** + * @var ValidatorInterface + */ + protected $validator; + /** + * @var ErrorMessageMapperInterface + */ + private $errorMessageMapper; + + /** + * GatewayResponseCommand constructor. + * + * @param Command\Result\ArrayResultFactory $arrayResultFactory + * @param HandlerInterface $orderUpdateHandler + * @param ErrorMessageMapperInterface $errorMessageMapper + * @param ValidatorInterface|null $validator + */ + public function __construct( + Command\Result\ArrayResultFactory $arrayResultFactory, + HandlerInterface $orderUpdateHandler, + ErrorMessageMapperInterface $errorMessageMapper, + ?ValidatorInterface $validator = null + ) { + $this->arrayResultFactory = $arrayResultFactory; + $this->orderUpdateHandler = $orderUpdateHandler; + $this->validator = $validator; + $this->errorMessageMapper = $errorMessageMapper; + } + + /** + * Executes command basing on business object + * + * @param array $commandSubject + * @return null|Command\ResultInterface + * @throws CommandException + */ + public function execute(array $commandSubject) + { + if ($this->validator) { + $result = $this->validator->validate($commandSubject); + + if (!$result->isValid()) { + $this->processErrors($result); + } + } + $paymentDO = SubjectReader::readPayment($commandSubject); + $payment = $paymentDO->getPayment(); + + $response = [ + 'status' => $payment->getAdditionalInformation('gateway_status'), + 'response_code' => $payment->getAdditionalInformation('gateway_response_code'), + 'transaction_type' => $payment->getAdditionalInformation('gateway_transaction_type'), + 'approval_code' => $payment->getAdditionalInformation('gateway_approval_code'), + 'order_number' => $payment->getAdditionalInformation('monri_order_number'), + ]; + + $this->assertGatewayResponse($response); + + $this->orderUpdateHandler->handle($commandSubject, $response); + + $successfulPayment = $this->isPaymentApproved($response); + + + $responseCode = $paymentDO->getPayment()->getAdditionalInformation('gateway_response_code'); + + $responseCodeMessage = null; + if ($responseCode) { + $responseCodeMessage = $this->errorMessageMapper->getMessage($responseCode); + } + + return $this->arrayResultFactory->create(['array' => [ + 'message' => $successfulPayment ? __('The payment has been accepted.') : __('The payment has been denied.'), + 'successful' => $successfulPayment, + 'response_code' => $responseCode, + 'response_code_message' => $responseCodeMessage, + ]]); + } + + /** + * Asserts the gateway response and throws an exception in case a crucial field is missing in the response. + * + * @param array $response + * @throws CommandException + */ + protected function assertGatewayResponse(array $response) + { + if (!array_key_exists(self::STATUS_FIELD, $response)) { + throw new CommandException(__('Status field missing.')); + } + } + + /** + * Checks to see if the gateway approved the transaction or not. + * + * @param array $response + * @return bool + */ + protected function isPaymentApproved(array $response) + { + return $response[self::STATUS_FIELD] === self::STATUS_APPROVED; + } + + /** + * Process errors + * + * @param ResultInterface $validationResult + * @throws CommandException + */ + protected function processErrors(ResultInterface $validationResult) + { + $errors = array_merge($validationResult->getErrorCodes(), $validationResult->getFailsDescription()); + + $message = !empty($errors) + ? __(implode(PHP_EOL, $errors)) + : __('There has been an issue processing your payment.'); + + throw new CommandException($message); + } +} diff --git a/Gateway/Command/GooglePay/InitializeCommand.php b/Gateway/Command/GooglePay/InitializeCommand.php index a51f14e..53ef908 100644 --- a/Gateway/Command/GooglePay/InitializeCommand.php +++ b/Gateway/Command/GooglePay/InitializeCommand.php @@ -18,6 +18,7 @@ use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Payment; use Monri\Payments\Gateway\Config\GooglePay as Config; +use Monri\Payments\Helper\Formatter; class InitializeCommand implements CommandInterface { @@ -31,6 +32,11 @@ class InitializeCommand implements CommandInterface */ private $logger; + /** + * @var Formatter + */ + private $formatter; + /** * InitializeCommand constructor. * @@ -39,10 +45,12 @@ class InitializeCommand implements CommandInterface */ public function __construct( Config $config, - Logger $logger + Logger $logger, + Formatter $formatter ) { $this->config = $config; $this->logger = $logger; + $this->formatter = $formatter; } /** @@ -64,6 +72,13 @@ public function execute(array $commandSubject) $payment->getOrder()->setCanSendNewEmailFlag(false); try { + $payment->setAdditionalInformation( + 'monri_order_number', + $this->formatter->formatText( + $payment->getOrder()->getIncrementId() . uniqid('-'), + 40 + ) + ); $payment->setAdditionalInformation( 'transaction_type', 'purchase' diff --git a/Gateway/Http/StatusClient.php b/Gateway/Http/StatusClient.php new file mode 100644 index 0000000..dc36e32 --- /dev/null +++ b/Gateway/Http/StatusClient.php @@ -0,0 +1,156 @@ + __METHOD__, + 'request_data' => [], + 'response_data' => [], + 'errors' => [], + 'success' => true, + ]; + + $requestUri = $transferObject->getUri(); + $requestMethod = strtoupper($transferObject->getMethod()); + $requestPayload = $this->prepareRequestPayload($transferObject->getBody()); + + $log['request_data'] = $requestPayload; + + /** @var \Magento\Framework\HTTP\ClientInterface $client */ + $client = $this->httpClientFactory->create(); + + $client->setTimeout($this->timeout); + + $client->setHeaders(array_merge( + $transferObject->getHeaders(), + [ + 'Content-Type' => $this->requestType + ] + )); + + if ($requestMethod === 'POST') { + $client->post($requestUri, $requestPayload); + } else { + $client->get($requestUri); + } + + $responseStatus = $client->getStatus(); + + $response = $this->parseResponseBody($client->getBody()); + + $log['response_data'] = $response; + + try { + $this->assertServerResponse($response, $responseStatus); + } catch (ClientException $e) { + $log['errors'][] = 'Exception caught: ' . $e->getMessage(); + $log['success'] = false; + $this->logger->debug($log); + throw new \Magento\Framework\Exception\LocalizedException( + __('Order status fetch failed.') + ); + } + + $this->logger->debug($log); + return $response; + } + + /** + * Prepares request payload + * + * @param array $payload + * @return bool|string + */ + protected function prepareRequestPayload(array $payload) + { + try { + $serialized = $this->serializer->serialize($payload); + if ($serialized === false) { + return ''; + } + + return $serialized; + } catch (Exception $e) { + return ''; + } + } + + /** + * Parses response and returns array + * + * @param string $response + * @return array + */ + protected function parseResponseBody($response) + { + try { + //the response is xml, so we need to convert it to json and then to array + $xml = simplexml_load_string($response); + $json = json_encode($xml); + $data = json_decode($json,TRUE); + if ($data === null) { + return []; + } + + return $data; + } catch (Exception $e) { + return []; + } + } + + /** + * Validate server response + * + * @param array $responseBody + * @param int $statusCode + * @throws ClientException + */ + protected function assertServerResponse(array $responseBody, $statusCode) + { + if ($statusCode >= 400 && $statusCode <= 499) { + if (isset($responseBody['error'])) { + $errors = $responseBody['error']; + if (!is_array($errors)) { + $errors = [$errors]; + } + + throw new ClientException(__('Client error (%1): %2', $statusCode, implode(', ', $errors))); + } else { + throw new ClientException(__('Client error (%1)', $statusCode)); + } + } elseif ($statusCode >= 500 && $statusCode <= 599) { + throw new ClientException(__('Server error (%1)', $statusCode)); + } + } +} + diff --git a/Gateway/Request/GooglePay/OrderDetailsBuilder.php b/Gateway/Request/GooglePay/OrderDetailsBuilder.php index e5ce74c..d6f7f39 100644 --- a/Gateway/Request/GooglePay/OrderDetailsBuilder.php +++ b/Gateway/Request/GooglePay/OrderDetailsBuilder.php @@ -53,16 +53,15 @@ public function __construct( public function build(array $buildSubject) { $paymentDataObject = SubjectReader::readPayment($buildSubject); + $payment = $paymentDataObject->getPayment(); /** @var \Magento\Payment\Gateway\Data\Quote\QuoteAdapter $order */ $order = $paymentDataObject->getOrder(); - $orderNumber = $this->formatter->formatText($order->getOrderIncrementId() . uniqid('-'), 40); + $orderNumber = $payment->getAdditionalInformation('monri_order_number'); $orderIpAddress = $paymentDataObject->getPayment()->getOrder()->getRemoteIp(); - $paymentDataObject->getPayment()->setAdditionalInformation(self::ORDER_NUMBER_FIELD, $orderNumber); - $orderInfo = __('Order: %1', $order->getOrderIncrementId())->render(); $transportObject = $this->dataObjectFactory->create([ @@ -90,7 +89,6 @@ public function build(array $buildSubject) $order->getGrandTotalAmount() ); } catch (\TypeError $e) { - $payment = $paymentDataObject->getPayment(); $orderObject = $payment->getOrder(); $orderAmount = $this->formatter->formatPrice( $orderObject->getBaseGrandTotal() diff --git a/Gateway/Request/StatusRequestBuilder.php b/Gateway/Request/StatusRequestBuilder.php new file mode 100644 index 0000000..14620f6 --- /dev/null +++ b/Gateway/Request/StatusRequestBuilder.php @@ -0,0 +1,61 @@ +getOrder(); + + $payment = $paymentDataObject->getPayment(); + $orderNumber = $payment->getAdditionalInformation('monri_order_number'); + $currencyCode = $order->getCurrencyCode(); + + + /* + Added in 2.4.8, because \PayPal\Braintree\Gateway\Data\Order\OrderAdapter puts themselves as preference for + \Magento\Payment\Gateway\Data\Order\OrderAdapter. It declares strict types, but getGrandTotalAmount returns + string instead of float, causing it to break execution. + */ + try { + $amount = $this->formatter->formatPrice( + $order->getGrandTotalAmount() + ); + } catch (\TypeError $e) { + $orderObject = $payment->getOrder(); + $amount = $this->formatter->formatPrice( + $orderObject->getBaseGrandTotal() + ); + } + $storeId = $order->getStoreId(); + $authToken = $this->config->getClientAuthenticityToken($storeId); + + $digest = $this->digest->build( + $orderNumber, + $order->getStoreId() + ); + + return [ + 'order' => [ + 'order_number' => $orderNumber, + 'authenticity_token' => $authToken, + 'digest' => $digest, + ], + '__store' => $order->getStoreId(), + ]; + } +} diff --git a/Gateway/Response/GooglePay/OrderUpdateHandler.php b/Gateway/Response/GooglePay/OrderUpdateHandler.php new file mode 100644 index 0000000..9610193 --- /dev/null +++ b/Gateway/Response/GooglePay/OrderUpdateHandler.php @@ -0,0 +1,275 @@ + + */ + +namespace Monri\Payments\Gateway\Response\GooglePay; + +use Exception; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\TMapFactory; +use Magento\Payment\Gateway\Command\CommandException; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\Method\Logger; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\OrderRepository; +use Monri\Payments\Gateway\Config; +use Monri\Payments\Gateway\Exception\TransactionAlreadyProcessedException; +use Monri\Payments\Lock\Order\LockInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class OrderUpdateHandler implements HandlerInterface +{ + /** + * @var OrderRepository + */ + private $orderRepository; + + /** + * @var OrderSender + */ + private $orderSender; + + /** + * @var Config + */ + private $config; + + /** + * @var Logger + */ + private $logger; + + /** + * @var LockInterface + */ + private $locker; + + /** + * @var HandlerInterface[] + */ + private $transactionHandlers; + + /** + * @var HandlerInterface + */ + private $unsuccessfulTransactionHandler; + + /** + * OrderUpdateHandler constructor. + * + * @param OrderRepository $orderRepository + * @param OrderSender $orderSender + * @param Logger $logger + * @param TMapFactory $TMapFactory + * @param array $transactionHandlers + * @param HandlerInterface $unsuccessfulTransactionHandler + * @param Config $config + * @param LockInterface $locker + */ + public function __construct( + OrderRepository $orderRepository, + OrderSender $orderSender, + Logger $logger, + TMapFactory $TMapFactory, + array $transactionHandlers, + HandlerInterface $unsuccessfulTransactionHandler, + Config $config, + LockInterface $locker + ) { + $this->orderRepository = $orderRepository; + $this->orderSender = $orderSender; + $this->config = $config; + $this->logger = $logger; + $this->locker = $locker; + + $this->transactionHandlers = $TMapFactory->create([ + 'type' => HandlerInterface::class, + 'array' => $transactionHandlers + ]); + + $this->unsuccessfulTransactionHandler = $unsuccessfulTransactionHandler; + } + + /** + * Handles response + * + * @param array $handlingSubject + * @param array $response + * @return void + * @throws AlreadyExistsException + * @throws InputException + * @throws NoSuchEntityException + * @throws CommandException + */ + public function handle(array $handlingSubject, array $response) + { + $log = [ + 'location' => __METHOD__, + 'errors' => [], + 'action' => null, + 'email_sent' => false, + ]; + + if (!isset($response['status'])) { + $log['errors'][] = 'Status not set in response, invalid response.'; + $this->logger->debug($log); + return; + } + + $paymentDataObject = SubjectReader::readPayment($handlingSubject); + + /** @var Payment $payment */ + $payment = $paymentDataObject->getPayment(); + + /** @var Order $order */ + $order = $payment->getOrder(); + + $this->ensureNotLocked($order); + $this->locker->lock($order->getId()); + + if (!isset($response['transaction_type'])) { + $response['transaction_type'] = $this->getTransactionTypeFromPayment($payment, $order); + } + + try { + $this->processResponse($handlingSubject, $response, $order); + + if (!$order->getEmailSent() && $this->isSuccessfulResponse($response)) { + $log['email_sent'] = true; + $this->orderSender->send($order); + } + } finally { + $this->locker->unlock($order->getId()); + $this->logger->debug($log); + } + } + + /** + * Processes the response + * + * @param array $handlingSubject + * @param array $response + * @param OrderInterface $order + * @return void + * @throws AlreadyExistsException + * @throws CommandException + * @throws InputException + * @throws NoSuchEntityException + * @throws TransactionAlreadyProcessedException + */ + protected function processResponse( + array $handlingSubject, + array $response, + OrderInterface $order + ): void { + try { + if ($this->isSuccessfulResponse($response)) { + $this->processSuccessfulGatewayResponse($handlingSubject, $response); + } else { + $this->processUnsuccessfulGatewayResponse($handlingSubject, $response); + } + } catch (TransactionAlreadyProcessedException $e) { + // pass through TransactionAlreadyProcessedException + throw $e; + } catch (Exception $e) { + throw new CommandException(__('Failed to process transaction: %1', $e->getMessage())); + } + + $this->orderRepository->save($order); + } + + /** + * Performs any necessary actions for a given transaction type. + * + * @param array $handlingSubject + * @param array $response + */ + protected function processSuccessfulGatewayResponse( + array $handlingSubject, + array $response + ) { + $transactionType = isset($response['transaction_type']) ? $response['transaction_type'] : null; + + if (isset($this->transactionHandlers[$transactionType])) { + $this->transactionHandlers[$transactionType]->handle($handlingSubject, $response); + } + } + + /** + * Processes an unsuccessful transaction. + * + * @param array $handlingSubject + * @param array $response + */ + protected function processUnsuccessfulGatewayResponse( + array $handlingSubject, + array $response + ) { + $this->unsuccessfulTransactionHandler->handle($handlingSubject, $response); + } + + /** + * Determines if a given response is successful. + * + * @param array $response + * @return bool + */ + protected function isSuccessfulResponse(array $response) + { + $isSuccessCode = isset($response['response_code']) ? $response['response_code'] === '0000' : true; + + return $isSuccessCode && $response['status'] === 'approved'; + } + + /** + * Returns the transaction type. + * + * @param InfoInterface $payment + * @param Order $order + * @return string + */ + protected function getTransactionTypeFromPayment(InfoInterface $payment, Order $order) + { + $transactionType = $payment->getAdditionalInformation('transaction_type'); + + if (!$transactionType) { + $transactionType = $this->config->getTransactionType($order->getStoreId()); + } + + return $transactionType; + } + + /** + * Throws TransactionAlreadyProcessedException on locked order + * + * @param OrderInterface $order + * @throws TransactionAlreadyProcessedException + * @return void + */ + protected function ensureNotLocked(OrderInterface $order): void + { + $orderId = $order->getId(); + if ($this->locker->isLocked($orderId)) { + $this->logger->debug([ + 'message' => __('Order is currently being processed (lock). Order ID: %1', $orderId), + 'log_origin' => __METHOD__ + ]); + + throw new TransactionAlreadyProcessedException(__('Order lock. Order ID: %1', $orderId)); + } + } +} diff --git a/Gateway/Response/GooglePay/StatusHandler.php b/Gateway/Response/GooglePay/StatusHandler.php new file mode 100644 index 0000000..101eb84 --- /dev/null +++ b/Gateway/Response/GooglePay/StatusHandler.php @@ -0,0 +1,23 @@ +getPayment(); + + // Save gateway status info for use is payment handler + $payment->setAdditionalInformation('gateway_status', $response['status'] ?? null); + $payment->setAdditionalInformation('gateway_response_code', $response['response-code'] ?? null); + $payment->setAdditionalInformation('gateway_transaction_type', $response['transaction-type'] ?? null); + $payment->setAdditionalInformation('gateway_approval_code', $response['approval-code'] ?? null); + } +} diff --git a/Model/Crypto/Components/OrderStatusDigest.php b/Model/Crypto/Components/OrderStatusDigest.php new file mode 100644 index 0000000..dc21f43 --- /dev/null +++ b/Model/Crypto/Components/OrderStatusDigest.php @@ -0,0 +1,26 @@ +config->getClientKey($storeId); + $data = $key . $order_number; + + return hash('SHA1', $data); + } +} diff --git a/composer.json b/composer.json index 39014f4..c3fde62 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "magento/module-checkout": "^100.2", "magento/module-store": "^100.2||^101.0", "magento/module-quote": "^101.0", - "php": "^8.1" + "php": "^8.1", + "ext-simplexml": "*" }, "type": "magento2-module", "autoload": { diff --git a/etc/di.xml b/etc/di.xml index 20046b0..5b44447 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -724,12 +724,19 @@
+ + + Monri\Payments\Gateway\Response\OrderUpdateHandler + + + MonriGooglePayCreateRequestCommand Monri\Payments\Gateway\Command\GooglePay\InitializeCommand - MonriPaymentsGatewayResponseCommand + MonriGooglePayGatewayResponseCommand + MonriGooglePayCheckStatusCommand @@ -753,6 +760,13 @@ + + + MonriGooglePayCommandManager + MonriPaymentsLogger + + + @@ -760,6 +774,12 @@ + + + Monri\Payments\Gateway\Config\GooglePay + + + @@ -768,6 +788,13 @@ + + + Monri\Payments\Gateway\Config\GooglePay + MonriGooglePayOrderStatusDigest + + + Monri\Payments\Gateway\Request\GooglePay\OrderDetailsBuilder @@ -784,6 +811,22 @@ + + + Monri\Payments\Gateway\Config\GooglePay + MonriGooglePayOrderStatusDigest + + + + + + MonriGooglePayStatusRequestBuilder + MonriGooglePayOrderStatusTransferFactory + Monri\Payments\Gateway\Http\StatusClient + Monri\Payments\Gateway\Response\GooglePay\StatusHandler + + + diff --git a/view/frontend/web/js/view/init/googlepay-init.js b/view/frontend/web/js/view/init/googlepay-init.js index c4208f6..06c6c5f 100644 --- a/view/frontend/web/js/view/init/googlepay-init.js +++ b/view/frontend/web/js/view/init/googlepay-init.js @@ -61,7 +61,7 @@ define([ if (event.data.type === 'PAYMENT_RESULT') { const {transaction} = event.data; if (transaction.status === 'approved') { - console.log('success') + window.location.href = urlBuilder.build('monripayments/googlepay/success'); } else { console.log('error') } From 705518c66dcfc4a194f1c9a150d934ad3c6942b3 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 09:33:30 +0100 Subject: [PATCH 03/18] message display improvements --- Controller/GooglePay/Success.php | 11 ++++++----- etc/di.xml | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php index 18215a2..cada5f7 100644 --- a/Controller/GooglePay/Success.php +++ b/Controller/GooglePay/Success.php @@ -92,16 +92,17 @@ public function execute() $gatewayResponse = $this->getRequest()->getParams(); $log['payload'] = $gatewayResponse; - $gatewayResponse['status'] = 'approved'; - //Google pay has no digest in success url. Instead, we get order status using API and save it in payment + //Google pay has no digest in success url. Instead, we get order status using API and save it in payment additonal info $this->commandManager->executeByCode('check_status', $payment); - $result = $this->commandManager->executeByCode('gateway_response', $payment);; + $result = $this->commandManager->executeByCode('gateway_response', $payment)->get(); + $responseCodeMessage = $result['response_code_message'] ?? null; - if ( $result->get( 'response_code_message' ) !== null ) { + if ($responseCodeMessage !== null) { + $responseCodeMessage = (string)$responseCodeMessage; $this->messageManager->addNoticeMessage( - __('The payment has been accepted: %1', $result->get( 'response_code_message' )) + __('The payment has been accepted: %1', $responseCodeMessage) ); } else { $this->messageManager->addNoticeMessage(__('The payment has been accepted.')); diff --git a/etc/di.xml b/etc/di.xml index 5b44447..3281838 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -727,6 +727,7 @@ Monri\Payments\Gateway\Response\OrderUpdateHandler + MonriPaymentsErrorsWithRawMapper @@ -824,6 +825,7 @@ MonriGooglePayOrderStatusTransferFactory Monri\Payments\Gateway\Http\StatusClient Monri\Payments\Gateway\Response\GooglePay\StatusHandler + MonriPaymentsVirtualLogger From 9bae8b9cd2e89c9ec93ece9c7c0538a0b4a90554 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 10:14:40 +0100 Subject: [PATCH 04/18] magento coding standard --- Block/Customer/GooglePay/PaymentInfo.php | 1 + Controller/GooglePay/Cancel.php | 1 - Controller/GooglePay/Payment.php | 13 +++++++++---- Controller/GooglePay/Success.php | 2 -- .../GooglePay/GatewayResponseCommand.php | 1 - .../Command/GooglePay/InitializeCommand.php | 1 + Gateway/Config/GooglePay.php | 4 ---- Gateway/Http/StatusClient.php | 7 ++++--- .../Request/GooglePay/OrderDetailsBuilder.php | 9 ++++++--- Gateway/Request/StatusRequestBuilder.php | 18 ++++++++++++++++-- Gateway/Response/GooglePay/StatusHandler.php | 10 ++++++++++ Model/Crypto/Components/OrderStatusDigest.php | 8 +++++++- ViewModel/GooglePayConfig.php | 17 +++++++++++++++-- .../frontend/templates/googlepay/payment.phtml | 4 ++-- 14 files changed, 71 insertions(+), 25 deletions(-) diff --git a/Block/Customer/GooglePay/PaymentInfo.php b/Block/Customer/GooglePay/PaymentInfo.php index 8f22d0a..82ef29f 100644 --- a/Block/Customer/GooglePay/PaymentInfo.php +++ b/Block/Customer/GooglePay/PaymentInfo.php @@ -1,6 +1,7 @@ getRequest()->getParams(); $log['payload'] = $gatewayResponse; - /** @var InfoInterface $payment */ $payment = $order->getPayment(); diff --git a/Controller/GooglePay/Payment.php b/Controller/GooglePay/Payment.php index ad6a546..ee5813d 100644 --- a/Controller/GooglePay/Payment.php +++ b/Controller/GooglePay/Payment.php @@ -6,14 +6,19 @@ class Payment implements HttpGetActionInterface { - private $resultPageFactory; - + /** + * Payment constructor. + * + * @param PageFactory $resultPageFactory + */ public function __construct( - PageFactory $resultPageFactory + private PageFactory $resultPageFactory ) { - $this->resultPageFactory = $resultPageFactory; } + /** + * @inheritDoc + */ public function execute() { return $this->resultPageFactory->create(); diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php index cada5f7..a7d6b5b 100644 --- a/Controller/GooglePay/Success.php +++ b/Controller/GooglePay/Success.php @@ -128,6 +128,4 @@ public function execute() return $resultRedirect->setPath('checkout/onepage/success'); } - } - diff --git a/Gateway/Command/GooglePay/GatewayResponseCommand.php b/Gateway/Command/GooglePay/GatewayResponseCommand.php index c55ad24..ca5c5a6 100644 --- a/Gateway/Command/GooglePay/GatewayResponseCommand.php +++ b/Gateway/Command/GooglePay/GatewayResponseCommand.php @@ -96,7 +96,6 @@ public function execute(array $commandSubject) $successfulPayment = $this->isPaymentApproved($response); - $responseCode = $paymentDO->getPayment()->getAdditionalInformation('gateway_response_code'); $responseCodeMessage = null; diff --git a/Gateway/Command/GooglePay/InitializeCommand.php b/Gateway/Command/GooglePay/InitializeCommand.php index 53ef908..9445931 100644 --- a/Gateway/Command/GooglePay/InitializeCommand.php +++ b/Gateway/Command/GooglePay/InitializeCommand.php @@ -42,6 +42,7 @@ class InitializeCommand implements CommandInterface * * @param Config $config * @param Logger $logger + * @param Formatter $formatter */ public function __construct( Config $config, diff --git a/Gateway/Config/GooglePay.php b/Gateway/Config/GooglePay.php index 611e28a..6442881 100644 --- a/Gateway/Config/GooglePay.php +++ b/Gateway/Config/GooglePay.php @@ -43,8 +43,4 @@ public function getPaymentAction($storeId = null) { return $this->getValue(self::PAYMENT_ACTION, $storeId); } - - public function getIsTestMode($storeId = null) { - return $this->getIsSandboxMode( $storeId ) === true ? 'test' : 'prod'; - } } diff --git a/Gateway/Http/StatusClient.php b/Gateway/Http/StatusClient.php index dc36e32..4823c9f 100644 --- a/Gateway/Http/StatusClient.php +++ b/Gateway/Http/StatusClient.php @@ -1,6 +1,7 @@ getOrderIncrementId())->render(); + //Google Pay only supports purchase transactions + $transactionType = 'purchase'; + $transportObject = $this->dataObjectFactory->create([ 'data' => [ 'description' => $orderInfo @@ -103,7 +106,7 @@ public function build(array $buildSubject) self::AMOUNT_FIELD => $orderAmount, self::CURRENCY_FIELD => $currencyCode, self::IP_ADDRESS_FIELD => $orderIpAddress, - self::TRANSACTION_TYPE_FIELD => 'purchase', + self::TRANSACTION_TYPE_FIELD => $transactionType, ]; } } diff --git a/Gateway/Request/StatusRequestBuilder.php b/Gateway/Request/StatusRequestBuilder.php index 14620f6..250c42b 100644 --- a/Gateway/Request/StatusRequestBuilder.php +++ b/Gateway/Request/StatusRequestBuilder.php @@ -10,11 +10,26 @@ class StatusRequestBuilder implements BuilderInterface { + /** + * StatusRequestBuilder constructor. + * + * @param Digest $digest + * @param Formatter $formatter + * @param Config $config + * */ public function __construct( private Digest $digest, private Formatter $formatter, private Config $config, - ){} + ) { + } + + /** + * Build request + * + * @param array $buildSubject + * @return array + * */ public function build(array $buildSubject) { $paymentDataObject = SubjectReader::readPayment($buildSubject); @@ -25,7 +40,6 @@ public function build(array $buildSubject) $orderNumber = $payment->getAdditionalInformation('monri_order_number'); $currencyCode = $order->getCurrencyCode(); - /* Added in 2.4.8, because \PayPal\Braintree\Gateway\Data\Order\OrderAdapter puts themselves as preference for \Magento\Payment\Gateway\Data\Order\OrderAdapter. It declares strict types, but getGrandTotalAmount returns diff --git a/Gateway/Response/GooglePay/StatusHandler.php b/Gateway/Response/GooglePay/StatusHandler.php index 101eb84..ebf3986 100644 --- a/Gateway/Response/GooglePay/StatusHandler.php +++ b/Gateway/Response/GooglePay/StatusHandler.php @@ -2,12 +2,22 @@ namespace Monri\Payments\Gateway\Response\GooglePay; +use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Sales\Model\Order\Payment; class StatusHandler implements HandlerInterface { + /** + * Updates payment additional information with gateway response details. + * + * @param array $handlingSubject The handling subject containing contextual information for processing. + * @param array $response The response data received from the payment gateway. + * + * @return void + * @throws LocalizedException + */ public function handle(array $handlingSubject, array $response) { $paymentDO = SubjectReader::readPayment($handlingSubject); diff --git a/Model/Crypto/Components/OrderStatusDigest.php b/Model/Crypto/Components/OrderStatusDigest.php index dc21f43..2829c9b 100644 --- a/Model/Crypto/Components/OrderStatusDigest.php +++ b/Model/Crypto/Components/OrderStatusDigest.php @@ -6,13 +6,19 @@ class OrderStatusDigest { + + /** + * @param Config $config + */ public function __construct( private Config $config - ){} + ) { + } /** * Build digest * + * @param string $order_number * @param int|null $storeId * @return string */ diff --git a/ViewModel/GooglePayConfig.php b/ViewModel/GooglePayConfig.php index 61b9a26..286fde6 100644 --- a/ViewModel/GooglePayConfig.php +++ b/ViewModel/GooglePayConfig.php @@ -16,6 +16,14 @@ class GooglePayConfig implements ArgumentInterface { + /** + * @param Session $checkoutSession + * @param OrderRepository $orderRepository + * @param GatewayCommand $googlePayCommand + * @param Config $config + * @param PaymentDataObjectFactory $paymentDataObjectFactory * + * @param Logger $logger + */ public function __construct( private Session $checkoutSession, private OrderRepository $orderRepository, @@ -23,8 +31,14 @@ public function __construct( private Config $config, private PaymentDataObjectFactory $paymentDataObjectFactory, private Logger $logger - ) {} + ) { + } + /** + * Get Google Pay configuration for frontend component. + * + * @return array + */ public function getConfig() { try { @@ -61,6 +75,5 @@ public function getConfig() 'message' => __('Google Pay is currently unavailable.') ]; } - } } diff --git a/view/frontend/templates/googlepay/payment.phtml b/view/frontend/templates/googlepay/payment.phtml index b793693..2124720 100644 --- a/view/frontend/templates/googlepay/payment.phtml +++ b/view/frontend/templates/googlepay/payment.phtml @@ -1,6 +1,6 @@ getData( 'view_model' )->getConfig(); +$config = $block->getData('view_model')->getConfig(); ?>
@@ -10,7 +10,7 @@ $config = $block->getData( 'view_model' )->getConfig(); From 3a7bb6580db7deabd4b770b81c754dc003e107e6 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 10:24:35 +0100 Subject: [PATCH 05/18] deleted unnecessary files --- Block/Customer/GooglePay/PaymentInfo.php | 29 ---- Controller/GooglePay/Form/Data.php | 162 ----------------------- etc/di.xml | 6 - 3 files changed, 197 deletions(-) delete mode 100644 Block/Customer/GooglePay/PaymentInfo.php delete mode 100644 Controller/GooglePay/Form/Data.php diff --git a/Block/Customer/GooglePay/PaymentInfo.php b/Block/Customer/GooglePay/PaymentInfo.php deleted file mode 100644 index 82ef29f..0000000 --- a/Block/Customer/GooglePay/PaymentInfo.php +++ /dev/null @@ -1,29 +0,0 @@ -checkoutSession->getData('last_order_id'); - - if (!$orderId) { - $log['errors'][] = 'Missing order ID field.'; - throw new InputException(__('Missing fields.')); - } - - $order = $this->orderRepository->get($orderId); - - $payment = $order->getPayment(); - - $this->commandManager->executeByCode('create_request', $payment); - - return $payment->getAdditionalInformation( - PaymentCreateHandler::INITIAL_DATA - ); - } -} diff --git a/Controller/GooglePay/Form/Data.php b/Controller/GooglePay/Form/Data.php deleted file mode 100644 index fd59921..0000000 --- a/Controller/GooglePay/Form/Data.php +++ /dev/null @@ -1,162 +0,0 @@ - - */ - -namespace Monri\Payments\Controller\GooglePay\Form; - -use Exception; -use Magento\Checkout\Model\Session; -use Magento\Framework\App\Action\Action; -use Magento\Framework\App\Action\Context; -use Magento\Framework\App\ResponseInterface; -use Magento\Framework\Controller\Result\Json; -use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\Controller\ResultInterface; -use Magento\Framework\Exception\InputException; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Payment\Gateway\Command\CommandException; -use Magento\Payment\Gateway\Command\CommandManagerInterface; -use Magento\Payment\Model\InfoInterface; -use Magento\Payment\Model\Method\Logger; -use Magento\Sales\Model\OrderRepository; -use Monri\Payments\Gateway\Config\GooglePay as Config; -use Monri\Payments\Gateway\Response\Components\PaymentCreateHandler; - -/** - * @SuppressWarnings(PHPMD.AllPurposeAction) - */ -class Data extends Action -{ - /** - * @var Session - */ - private $checkoutSession; - - /** - * @var OrderRepository - */ - private $orderRepository; - - /** - * @var CommandManagerInterface - */ - private $commandManager; - - /** - * @var Config - */ - private $config; - - /** - * @var Logger - */ - private $logger; - - /** - * Data constructor. - * - * @param Context $context - * @param Session $checkoutSession - * @param OrderRepository $orderRepository - * @param Config $config - * @param CommandManagerInterface $commandManager - * @param Logger $logger - */ - public function __construct( - Context $context, - Session $checkoutSession, - OrderRepository $orderRepository, - Config $config, - CommandManagerInterface $commandManager, - Logger $logger - ) { - $this->commandManager = $commandManager; - $this->checkoutSession = $checkoutSession; - $this->orderRepository = $orderRepository; - $this->config = $config; - $this->logger = $logger; - - parent::__construct($context); - } - - /** - * Generate form data that will be used for the redirect to the gateway - * - * @return ResultInterface|ResponseInterface - */ - public function execute() - { - $log = [ - 'location' => __METHOD__, - 'errors' => [], - 'success' => true, - 'payload' => [] - ]; - - /** @var Json $resultJson */ - $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); - - try { - $orderId = $this->checkoutSession->getData('last_order_id'); - - if (!$orderId) { - $log['errors'][] = 'Missing order ID field.'; - throw new InputException(__('Missing fields.')); - } - - $order = $this->orderRepository->get($orderId); - - /** @var InfoInterface $payment */ - $payment = $order->getPayment(); - - $this->commandManager->executeByCode('create_request', $payment); - - $result = $payment->getAdditionalInformation(PaymentCreateHandler::INITIAL_DATA); - $storeId = $order->getStoreId(); - - $resultJson->setData([ - 'payload' => $result, - 'url' => $this->config->getGatewayPaymentCreateURL($storeId), - 'componentsJsUrl' => $this->config->getComponentsJsURL($storeId), - 'isTest' => $this->config->getIsSandboxMode($storeId), - 'error' => null, - 'authenticityToken' => $this->config->getClientAuthenticityToken($storeId), - ]); - - $log['payload'] = $result; - } catch (InputException | NoSuchEntityException | CommandException $e) { - $resultJson->setData([ - 'payload' => [], - 'url' => '', - 'error' => __('Error processing payment, please try again later.') - ]); - - $log['errors'][] = 'Exception caught: ' . $e->getMessage(); - $log['success'] = false; - - $resultJson->setHttpResponseCode(400); - return $resultJson; - } catch (Exception $e) { - $resultJson->setData([ - 'payload' => [], - 'url' => '', - 'error' => __('Error processing payment, please try again later.') - ]); - - $log['errors'][] = 'Unexpected exception caught: ' . $e->getMessage(); - $log['success'] = false; - - $resultJson->setHttpResponseCode(500); - return $resultJson; - } finally { - $this->logger->debug($log); - } - - return $resultJson; - } -} diff --git a/etc/di.xml b/etc/di.xml index 3281838..e1be42b 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -748,12 +748,6 @@
- - - MonriGooglePayCommandManager - - - MonriGooglePayCommandManager From 878feb53a31a62a9e404e58bcdd97cbdac0ff115 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 11:19:22 +0100 Subject: [PATCH 06/18] refactoring --- Controller/GooglePay/Cancel.php | 23 ++- Controller/GooglePay/Success.php | 26 ++- .../GooglePay/GatewayResponseCommand.php | 154 ------------------ etc/di.xml | 3 +- 4 files changed, 45 insertions(+), 161 deletions(-) delete mode 100644 Gateway/Command/GooglePay/GatewayResponseCommand.php diff --git a/Controller/GooglePay/Cancel.php b/Controller/GooglePay/Cancel.php index e8b65ed..bd88a59 100644 --- a/Controller/GooglePay/Cancel.php +++ b/Controller/GooglePay/Cancel.php @@ -83,13 +83,11 @@ public function execute() $this->checkoutSession->getData('last_order_id') ); - $gatewayResponse = $this->getRequest()->getParams(); - $log['payload'] = $gatewayResponse; - /** @var InfoInterface $payment */ $payment = $order->getPayment(); - $gatewayResponse['status'] = 'declined'; + $gatewayResponse = $this->buildCancelGatewayResponse($payment); + $log['payload'] = $gatewayResponse; $result = $this->processGatewayResponse($gatewayResponse, $payment, ['disabled' => true]); @@ -115,4 +113,21 @@ public function execute() return $resultRedirect->setPath('checkout/cart'); } + + /** + * Build minimal cancel response for Google Pay gateway_response command. + * + * @param InfoInterface $payment + * @return array + */ + private function buildCancelGatewayResponse(InfoInterface $payment): array + { + $orderNumber = $payment->getAdditionalInformation('monri_order_number') + ?: $payment->getOrder()->getIncrementId(); + + return [ + 'status' => 'declined', + 'order_number' => $orderNumber, + ]; + } } diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php index a7d6b5b..d2afc17 100644 --- a/Controller/GooglePay/Success.php +++ b/Controller/GooglePay/Success.php @@ -96,7 +96,9 @@ public function execute() //Google pay has no digest in success url. Instead, we get order status using API and save it in payment additonal info $this->commandManager->executeByCode('check_status', $payment); - $result = $this->commandManager->executeByCode('gateway_response', $payment)->get(); + $result = $this->commandManager->executeByCode('gateway_response', $payment, [ + 'response' => $this->buildGatewayResponse($payment), + ])->get(); $responseCodeMessage = $result['response_code_message'] ?? null; if ($responseCodeMessage !== null) { @@ -128,4 +130,26 @@ public function execute() return $resultRedirect->setPath('checkout/onepage/success'); } + + /** + * Build response payload consumed by GooglePay gateway_response command. + * + * @param InfoInterface $payment + * @return array + */ + private function buildGatewayResponse(InfoInterface $payment): array + { + $responseCode = $payment->getAdditionalInformation('gateway_response_code'); + $orderNumber = $responseCode + ? $payment->getAdditionalInformation('monri_order_number') + : $payment->getOrder()->getIncrementId(); + + return [ + 'status' => $payment->getAdditionalInformation('gateway_status'), + 'response_code' => $responseCode, + 'transaction_type' => $payment->getAdditionalInformation('gateway_transaction_type'), + 'approval_code' => $payment->getAdditionalInformation('gateway_approval_code'), + 'order_number' => $orderNumber, + ]; + } } diff --git a/Gateway/Command/GooglePay/GatewayResponseCommand.php b/Gateway/Command/GooglePay/GatewayResponseCommand.php deleted file mode 100644 index ca5c5a6..0000000 --- a/Gateway/Command/GooglePay/GatewayResponseCommand.php +++ /dev/null @@ -1,154 +0,0 @@ - - */ - -namespace Monri\Payments\Gateway\Command\GooglePay; - -use Magento\Payment\Gateway\Command; -use Magento\Payment\Gateway\Command\CommandException; -use Magento\Payment\Gateway\CommandInterface; -use Magento\Payment\Gateway\ErrorMapper\ErrorMessageMapperInterface; -use Magento\Payment\Gateway\Helper\SubjectReader; -use Magento\Payment\Gateway\Response\HandlerInterface; -use Magento\Payment\Gateway\Validator\ResultInterface; -use Magento\Payment\Gateway\Validator\ValidatorInterface; - -class GatewayResponseCommand implements CommandInterface -{ - public const STATUS_FIELD = 'status'; - - public const STATUS_APPROVED = 'approved'; - - /** - * @var Command\Result\ArrayResultFactory - */ - protected $arrayResultFactory; - - /** - * @var HandlerInterface - */ - protected $orderUpdateHandler; - - /** - * @var ValidatorInterface - */ - protected $validator; - /** - * @var ErrorMessageMapperInterface - */ - private $errorMessageMapper; - - /** - * GatewayResponseCommand constructor. - * - * @param Command\Result\ArrayResultFactory $arrayResultFactory - * @param HandlerInterface $orderUpdateHandler - * @param ErrorMessageMapperInterface $errorMessageMapper - * @param ValidatorInterface|null $validator - */ - public function __construct( - Command\Result\ArrayResultFactory $arrayResultFactory, - HandlerInterface $orderUpdateHandler, - ErrorMessageMapperInterface $errorMessageMapper, - ?ValidatorInterface $validator = null - ) { - $this->arrayResultFactory = $arrayResultFactory; - $this->orderUpdateHandler = $orderUpdateHandler; - $this->validator = $validator; - $this->errorMessageMapper = $errorMessageMapper; - } - - /** - * Executes command basing on business object - * - * @param array $commandSubject - * @return null|Command\ResultInterface - * @throws CommandException - */ - public function execute(array $commandSubject) - { - if ($this->validator) { - $result = $this->validator->validate($commandSubject); - - if (!$result->isValid()) { - $this->processErrors($result); - } - } - $paymentDO = SubjectReader::readPayment($commandSubject); - $payment = $paymentDO->getPayment(); - - $response = [ - 'status' => $payment->getAdditionalInformation('gateway_status'), - 'response_code' => $payment->getAdditionalInformation('gateway_response_code'), - 'transaction_type' => $payment->getAdditionalInformation('gateway_transaction_type'), - 'approval_code' => $payment->getAdditionalInformation('gateway_approval_code'), - 'order_number' => $payment->getAdditionalInformation('monri_order_number'), - ]; - - $this->assertGatewayResponse($response); - - $this->orderUpdateHandler->handle($commandSubject, $response); - - $successfulPayment = $this->isPaymentApproved($response); - - $responseCode = $paymentDO->getPayment()->getAdditionalInformation('gateway_response_code'); - - $responseCodeMessage = null; - if ($responseCode) { - $responseCodeMessage = $this->errorMessageMapper->getMessage($responseCode); - } - - return $this->arrayResultFactory->create(['array' => [ - 'message' => $successfulPayment ? __('The payment has been accepted.') : __('The payment has been denied.'), - 'successful' => $successfulPayment, - 'response_code' => $responseCode, - 'response_code_message' => $responseCodeMessage, - ]]); - } - - /** - * Asserts the gateway response and throws an exception in case a crucial field is missing in the response. - * - * @param array $response - * @throws CommandException - */ - protected function assertGatewayResponse(array $response) - { - if (!array_key_exists(self::STATUS_FIELD, $response)) { - throw new CommandException(__('Status field missing.')); - } - } - - /** - * Checks to see if the gateway approved the transaction or not. - * - * @param array $response - * @return bool - */ - protected function isPaymentApproved(array $response) - { - return $response[self::STATUS_FIELD] === self::STATUS_APPROVED; - } - - /** - * Process errors - * - * @param ResultInterface $validationResult - * @throws CommandException - */ - protected function processErrors(ResultInterface $validationResult) - { - $errors = array_merge($validationResult->getErrorCodes(), $validationResult->getFailsDescription()); - - $message = !empty($errors) - ? __(implode(PHP_EOL, $errors)) - : __('There has been an issue processing your payment.'); - - throw new CommandException($message); - } -} diff --git a/etc/di.xml b/etc/di.xml index e1be42b..a8d1ca3 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -724,10 +724,9 @@ - + Monri\Payments\Gateway\Response\OrderUpdateHandler - MonriPaymentsErrorsWithRawMapper From 5dc5c00f41d7282fa0dea24a8bb37b6872fcc5cc Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 11:29:36 +0100 Subject: [PATCH 07/18] log fix --- Controller/GooglePay/Success.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php index d2afc17..7d46f36 100644 --- a/Controller/GooglePay/Success.php +++ b/Controller/GooglePay/Success.php @@ -90,14 +90,14 @@ public function execute() /** @var InfoInterface $payment */ $payment = $order->getPayment(); - $gatewayResponse = $this->getRequest()->getParams(); - $log['payload'] = $gatewayResponse; - //Google pay has no digest in success url. Instead, we get order status using API and save it in payment additonal info $this->commandManager->executeByCode('check_status', $payment); + $gatewayResponse = $this->buildGatewayResponse($payment); + $log['payload'] = $gatewayResponse; + $result = $this->commandManager->executeByCode('gateway_response', $payment, [ - 'response' => $this->buildGatewayResponse($payment), + 'response' => $gatewayResponse, ])->get(); $responseCodeMessage = $result['response_code_message'] ?? null; From 8fb1d24355a53737ed305ba1d509b621d334f657 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 11:33:11 +0100 Subject: [PATCH 08/18] magento coding standard. --- Controller/GooglePay/Success.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php index 7d46f36..0a4ca4f 100644 --- a/Controller/GooglePay/Success.php +++ b/Controller/GooglePay/Success.php @@ -90,7 +90,8 @@ public function execute() /** @var InfoInterface $payment */ $payment = $order->getPayment(); - //Google pay has no digest in success url. Instead, we get order status using API and save it in payment additonal info + //Google pay has no digest in success url. + // Instead, we get order status using API and save it in payment additonal info $this->commandManager->executeByCode('check_status', $payment); $gatewayResponse = $this->buildGatewayResponse($payment); From 739c88880dc089374bda7a8db7d0a5b0e0433f49 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 11:42:58 +0100 Subject: [PATCH 09/18] cleanup --- Block/GooglePay/Payment.php | 8 -------- view/frontend/layout/monripayments_googlepay_payment.xml | 2 +- view/frontend/templates/googlepay/payment.phtml | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 Block/GooglePay/Payment.php diff --git a/Block/GooglePay/Payment.php b/Block/GooglePay/Payment.php deleted file mode 100644 index d226d4d..0000000 --- a/Block/GooglePay/Payment.php +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/view/frontend/templates/googlepay/payment.phtml b/view/frontend/templates/googlepay/payment.phtml index 2124720..2d35d3b 100644 --- a/view/frontend/templates/googlepay/payment.phtml +++ b/view/frontend/templates/googlepay/payment.phtml @@ -1,5 +1,5 @@ getData('view_model')->getConfig(); ?> From 976c34779e4933d4c651bc207342235ded0ce99b Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 14:27:26 +0100 Subject: [PATCH 10/18] refactoring --- Controller/GooglePay/Cancel.php | 5 +---- Controller/GooglePay/Success.php | 9 ++------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Controller/GooglePay/Cancel.php b/Controller/GooglePay/Cancel.php index bd88a59..4812560 100644 --- a/Controller/GooglePay/Cancel.php +++ b/Controller/GooglePay/Cancel.php @@ -122,12 +122,9 @@ public function execute() */ private function buildCancelGatewayResponse(InfoInterface $payment): array { - $orderNumber = $payment->getAdditionalInformation('monri_order_number') - ?: $payment->getOrder()->getIncrementId(); - return [ 'status' => 'declined', - 'order_number' => $orderNumber, + 'order_number' => $payment->getAdditionalInformation('monri_order_number'), ]; } } diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php index 0a4ca4f..eaf4d2d 100644 --- a/Controller/GooglePay/Success.php +++ b/Controller/GooglePay/Success.php @@ -140,17 +140,12 @@ public function execute() */ private function buildGatewayResponse(InfoInterface $payment): array { - $responseCode = $payment->getAdditionalInformation('gateway_response_code'); - $orderNumber = $responseCode - ? $payment->getAdditionalInformation('monri_order_number') - : $payment->getOrder()->getIncrementId(); - return [ 'status' => $payment->getAdditionalInformation('gateway_status'), - 'response_code' => $responseCode, + 'response_code' => $payment->getAdditionalInformation('gateway_response_code'), 'transaction_type' => $payment->getAdditionalInformation('gateway_transaction_type'), 'approval_code' => $payment->getAdditionalInformation('gateway_approval_code'), - 'order_number' => $orderNumber, + 'order_number' => $payment->getAdditionalInformation('monri_order_number'), ]; } } From b8df74cd19789922f6e4b22365d13f052291c9a3 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 15:29:04 +0100 Subject: [PATCH 11/18] cleanup and refactoring --- .../Command/GooglePay/InitializeCommand.php | 4 - Gateway/Config/GooglePay.php | 11 - .../PaymentCreateTransferFactory.php | 2 +- .../Response/GooglePay/OrderUpdateHandler.php | 275 ------------------ etc/config.xml | 2 +- etc/di.xml | 1 + 6 files changed, 3 insertions(+), 292 deletions(-) delete mode 100644 Gateway/Response/GooglePay/OrderUpdateHandler.php diff --git a/Gateway/Command/GooglePay/InitializeCommand.php b/Gateway/Command/GooglePay/InitializeCommand.php index 9445931..a8338d3 100644 --- a/Gateway/Command/GooglePay/InitializeCommand.php +++ b/Gateway/Command/GooglePay/InitializeCommand.php @@ -80,10 +80,6 @@ public function execute(array $commandSubject) 40 ) ); - $payment->setAdditionalInformation( - 'transaction_type', - 'purchase' - ); } catch (LocalizedException $e) { $this->logger->debug(['Failed to set transaction type for payment: ' . $e->getMessage()]); } diff --git a/Gateway/Config/GooglePay.php b/Gateway/Config/GooglePay.php index 6442881..56ac41b 100644 --- a/Gateway/Config/GooglePay.php +++ b/Gateway/Config/GooglePay.php @@ -32,15 +32,4 @@ public function getComponentsJsURL($storeId = null) { return $this->getGatewayResourceURL('dist/components.js', $storeId); } - - /** - * Get configured payment action - * - * @param int|null $storeId - * @return string - */ - public function getPaymentAction($storeId = null) - { - return $this->getValue(self::PAYMENT_ACTION, $storeId); - } } diff --git a/Gateway/Http/Components/PaymentCreateTransferFactory.php b/Gateway/Http/Components/PaymentCreateTransferFactory.php index ea5493a..413de8f 100644 --- a/Gateway/Http/Components/PaymentCreateTransferFactory.php +++ b/Gateway/Http/Components/PaymentCreateTransferFactory.php @@ -14,7 +14,7 @@ use Magento\Payment\Gateway\Http\TransferFactoryInterface; use Magento\Payment\Gateway\Http\TransferInterface; use Monri\Payments\Model\Crypto\Components\Digest; -use Monri\Payments\Gateway\Config\GooglePay as Config; +use Monri\Payments\Gateway\Config; class PaymentCreateTransferFactory implements TransferFactoryInterface { diff --git a/Gateway/Response/GooglePay/OrderUpdateHandler.php b/Gateway/Response/GooglePay/OrderUpdateHandler.php deleted file mode 100644 index 9610193..0000000 --- a/Gateway/Response/GooglePay/OrderUpdateHandler.php +++ /dev/null @@ -1,275 +0,0 @@ - - */ - -namespace Monri\Payments\Gateway\Response\GooglePay; - -use Exception; -use Magento\Framework\Exception\AlreadyExistsException; -use Magento\Framework\Exception\InputException; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\ObjectManager\TMapFactory; -use Magento\Payment\Gateway\Command\CommandException; -use Magento\Payment\Gateway\Helper\SubjectReader; -use Magento\Payment\Gateway\Response\HandlerInterface; -use Magento\Payment\Model\InfoInterface; -use Magento\Payment\Model\Method\Logger; -use Magento\Sales\Api\Data\OrderInterface; -use Magento\Sales\Model\Order; -use Magento\Sales\Model\Order\Email\Sender\OrderSender; -use Magento\Sales\Model\Order\Payment; -use Magento\Sales\Model\OrderRepository; -use Monri\Payments\Gateway\Config; -use Monri\Payments\Gateway\Exception\TransactionAlreadyProcessedException; -use Monri\Payments\Lock\Order\LockInterface; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class OrderUpdateHandler implements HandlerInterface -{ - /** - * @var OrderRepository - */ - private $orderRepository; - - /** - * @var OrderSender - */ - private $orderSender; - - /** - * @var Config - */ - private $config; - - /** - * @var Logger - */ - private $logger; - - /** - * @var LockInterface - */ - private $locker; - - /** - * @var HandlerInterface[] - */ - private $transactionHandlers; - - /** - * @var HandlerInterface - */ - private $unsuccessfulTransactionHandler; - - /** - * OrderUpdateHandler constructor. - * - * @param OrderRepository $orderRepository - * @param OrderSender $orderSender - * @param Logger $logger - * @param TMapFactory $TMapFactory - * @param array $transactionHandlers - * @param HandlerInterface $unsuccessfulTransactionHandler - * @param Config $config - * @param LockInterface $locker - */ - public function __construct( - OrderRepository $orderRepository, - OrderSender $orderSender, - Logger $logger, - TMapFactory $TMapFactory, - array $transactionHandlers, - HandlerInterface $unsuccessfulTransactionHandler, - Config $config, - LockInterface $locker - ) { - $this->orderRepository = $orderRepository; - $this->orderSender = $orderSender; - $this->config = $config; - $this->logger = $logger; - $this->locker = $locker; - - $this->transactionHandlers = $TMapFactory->create([ - 'type' => HandlerInterface::class, - 'array' => $transactionHandlers - ]); - - $this->unsuccessfulTransactionHandler = $unsuccessfulTransactionHandler; - } - - /** - * Handles response - * - * @param array $handlingSubject - * @param array $response - * @return void - * @throws AlreadyExistsException - * @throws InputException - * @throws NoSuchEntityException - * @throws CommandException - */ - public function handle(array $handlingSubject, array $response) - { - $log = [ - 'location' => __METHOD__, - 'errors' => [], - 'action' => null, - 'email_sent' => false, - ]; - - if (!isset($response['status'])) { - $log['errors'][] = 'Status not set in response, invalid response.'; - $this->logger->debug($log); - return; - } - - $paymentDataObject = SubjectReader::readPayment($handlingSubject); - - /** @var Payment $payment */ - $payment = $paymentDataObject->getPayment(); - - /** @var Order $order */ - $order = $payment->getOrder(); - - $this->ensureNotLocked($order); - $this->locker->lock($order->getId()); - - if (!isset($response['transaction_type'])) { - $response['transaction_type'] = $this->getTransactionTypeFromPayment($payment, $order); - } - - try { - $this->processResponse($handlingSubject, $response, $order); - - if (!$order->getEmailSent() && $this->isSuccessfulResponse($response)) { - $log['email_sent'] = true; - $this->orderSender->send($order); - } - } finally { - $this->locker->unlock($order->getId()); - $this->logger->debug($log); - } - } - - /** - * Processes the response - * - * @param array $handlingSubject - * @param array $response - * @param OrderInterface $order - * @return void - * @throws AlreadyExistsException - * @throws CommandException - * @throws InputException - * @throws NoSuchEntityException - * @throws TransactionAlreadyProcessedException - */ - protected function processResponse( - array $handlingSubject, - array $response, - OrderInterface $order - ): void { - try { - if ($this->isSuccessfulResponse($response)) { - $this->processSuccessfulGatewayResponse($handlingSubject, $response); - } else { - $this->processUnsuccessfulGatewayResponse($handlingSubject, $response); - } - } catch (TransactionAlreadyProcessedException $e) { - // pass through TransactionAlreadyProcessedException - throw $e; - } catch (Exception $e) { - throw new CommandException(__('Failed to process transaction: %1', $e->getMessage())); - } - - $this->orderRepository->save($order); - } - - /** - * Performs any necessary actions for a given transaction type. - * - * @param array $handlingSubject - * @param array $response - */ - protected function processSuccessfulGatewayResponse( - array $handlingSubject, - array $response - ) { - $transactionType = isset($response['transaction_type']) ? $response['transaction_type'] : null; - - if (isset($this->transactionHandlers[$transactionType])) { - $this->transactionHandlers[$transactionType]->handle($handlingSubject, $response); - } - } - - /** - * Processes an unsuccessful transaction. - * - * @param array $handlingSubject - * @param array $response - */ - protected function processUnsuccessfulGatewayResponse( - array $handlingSubject, - array $response - ) { - $this->unsuccessfulTransactionHandler->handle($handlingSubject, $response); - } - - /** - * Determines if a given response is successful. - * - * @param array $response - * @return bool - */ - protected function isSuccessfulResponse(array $response) - { - $isSuccessCode = isset($response['response_code']) ? $response['response_code'] === '0000' : true; - - return $isSuccessCode && $response['status'] === 'approved'; - } - - /** - * Returns the transaction type. - * - * @param InfoInterface $payment - * @param Order $order - * @return string - */ - protected function getTransactionTypeFromPayment(InfoInterface $payment, Order $order) - { - $transactionType = $payment->getAdditionalInformation('transaction_type'); - - if (!$transactionType) { - $transactionType = $this->config->getTransactionType($order->getStoreId()); - } - - return $transactionType; - } - - /** - * Throws TransactionAlreadyProcessedException on locked order - * - * @param OrderInterface $order - * @throws TransactionAlreadyProcessedException - * @return void - */ - protected function ensureNotLocked(OrderInterface $order): void - { - $orderId = $order->getId(); - if ($this->locker->isLocked($orderId)) { - $this->logger->debug([ - 'message' => __('Order is currently being processed (lock). Order ID: %1', $orderId), - 'log_origin' => __METHOD__ - ]); - - throw new TransactionAlreadyProcessedException(__('Order lock. Order ID: %1', $orderId)); - } - } -} diff --git a/etc/config.xml b/etc/config.xml index 0eda835..1098b9b 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -91,7 +91,7 @@ 1 1 1 - 1 + 0 1 diff --git a/etc/di.xml b/etc/di.xml index a8d1ca3..5d5c319 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -802,6 +802,7 @@ MonriGooglePayCreateRequestCommand + MonriPaymentsLogger From 733e5e571affe315709a2abdf9796b0b28bf5c1d Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 25 Mar 2026 15:52:10 +0100 Subject: [PATCH 12/18] fix config for components --- etc/di.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/etc/di.xml b/etc/di.xml index 5d5c319..5d768d2 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -564,13 +564,19 @@ Monri\Payments\Gateway\Request\Components\OrderDetailsBuilder - Monri\Payments\Gateway\Http\Components\PaymentCreateTransferFactory + MonriComponentsPaymentCreateTransferFactory MonriComponentsJsonClient Monri\Payments\Gateway\Response\Components\PaymentCreateHandler MonriPaymentsVirtualLogger + + + Monri\Payments\Gateway\Config\Components + + + Monri\Payments\Gateway\Validator\Components\OrderValidator From e76070fa2fa8391abe64dc6aaa28e98c6191b7f0 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Fri, 27 Mar 2026 09:18:51 +0100 Subject: [PATCH 13/18] version bump --- CHANGELOG.md | 3 +++ composer.json | 2 +- etc/adminhtml/system.xml | 8 ++++---- view/frontend/web/js/view/monri_components.js | 2 +- view/frontend/web/js/view/monri_google_pay.js | 2 +- view/frontend/web/js/view/monri_payments.js | 2 +- view/frontend/web/js/view/monri_wspay.js | 2 +- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4b617..84564bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 1.8.0 +- Monri Components GooglePay integration + ### 1.7.0 - Improved Monri WebPay tokenization by using API for transaction creation. - Added "Instant Purchase" on the product page for customers with saved Monri WebPay tokens. diff --git a/composer.json b/composer.json index c3fde62..3907ddd 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ } ], "description": "The official Monri Payments Magento 2 module", - "version": "1.7.0", + "version": "1.8.0", "license": [ "OSL-3.0" ], diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 7e101a0..aa9a355 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -19,7 +19,7 @@ showInDefault="1" showInWebsite="1" showInStore="0"> Magento\Config\Model\Config\Source\Yesno - +
@@ -105,7 +105,7 @@ showInDefault="1" showInWebsite="1" showInStore="0"> Magento\Config\Model\Config\Source\Yesno - + @@ -180,7 +180,7 @@ showInDefault="1" showInWebsite="1" showInStore="0"> Magento\Config\Model\Config\Source\Yesno - + @@ -243,7 +243,7 @@ Magento\Config\Model\Config\Source\Yesno - + diff --git a/view/frontend/web/js/view/monri_components.js b/view/frontend/web/js/view/monri_components.js index 59ff1d9..07c4391 100644 --- a/view/frontend/web/js/view/monri_components.js +++ b/view/frontend/web/js/view/monri_components.js @@ -4,7 +4,7 @@ * (c) Monri Payments d.o.o. * * @author Favicode - * @version 1.7.0 + * @version 1.8.0 */ define( diff --git a/view/frontend/web/js/view/monri_google_pay.js b/view/frontend/web/js/view/monri_google_pay.js index 8f0ce60..a95d397 100644 --- a/view/frontend/web/js/view/monri_google_pay.js +++ b/view/frontend/web/js/view/monri_google_pay.js @@ -4,7 +4,7 @@ * (c) Monri Payments d.o.o. * * @author Favicode - * @version 1.7.0 + * @version 1.8.0 */ define( diff --git a/view/frontend/web/js/view/monri_payments.js b/view/frontend/web/js/view/monri_payments.js index d9cbb0c..0c7a0f7 100644 --- a/view/frontend/web/js/view/monri_payments.js +++ b/view/frontend/web/js/view/monri_payments.js @@ -4,7 +4,7 @@ * (c) Monri Payments d.o.o. * * @author Favicode - * @version 1.7.0 + * @version 1.8.0 */ define( diff --git a/view/frontend/web/js/view/monri_wspay.js b/view/frontend/web/js/view/monri_wspay.js index 40db9fb..d1cc7e2 100644 --- a/view/frontend/web/js/view/monri_wspay.js +++ b/view/frontend/web/js/view/monri_wspay.js @@ -4,7 +4,7 @@ * (c) Monri Payments d.o.o. * * @author Favicode - * @version 1.7.0 + * @version 1.8.0 */ define([ From 522296e1788b38791e4a559f664e95dbf0f94780 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Fri, 27 Mar 2026 13:01:12 +0100 Subject: [PATCH 14/18] cleanup --- etc/di.xml | 11 ++++++++++- view/frontend/web/js/view/init/googlepay-init.js | 5 +++-- .../web/js/view/method-renderer/monri_google_pay.js | 1 - 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/etc/di.xml b/etc/di.xml index 5d768d2..dc7cb15 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -732,7 +732,16 @@ - Monri\Payments\Gateway\Response\OrderUpdateHandler + MonriGooglePayOrderUpdateHandler + MonriPaymentsErrorsMapper + + + + + + + Monri\Payments\Gateway\Response\Transactions\PurchaseHandler + diff --git a/view/frontend/web/js/view/init/googlepay-init.js b/view/frontend/web/js/view/init/googlepay-init.js index 06c6c5f..0627d10 100644 --- a/view/frontend/web/js/view/init/googlepay-init.js +++ b/view/frontend/web/js/view/init/googlepay-init.js @@ -58,12 +58,13 @@ define([ window.location.href = urlBuilder.build('monripayments/googlepay/cancel'); } - if (event.data.type === 'PAYMENT_RESULT') { + if (event.data?.type === 'PAYMENT_RESULT') { const {transaction} = event.data; if (transaction.status === 'approved') { window.location.href = urlBuilder.build('monripayments/googlepay/success'); } else { - console.log('error') + customerData.invalidate(['cart', 'checkout-data']); + window.location.href = urlBuilder.build('monripayments/googlepay/cancel'); } } }); diff --git a/view/frontend/web/js/view/method-renderer/monri_google_pay.js b/view/frontend/web/js/view/method-renderer/monri_google_pay.js index 68aefb0..9182fbb 100644 --- a/view/frontend/web/js/view/method-renderer/monri_google_pay.js +++ b/view/frontend/web/js/view/method-renderer/monri_google_pay.js @@ -41,7 +41,6 @@ define( 'method': this.getCode(), 'additional_data': {} }; - console.log('data: ', data) return data; }, From 3f4d9605a89820106201151f259c11b06f079604 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Tue, 31 Mar 2026 10:31:07 +0200 Subject: [PATCH 15/18] improved order id when generating client secret --- Gateway/Command/GooglePay/InitializeCommand.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Gateway/Command/GooglePay/InitializeCommand.php b/Gateway/Command/GooglePay/InitializeCommand.php index a8338d3..109df98 100644 --- a/Gateway/Command/GooglePay/InitializeCommand.php +++ b/Gateway/Command/GooglePay/InitializeCommand.php @@ -19,6 +19,7 @@ use Magento\Sales\Model\Order\Payment; use Monri\Payments\Gateway\Config\GooglePay as Config; use Monri\Payments\Helper\Formatter; +use Monri\Payments\Gateway\Helper\TestModeHelper; class InitializeCommand implements CommandInterface { @@ -73,10 +74,14 @@ public function execute(array $commandSubject) $payment->getOrder()->setCanSendNewEmailFlag(false); try { + $orderId = $payment->getOrder()->getIncrementId(); + if ($this->config->getValue('sandbox')) { + $orderId = TestModeHelper::generateTestOrderId($orderId); + } $payment->setAdditionalInformation( 'monri_order_number', $this->formatter->formatText( - $payment->getOrder()->getIncrementId() . uniqid('-'), + $orderId, 40 ) ); From 4e021ea7483cf39662c2dee120bfadf03c50fb68 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 8 Apr 2026 14:50:59 +0200 Subject: [PATCH 16/18] added order status controller in order to check status from frontend --- Controller/GooglePay/OrderStatus.php | 140 +++++++++++++++++++++++++++ etc/di.xml | 7 ++ 2 files changed, 147 insertions(+) create mode 100644 Controller/GooglePay/OrderStatus.php diff --git a/Controller/GooglePay/OrderStatus.php b/Controller/GooglePay/OrderStatus.php new file mode 100644 index 0000000..1b35620 --- /dev/null +++ b/Controller/GooglePay/OrderStatus.php @@ -0,0 +1,140 @@ + + */ + +namespace Monri\Payments\Controller\GooglePay; + +use Exception; +use Magento\Checkout\Model\Session; +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Payment\Gateway\Command\CommandManagerInterface; +use Magento\Payment\Model\Method\Logger; +use Magento\Sales\Model\OrderRepository; +use Monri\Payments\Model\GetOrderIdByIncrement; + +/** + * AJAX endpoint for polling Google Pay order payment status. + * + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ +class OrderStatus extends Action +{ + /** + * @var Session + */ + private $checkoutSession; + + /** + * @var OrderRepository + */ + private $orderRepository; + + /** + * @var CommandManagerInterface + */ + private $commandManager; + + /** + * @var Logger + */ + private $logger; + + /** + * @var GetOrderIdByIncrement + */ + private $getOrderIdByIncrement; + + /** + * OrderStatus constructor. + * + * @param Context $context + * @param Session $checkoutSession + * @param OrderRepository $orderRepository + * @param CommandManagerInterface $commandManager + * @param GetOrderIdByIncrement $getOrderIdByIncrement + * @param Logger $logger + */ + public function __construct( + Context $context, + Session $checkoutSession, + OrderRepository $orderRepository, + CommandManagerInterface $commandManager, + GetOrderIdByIncrement $getOrderIdByIncrement, + Logger $logger + ) { + parent::__construct($context); + + $this->checkoutSession = $checkoutSession; + $this->orderRepository = $orderRepository; + $this->commandManager = $commandManager; + $this->getOrderIdByIncrement = $getOrderIdByIncrement; + $this->logger = $logger; + } + + /** + * Poll order payment status and return JSON. + * + * Returns: + * {"status": "approved"} – payment was approved + * {"status": "pending"} – payment not yet finalised + * {"status": "error", "message": "..."} – unrecoverable error + * + * @return Json + */ + public function execute() + { + $log = [ + 'location' => __METHOD__, + 'errors' => [], + ]; + + /** @var Json $result */ + $result = $this->resultFactory->create(ResultFactory::TYPE_JSON); + + try { + $orderId = $this->checkoutSession->getData('last_order_id'); + + if (!$orderId) { + $result->setData(['status' => 'error', 'message' => 'Order not found.']); + return $result; + } + + $order = $this->orderRepository->get($orderId); + $payment = $order->getPayment(); + + // Run check_status which fetches the latest status from Monri API + // and stores it in payment additional information. + $this->commandManager->executeByCode('check_status', $payment); + + $gatewayStatus = $payment->getAdditionalInformation('gateway_status'); + $log['gateway_status'] = $gatewayStatus; + + if ($gatewayStatus === 'approved') { + $result->setData(['status' => 'approved']); + } else { + $result->setData(['status' => 'pending']); + } + } catch (InputException | NoSuchEntityException $e) { + $log['errors'][] = 'Order not found: ' . $e->getMessage(); + $result->setData(['status' => 'error', 'message' => 'Order not found.']); + } catch (Exception $e) { + $log['errors'][] = 'Exception: ' . $e->getMessage(); + $result->setData(['status' => 'error', 'message' => 'Error checking order status.']); + } finally { + $this->logger->debug($log); + } + + return $result; + } +} + diff --git a/etc/di.xml b/etc/di.xml index dc7cb15..ab2caa1 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -776,6 +776,13 @@ + + + MonriGooglePayCommandManager + MonriPaymentsLogger + + + From b9ef886c99b43418fbaf5eac6108275d9cc3655a Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 8 Apr 2026 15:04:37 +0200 Subject: [PATCH 17/18] moved out status handler out of google pay --- .../{GooglePay => }/StatusHandler.php | 19 +++++++++++++------ etc/di.xml | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) rename Gateway/Response/{GooglePay => }/StatusHandler.php (71%) diff --git a/Gateway/Response/GooglePay/StatusHandler.php b/Gateway/Response/StatusHandler.php similarity index 71% rename from Gateway/Response/GooglePay/StatusHandler.php rename to Gateway/Response/StatusHandler.php index ebf3986..b8a1e92 100644 --- a/Gateway/Response/GooglePay/StatusHandler.php +++ b/Gateway/Response/StatusHandler.php @@ -1,6 +1,13 @@ + */ -namespace Monri\Payments\Gateway\Response\GooglePay; +namespace Monri\Payments\Gateway\Response; use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Response\HandlerInterface; @@ -10,11 +17,10 @@ class StatusHandler implements HandlerInterface { /** - * Updates payment additional information with gateway response details. - * - * @param array $handlingSubject The handling subject containing contextual information for processing. - * @param array $response The response data received from the payment gateway. + * Updates payment additional information with gateway status response details. * + * @param array $handlingSubject + * @param array $response * @return void * @throws LocalizedException */ @@ -24,10 +30,11 @@ public function handle(array $handlingSubject, array $response) /** @var Payment $payment */ $payment = $paymentDO->getPayment(); - // Save gateway status info for use is payment handler + // Save gateway status info for use in payment handler $payment->setAdditionalInformation('gateway_status', $response['status'] ?? null); $payment->setAdditionalInformation('gateway_response_code', $response['response-code'] ?? null); $payment->setAdditionalInformation('gateway_transaction_type', $response['transaction-type'] ?? null); $payment->setAdditionalInformation('gateway_approval_code', $response['approval-code'] ?? null); } } + diff --git a/etc/di.xml b/etc/di.xml index ab2caa1..619c3b1 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -840,7 +840,7 @@ MonriGooglePayStatusRequestBuilder MonriGooglePayOrderStatusTransferFactory Monri\Payments\Gateway\Http\StatusClient - Monri\Payments\Gateway\Response\GooglePay\StatusHandler + Monri\Payments\Gateway\Response\StatusHandler MonriPaymentsVirtualLogger From 97b97ea87866370f849c4424c4b8327404f54bc1 Mon Sep 17 00:00:00 2001 From: David Runtic Date: Wed, 8 Apr 2026 15:06:47 +0200 Subject: [PATCH 18/18] coding standard --- Controller/GooglePay/OrderStatus.php | 1 - Gateway/Response/StatusHandler.php | 1 - 2 files changed, 2 deletions(-) diff --git a/Controller/GooglePay/OrderStatus.php b/Controller/GooglePay/OrderStatus.php index 1b35620..f929f62 100644 --- a/Controller/GooglePay/OrderStatus.php +++ b/Controller/GooglePay/OrderStatus.php @@ -137,4 +137,3 @@ public function execute() return $result; } } - diff --git a/Gateway/Response/StatusHandler.php b/Gateway/Response/StatusHandler.php index b8a1e92..178fdfd 100644 --- a/Gateway/Response/StatusHandler.php +++ b/Gateway/Response/StatusHandler.php @@ -37,4 +37,3 @@ public function handle(array $handlingSubject, array $response) $payment->setAdditionalInformation('gateway_approval_code', $response['approval-code'] ?? null); } } -