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/Controller/GooglePay/Cancel.php b/Controller/GooglePay/Cancel.php new file mode 100644 index 0000000..4812560 --- /dev/null +++ b/Controller/GooglePay/Cancel.php @@ -0,0 +1,130 @@ + + */ + +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') + ); + + /** @var InfoInterface $payment */ + $payment = $order->getPayment(); + + $gatewayResponse = $this->buildCancelGatewayResponse($payment); + $log['payload'] = $gatewayResponse; + + $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'); + } + + /** + * Build minimal cancel response for Google Pay gateway_response command. + * + * @param InfoInterface $payment + * @return array + */ + private function buildCancelGatewayResponse(InfoInterface $payment): array + { + return [ + 'status' => 'declined', + 'order_number' => $payment->getAdditionalInformation('monri_order_number'), + ]; + } +} diff --git a/Controller/GooglePay/OrderStatus.php b/Controller/GooglePay/OrderStatus.php new file mode 100644 index 0000000..f929f62 --- /dev/null +++ b/Controller/GooglePay/OrderStatus.php @@ -0,0 +1,139 @@ + + */ + +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/Controller/GooglePay/Payment.php b/Controller/GooglePay/Payment.php new file mode 100644 index 0000000..ee5813d --- /dev/null +++ b/Controller/GooglePay/Payment.php @@ -0,0 +1,26 @@ +resultPageFactory->create(); + } +} diff --git a/Controller/GooglePay/Success.php b/Controller/GooglePay/Success.php new file mode 100644 index 0000000..eaf4d2d --- /dev/null +++ b/Controller/GooglePay/Success.php @@ -0,0 +1,151 @@ + + */ + +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\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; +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(); + + //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' => $gatewayResponse, + ])->get(); + $responseCodeMessage = $result['response_code_message'] ?? null; + + if ($responseCodeMessage !== null) { + $responseCodeMessage = (string)$responseCodeMessage; + $this->messageManager->addNoticeMessage( + __('The payment has been accepted: %1', $responseCodeMessage) + ); + } 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'); + } + + /** + * Build response payload consumed by GooglePay gateway_response command. + * + * @param InfoInterface $payment + * @return array + */ + private function buildGatewayResponse(InfoInterface $payment): array + { + return [ + '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'), + ]; + } +} 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..109df98 --- /dev/null +++ b/Gateway/Command/GooglePay/InitializeCommand.php @@ -0,0 +1,95 @@ + + */ + +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; +use Monri\Payments\Helper\Formatter; +use Monri\Payments\Gateway\Helper\TestModeHelper; + +class InitializeCommand implements CommandInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @var Logger + */ + private $logger; + + /** + * @var Formatter + */ + private $formatter; + + /** + * InitializeCommand constructor. + * + * @param Config $config + * @param Logger $logger + * @param Formatter $formatter + */ + public function __construct( + Config $config, + Logger $logger, + Formatter $formatter + ) { + $this->config = $config; + $this->logger = $logger; + $this->formatter = $formatter; + } + + /** + * 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 { + $orderId = $payment->getOrder()->getIncrementId(); + if ($this->config->getValue('sandbox')) { + $orderId = TestModeHelper::generateTestOrderId($orderId); + } + $payment->setAdditionalInformation( + 'monri_order_number', + $this->formatter->formatText( + $orderId, + 40 + ) + ); + } 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..56ac41b --- /dev/null +++ b/Gateway/Config/GooglePay.php @@ -0,0 +1,35 @@ +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); + } +} 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..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\Components as Config; +use Monri\Payments\Gateway\Config; class PaymentCreateTransferFactory implements TransferFactoryInterface { diff --git a/Gateway/Http/StatusClient.php b/Gateway/Http/StatusClient.php new file mode 100644 index 0000000..4823c9f --- /dev/null +++ b/Gateway/Http/StatusClient.php @@ -0,0 +1,157 @@ + __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 new file mode 100644 index 0000000..58e99c2 --- /dev/null +++ b/Gateway/Request/GooglePay/OrderDetailsBuilder.php @@ -0,0 +1,112 @@ + + */ + +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 + */ + 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); + $payment = $paymentDataObject->getPayment(); + + /** @var \Magento\Payment\Gateway\Data\Quote\QuoteAdapter $order */ + $order = $paymentDataObject->getOrder(); + + $orderNumber = $payment->getAdditionalInformation('monri_order_number'); + + $orderIpAddress = $paymentDataObject->getPayment()->getOrder()->getRemoteIp(); + + $orderInfo = __('Order: %1', $order->getOrderIncrementId())->render(); + + //Google Pay only supports purchase transactions + $transactionType = 'purchase'; + + $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) { + $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 => $transactionType, + ]; + } +} diff --git a/Gateway/Request/StatusRequestBuilder.php b/Gateway/Request/StatusRequestBuilder.php new file mode 100644 index 0000000..250c42b --- /dev/null +++ b/Gateway/Request/StatusRequestBuilder.php @@ -0,0 +1,75 @@ +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/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/Gateway/Response/StatusHandler.php b/Gateway/Response/StatusHandler.php new file mode 100644 index 0000000..178fdfd --- /dev/null +++ b/Gateway/Response/StatusHandler.php @@ -0,0 +1,39 @@ + + */ + +namespace Monri\Payments\Gateway\Response; + +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 status response details. + * + * @param array $handlingSubject + * @param array $response + * @return void + * @throws LocalizedException + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = SubjectReader::readPayment($handlingSubject); + /** @var Payment $payment */ + $payment = $paymentDO->getPayment(); + + // 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/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/Model/Crypto/Components/OrderStatusDigest.php b/Model/Crypto/Components/OrderStatusDigest.php new file mode 100644 index 0000000..2829c9b --- /dev/null +++ b/Model/Crypto/Components/OrderStatusDigest.php @@ -0,0 +1,32 @@ +config->getClientKey($storeId); + $data = $key . $order_number; + + return hash('SHA1', $data); + } +} diff --git a/ViewModel/GooglePayConfig.php b/ViewModel/GooglePayConfig.php new file mode 100644 index 0000000..286fde6 --- /dev/null +++ b/ViewModel/GooglePayConfig.php @@ -0,0 +1,79 @@ +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/composer.json b/composer.json index 39014f4..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" ], @@ -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/adminhtml/system.xml b/etc/adminhtml/system.xml index 5bec32f..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 - + @@ -173,12 +173,77 @@ 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 + + Magento\Config\Model\Config\Source\Yesno - + diff --git a/etc/config.xml b/etc/config.xml index d292527..1098b9b 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 + 0 + 1 + + + 0 diff --git a/etc/di.xml b/etc/di.xml index ba9ff5d..619c3b1 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 @@ -678,6 +684,166 @@ + + + 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 + + + + + + MonriGooglePayOrderUpdateHandler + MonriPaymentsErrorsMapper + + + + + + + Monri\Payments\Gateway\Response\Transactions\PurchaseHandler + + + + + + + + MonriGooglePayCreateRequestCommand + Monri\Payments\Gateway\Command\GooglePay\InitializeCommand + MonriGooglePayGatewayResponseCommand + MonriGooglePayCheckStatusCommand + + + + + + + MonriGooglePayCommandPool + + + + + + MonriGooglePayCommandManager + MonriPaymentsLogger + + + + + + MonriGooglePayCommandManager + MonriPaymentsLogger + + + + + + MonriGooglePayCommandManager + MonriPaymentsLogger + + + + + + + Monri\Payments\Gateway\Config\GooglePay + + + + + + Monri\Payments\Gateway\Config\GooglePay + + + + + + + Monri\Payments\Gateway\Config\GooglePay + MonriGooglePayDigest + + + + + + Monri\Payments\Gateway\Config\GooglePay + MonriGooglePayOrderStatusDigest + + + + + + Monri\Payments\Gateway\Request\GooglePay\OrderDetailsBuilder + MonriGooglePayPaymentCreateTransferFactory + MonriComponentsJsonClient + Monri\Payments\Gateway\Response\GooglePay\PaymentCreateHandler + MonriPaymentsVirtualLogger + + + + + + MonriGooglePayCreateRequestCommand + MonriPaymentsLogger + + + + + + Monri\Payments\Gateway\Config\GooglePay + MonriGooglePayOrderStatusDigest + + + + + + MonriGooglePayStatusRequestBuilder + MonriGooglePayOrderStatusTransferFactory + Monri\Payments\Gateway\Http\StatusClient + Monri\Payments\Gateway\Response\StatusHandler + MonriPaymentsVirtualLogger + + 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..c01729c --- /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..2d35d3b --- /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..0627d10 --- /dev/null +++ b/view/frontend/web/js/view/init/googlepay-init.js @@ -0,0 +1,82 @@ +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') { + window.location.href = urlBuilder.build('monripayments/googlepay/success'); + } else { + customerData.invalidate(['cart', 'checkout-data']); + window.location.href = urlBuilder.build('monripayments/googlepay/cancel'); + } + } + }); + } + + 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..9182fbb --- /dev/null +++ b/view/frontend/web/js/view/method-renderer/monri_google_pay.js @@ -0,0 +1,54 @@ +/** + * 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': {} + }; + 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_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 new file mode 100644 index 0000000..a95d397 --- /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.8.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/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([ 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 @@ +
+
+ + +
+
+ + + +
+
+ + + +
+
+ +
+ + + +
+ +
+ +
+
+ +
+
+
+