From 72a3d05e39f0144e30266e5741d2d7c95970feaf Mon Sep 17 00:00:00 2001 From: Aashwin Mohan <96258159+aashwin-rvvup@users.noreply.github.com> Date: Tue, 21 Feb 2023 14:36:32 +0000 Subject: [PATCH] Feature: Paypal PDP flow (#16) --- Api/CartExpressPaymentRemoveInterface.php | 16 + Api/CartPaymentActionsGetInterface.php | 18 + Api/CartResetInterface.php | 19 + Api/ExpressPaymentCreateInterface.php | 22 + ...GuestCartExpressPaymentRemoveInterface.php | 17 + Api/GuestCartPaymentActionsGetInterface.php | 19 + Api/GuestCartResetInterface.php | 19 + Api/GuestExpressPaymentCreateInterface.php | 20 + Api/GuestPaymentActionsGetInterface.php | 2 + Api/PaymentActionsGetInterface.php | 17 - Api/PaymentMethodsAssetsGetInterface.php | 2 +- Api/PaymentMethodsSettingsGetInterface.php | 18 + Controller/Express/Create.php | 331 +++++++++++++++ Controller/Redirect/Cancel.php | 15 +- CustomerData/ExpressPayment.php | 147 +++++++ Exception/InputValidationException.php | 12 + Gateway/Command/CanRefund.php | 3 +- Gateway/Command/Capture.php | 5 +- Gateway/Http/Client/TransactionInitialize.php | 41 +- Gateway/Method.php | 8 + Gateway/Request/InitializeDataBuilder.php | 106 ++++- .../Response/InitializeResponseHandler.php | 152 +++++-- .../Validator/InitializeResponseValidator.php | 8 +- Model/ApiSettingsProvider.php | 49 +++ Model/CartExpressPaymentRemove.php | 104 +++++ Model/CartPaymentActionsGet.php | 155 +++++++ Model/CartReset.php | 45 ++ Model/Checks/HasCartExpressPayment.php | 72 ++++ .../Checks/HasCartExpressPaymentInterface.php | 16 + Model/ConfigProvider.php | 335 ++++++++++++++- Model/ExpressPaymentCreate.php | 90 ++++ Model/GuestCartExpressPaymentRemove.php | 46 ++ Model/GuestCartPaymentActionsGet.php | 50 +++ Model/GuestCartReset.php | 50 +++ Model/GuestExpressPaymentCreate.php | 51 +++ Model/GuestPaymentActionsGet.php | 41 +- Model/IsPaymentMethodAvailable.php | 28 +- Model/OrderDataBuilder.php | 298 ++++++++++--- Model/Payment/PaymentCreateExpress.php | 177 ++++++++ .../Payment/PaymentCreateExpressInterface.php | 21 + Model/PaymentMethodsAssetsGet.php | 59 +-- Model/PaymentMethodsAvailableGet.php | 9 +- Model/PaymentMethodsSettingsGet.php | 65 +++ Model/ProcessOrder/Cancel.php | 2 +- Model/ProcessOrder/Complete.php | 2 +- Model/ProcessOrder/Processing.php | 2 +- Model/Queue/Handler/Handler.php | 3 +- Model/SdkProxy.php | 82 +++- Model/Validation/IsValidAddress.php | 47 +++ Observer/DataAssignObserver.php | 12 +- .../Item/RemoveExpressPaymentDataObserver.php | 63 +++ .../RemoveExpressPaymentDataObserver.php | 42 ++ .../ExpressPaymentValidateCustomerAddress.php | 83 ++++ Plugin/JsLayout.php | 3 +- .../LimitCartExpressPayment.php | 44 ++ ...RemoveExpressPaymentInfoOnMethodChange.php | 70 ++++ ViewModel/Assets.php | 65 +++ ViewModel/CheckoutConfig.php | 80 ++++ ViewModel/PayPal.php | 146 +++++++ etc/di.xml | 92 +++- etc/events.xml | 6 + etc/frontend/di.xml | 71 +++- etc/frontend/events.xml | 14 +- etc/frontend/sections.xml | 92 ++++ etc/webapi.xml | 66 ++- etc/webapi_rest/di.xml | 21 +- i18n/en_US.csv | 8 +- view/frontend/layout/catalog_product_view.xml | 31 +- ...catalog_product_view_type_configurable.xml | 7 + ...catalog_product_view_type_downloadable.xml | 15 + view/frontend/requirejs-config.js | 6 + .../body/before-end/checkout-config.phtml | 29 ++ .../templates/head/additional/assets.phtml | 15 +- .../templates/product/view/addtocart.phtml | 114 ++--- .../product/view/info/addtocart.phtml | 33 ++ view/frontend/web/css/source/_module.less | 8 + view/frontend/web/js/action/add-to-cart.js | 35 ++ .../payment/get-order-payment-actions.js | 103 +++++ .../payment/remove-express-payment.js | 31 ++ view/frontend/web/js/action/create-cart.js | 31 ++ .../web/js/action/create-express-payment.js | 35 ++ view/frontend/web/js/action/empty-cart.js | 35 ++ .../web/js/action/remove-express-payment.js | 34 ++ .../web/js/action/set-cart-billing-address.js | 35 ++ .../web/js/action/set-session-message.js | 37 ++ .../web/js/checkout-data-resolver-mixin.js | 80 ++++ .../web/js/helper/checkout-data-helper.js | 62 +++ .../web/js/helper/get-add-to-cart-payload.js | 38 ++ .../web/js/helper/get-current-quote-id.js | 14 + view/frontend/web/js/helper/get-form-data.js | 43 ++ .../get-paypal-checkout-button-style.js | 45 ++ .../js/helper/get-paypal-pdp-button-style.js | 45 ++ view/frontend/web/js/helper/get-pdp-form.js | 10 + view/frontend/web/js/helper/get-store-code.js | 24 ++ .../web/js/helper/is-express-payment.js | 9 + view/frontend/web/js/helper/is-logged-in.js | 9 + .../js/helper/is-paypal-pdp-button-enabled.js | 12 + .../web/js/helper/validate-pdp-form.js | 17 + view/frontend/web/js/method/paypal/button.js | 392 ++++++++++++++++++ .../checkout/payment/order-payment-action.js | 84 ++++ .../payment/rvvup-method-properties.js | 67 +++ .../js/model/customer-data/express-payment.js | 49 +++ .../web/js/view/billing-address-mixin.js | 29 ++ .../payment/method-renderer/rvvup-method.js | 350 ++++++++++------ view/frontend/web/template/payment/rvvup.html | 27 +- 105 files changed, 5474 insertions(+), 460 deletions(-) create mode 100644 Api/CartExpressPaymentRemoveInterface.php create mode 100644 Api/CartPaymentActionsGetInterface.php create mode 100644 Api/CartResetInterface.php create mode 100644 Api/ExpressPaymentCreateInterface.php create mode 100644 Api/GuestCartExpressPaymentRemoveInterface.php create mode 100644 Api/GuestCartPaymentActionsGetInterface.php create mode 100644 Api/GuestCartResetInterface.php create mode 100644 Api/GuestExpressPaymentCreateInterface.php delete mode 100644 Api/PaymentActionsGetInterface.php create mode 100644 Api/PaymentMethodsSettingsGetInterface.php create mode 100644 Controller/Express/Create.php create mode 100644 CustomerData/ExpressPayment.php create mode 100644 Exception/InputValidationException.php create mode 100644 Model/ApiSettingsProvider.php create mode 100644 Model/CartExpressPaymentRemove.php create mode 100644 Model/CartPaymentActionsGet.php create mode 100644 Model/CartReset.php create mode 100644 Model/Checks/HasCartExpressPayment.php create mode 100644 Model/Checks/HasCartExpressPaymentInterface.php create mode 100644 Model/ExpressPaymentCreate.php create mode 100644 Model/GuestCartExpressPaymentRemove.php create mode 100644 Model/GuestCartPaymentActionsGet.php create mode 100644 Model/GuestCartReset.php create mode 100644 Model/GuestExpressPaymentCreate.php create mode 100644 Model/Payment/PaymentCreateExpress.php create mode 100644 Model/Payment/PaymentCreateExpressInterface.php create mode 100644 Model/PaymentMethodsSettingsGet.php create mode 100644 Model/Validation/IsValidAddress.php create mode 100644 Observer/Quote/Model/Quote/Item/RemoveExpressPaymentDataObserver.php create mode 100644 Observer/Session/RemoveExpressPaymentDataObserver.php create mode 100644 Plugin/Checkout/ExpressPaymentValidateCustomerAddress.php create mode 100644 Plugin/Quote/Api/PaymentMethodManagement/LimitCartExpressPayment.php create mode 100644 Plugin/Quote/Api/PaymentMethodManagement/RemoveExpressPaymentInfoOnMethodChange.php create mode 100644 ViewModel/CheckoutConfig.php create mode 100644 ViewModel/PayPal.php create mode 100644 etc/frontend/sections.xml create mode 100644 view/frontend/layout/catalog_product_view_type_configurable.xml create mode 100644 view/frontend/templates/body/before-end/checkout-config.phtml create mode 100644 view/frontend/templates/product/view/info/addtocart.phtml create mode 100644 view/frontend/web/js/action/add-to-cart.js create mode 100644 view/frontend/web/js/action/checkout/payment/get-order-payment-actions.js create mode 100644 view/frontend/web/js/action/checkout/payment/remove-express-payment.js create mode 100644 view/frontend/web/js/action/create-cart.js create mode 100644 view/frontend/web/js/action/create-express-payment.js create mode 100644 view/frontend/web/js/action/empty-cart.js create mode 100644 view/frontend/web/js/action/remove-express-payment.js create mode 100644 view/frontend/web/js/action/set-cart-billing-address.js create mode 100644 view/frontend/web/js/action/set-session-message.js create mode 100644 view/frontend/web/js/checkout-data-resolver-mixin.js create mode 100644 view/frontend/web/js/helper/checkout-data-helper.js create mode 100644 view/frontend/web/js/helper/get-add-to-cart-payload.js create mode 100644 view/frontend/web/js/helper/get-current-quote-id.js create mode 100644 view/frontend/web/js/helper/get-form-data.js create mode 100644 view/frontend/web/js/helper/get-paypal-checkout-button-style.js create mode 100644 view/frontend/web/js/helper/get-paypal-pdp-button-style.js create mode 100644 view/frontend/web/js/helper/get-pdp-form.js create mode 100644 view/frontend/web/js/helper/get-store-code.js create mode 100644 view/frontend/web/js/helper/is-express-payment.js create mode 100644 view/frontend/web/js/helper/is-logged-in.js create mode 100644 view/frontend/web/js/helper/is-paypal-pdp-button-enabled.js create mode 100644 view/frontend/web/js/helper/validate-pdp-form.js create mode 100644 view/frontend/web/js/method/paypal/button.js create mode 100644 view/frontend/web/js/model/checkout/payment/order-payment-action.js create mode 100644 view/frontend/web/js/model/checkout/payment/rvvup-method-properties.js create mode 100644 view/frontend/web/js/model/customer-data/express-payment.js create mode 100644 view/frontend/web/js/view/billing-address-mixin.js diff --git a/Api/CartExpressPaymentRemoveInterface.php b/Api/CartExpressPaymentRemoveInterface.php new file mode 100644 index 00000000..e88f93af --- /dev/null +++ b/Api/CartExpressPaymentRemoveInterface.php @@ -0,0 +1,16 @@ +request = $request; + $this->resultFactory = $resultFactory; + $this->formKeyValidator = $formKeyValidator; + $this->checkoutSession = $checkoutSession; + $this->customerSession = $customerSession; + $this->serializer = $serializer; + $this->cartRepository = $cartRepository; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->expressPaymentCreate = $expressPaymentCreate; + $this->logger = $logger; + } + + /** + * @return \Magento\Framework\Controller\Result\Json + */ + public function execute() + { + // First try catch block to validate request. + try { + $this->validateRequest(); + + $cartId = $this->getRequestCartId(); + + /** @var \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote $quote */ + $quote = $this->cartRepository->getActive( + is_numeric($cartId) + ? $cartId + : $this->maskedQuoteIdToQuoteId->execute($cartId) + ); + + $this->validateCustomerQuote($quote, $cartId); + } catch (NotFoundException|NoSuchEntityException $ex) { + // No logging for these error types. + return $this->returnFailedResponse(); + } + + // Then perform the Create Express Payment action + try { + $paymentActions = $this->expressPaymentCreate->execute( + $quote->getId(), + (string) $this->getRequestBody()['method_code'] + ); + } catch (PaymentValidationException|LocalizedException $ex) { + $this->logger->error('Error thrown on creating Express Payment', ['cart_id' => $cartId]); + return $this->returnFailedResponse(); + } + + // Now clear existing quote (should be destroyed from the session) & replace it with the new one. + $this->checkoutSession->clearQuote(); + $this->checkoutSession->replaceQuote($quote); + + $data = []; + + foreach ($paymentActions as $paymentAction) { + $data[] = [ + PaymentActionInterface::TYPE => $paymentAction->getType(), + PaymentActionInterface::METHOD => $paymentAction->getMethod(), + PaymentActionInterface::VALUE => $paymentAction->getValue() + ]; + } + + /** @var \Magento\Framework\Controller\Result\Json $result */ + $result = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $result->setHttpResponseCode(200); + $result->setData(['success' => true, 'data' => $data, 'error_message' => '']); + + return $result; + } + + /** + * Create exception in case CSRF validation failed. + * Return null if default exception will suffice. + * + * @param \Magento\Framework\App\RequestInterface $request + * @return \Magento\Framework\App\Request\InvalidRequestException|null + */ + public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException + { + return null; + } + + /** + * Perform custom request validation. + * Return null if default validation is needed. + * + * @param \Magento\Framework\App\RequestInterface $request + * @return bool + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + $requestBody = $this->getRequestBody(); + + // Set the param from the JSON body, so it is validated by core validator + $request->setParam('form_key', $requestBody['form_key'] ?? null); + + return $this->formKeyValidator->validate($request); + } + + /** + * Validate this is an AJAX POST request & all request params are set. + * + * We only support AJAX requests on this route for the time being. + * + * @return void + * @throws \Magento\Framework\Exception\NotFoundException + */ + private function validateRequest(): void + { + $requestBody = $this->getRequestBody(); + + foreach ($this->requestRequiredParams as $requestRequiredParam) { + if (!isset($requestBody[$requestRequiredParam])) { + $this->throwNotFound(); + } + } + + if (!$this->request->isPost() || !$this->request->isAjax()) { + $this->throwNotFound(); + } + } + + /** + * Get request body. Load if not set. + * + * @return array|null + */ + private function getRequestBody(): ?array + { + if (is_array($this->requestBody)) { + return $this->requestBody; + } + + try { + $this->requestBody = $this->serializer->unserialize($this->request->getContent()); + } catch (InvalidArgumentException $ex) { + $this->logger->error('Failed to decode JSON request with message: ' . $ex->getMessage()); + $this->requestBody = null; + } + + return $this->requestBody; + } + + /** + * Get the cart ID from the request params. + * + * @return string + * @throws \Magento\Framework\Exception\NotFoundException + */ + private function getRequestCartId(): string + { + $requestBody = $this->getRequestBody(); + + if (!isset($requestBody['cart_id'])) { + $this->throwNotFound(); + } + + return (string) $requestBody['cart_id']; + } + + /** + * @param \Magento\Quote\Api\Data\CartInterface $quote + * @param string $cartId + * @return void + * @throws \Magento\Framework\Exception\NotFoundException + */ + private function validateCustomerQuote(CartInterface $quote, string $cartId): void + { + // If the cart ID is not a numeric value, it should be a Masked Quote ID which are random, so continue. + if (!is_numeric($cartId)) { + return; + } + + // If the quote belongs to the customer of the session, continue. + if ($quote->getCustomer()->getId() === $this->customerSession->getCustomerId()) { + return; + } + + $this->throwNotFound(); + } + + /** + * @return void + * @throws \Magento\Framework\Exception\NotFoundException + */ + private function throwNotFound(): void + { + throw new NotFoundException(__('Page not found')); + } + + /** + * @return \Magento\Framework\Controller\Result\Json + */ + private function returnFailedResponse(): Json + { + /** @var \Magento\Framework\Controller\Result\Json $result */ + $result = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $result->setHttpResponseCode(200); + $result->setData([ + 'success' => false, + 'error_message' => 'There was an error when processing your request' + ]); + + return $result; + } +} diff --git a/Controller/Redirect/Cancel.php b/Controller/Redirect/Cancel.php index 9c3fc04d..1af406e2 100644 --- a/Controller/Redirect/Cancel.php +++ b/Controller/Redirect/Cancel.php @@ -20,15 +20,26 @@ class Cancel implements HttpGetActionInterface { /** @var ResultFactory */ private $resultFactory; - /** @var SessionManagerInterface */ + + /** + * Set via etc/frontend/di.xml + * + * @var \Magento\Framework\Session\SessionManagerInterface|\Magento\Checkout\Model\Session\Proxy + */ private $checkoutSession; + /** @var ManagerInterface */ private $messageManager; /** @var PaymentDataGetInterface */ private $paymentDataGet; /** @var ProcessorPool */ private $processorPool; - /** @var LoggerInterface */ + + /** + * Set via etc/frontend/di.xml + * + * @var \Psr\Log\LoggerInterface|RvvupLog + */ private $logger; /** diff --git a/CustomerData/ExpressPayment.php b/CustomerData/ExpressPayment.php new file mode 100644 index 00000000..7bfa2df3 --- /dev/null +++ b/CustomerData/ExpressPayment.php @@ -0,0 +1,147 @@ +checkoutSession = $checkoutSession; + $this->customerSession = $customerSession; + $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + $this->storeManager = $storeManager; + } + + /** + * Get Private section data for express payment section. + * + * @return array + */ + public function getSectionData(): array + { + return [ + 'store_code' => $this->getCurrentStoreCode(), + 'is_logged_in' => $this->customerSession->isLoggedIn(), + 'quote_id' => $this->getSessionQuoteId(), + 'is_express_payment' => $this->isExpressPaymentQuote() + ]; + } + + /** + * @return string + */ + private function getCurrentStoreCode(): string + { + try { + return $this->storeManager->getStore()->getCode(); + } catch (Exception $ex) { + return ''; + } + } + + /** + * @return string|null + */ + private function getSessionQuoteId(): ?string + { + try { + $quote = $this->getSessionQuote(); + + if ($quote === null) { + return null; + } + + // If logged-in user, return the current quote ID. + if (!$this->isGuest()) { + return (string) $quote->getId(); + } + + return $this->quoteIdToMaskedQuoteId->execute((int) $quote->getId()); + } catch (Exception $ex) { + return null; + } + } + + /** + * @return bool + */ + private function isExpressPaymentQuote(): bool + { + $quote = $this->getSessionQuote(); + + if ($quote === null) { + return false; + } + + return $quote->getPayment() !== null + && $quote->getPayment()->getAdditionalInformation(Method::EXPRESS_PAYMENT_KEY) === true; + } + + /** + * @return bool + */ + private function isGuest(): bool + { + return !$this->customerSession->isLoggedIn(); + } + + /** + * @return \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote|null + */ + private function getSessionQuote() + { + try { + $quote = $this->checkoutSession->getQuote(); + + if (!$quote->getId()) { + return null; + } + + return $quote; + } catch (Exception $ex) { + return null; + } + } +} diff --git a/Exception/InputValidationException.php b/Exception/InputValidationException.php new file mode 100644 index 00000000..efd66b5e --- /dev/null +++ b/Exception/InputValidationException.php @@ -0,0 +1,12 @@ +getPayment(); - return $this->sdkProxy->isOrderRefundable($payment->getAdditionalInformation('rvvup_order_id')); + return $this->sdkProxy->isOrderRefundable($payment->getAdditionalInformation(Method::ORDER_ID)); } catch (Exception $e) { return false; } diff --git a/Gateway/Command/Capture.php b/Gateway/Command/Capture.php index ed71a758..aeaeb9d8 100644 --- a/Gateway/Command/Capture.php +++ b/Gateway/Command/Capture.php @@ -7,6 +7,7 @@ use Magento\Payment\Gateway\Command\CommandException; use Magento\Payment\Gateway\CommandInterface; use Magento\Sales\Model\Order\Payment; +use Rvvup\Payments\Gateway\Method; class Capture extends AbstractCommand implements CommandInterface { @@ -18,7 +19,7 @@ public function execute(array $commandSubject) { /** @var Payment $payment */ $payment = $commandSubject['payment']->getPayment(); - $paymentId = $payment->getAdditionalInformation('rvvup_order_id'); + $paymentId = $payment->getAdditionalInformation(Method::ORDER_ID); $rvvupOrder = $this->sdkProxy->getOrder($paymentId); $state = self::STATE_MAP[$rvvupOrder['status']] ?? 'decline'; $this->$state($payment); @@ -30,7 +31,7 @@ public function execute(array $commandSubject) */ private function success(Payment $payment): void { - $rvvupOrderId = $payment->getAdditionalInformation('rvvup_order_id'); + $rvvupOrderId = $payment->getAdditionalInformation(Method::ORDER_ID); $payment->setTransactionId($rvvupOrderId); } diff --git a/Gateway/Http/Client/TransactionInitialize.php b/Gateway/Http/Client/TransactionInitialize.php index 386d5c2a..65e54c38 100644 --- a/Gateway/Http/Client/TransactionInitialize.php +++ b/Gateway/Http/Client/TransactionInitialize.php @@ -36,6 +36,11 @@ public function __construct( } /** + * Place the request via the API call. + * + * If `is_rvvup_express_payment_update` param is provided in the request data, + * perform express update call to complete the payment. + * * @param \Magento\Payment\Gateway\Http\TransferInterface $transferObject * @return array * @throws \Magento\Payment\Gateway\Http\ClientException @@ -43,7 +48,18 @@ public function __construct( public function placeRequest(TransferInterface $transferObject): array { try { - return $this->sdkProxy->createOrder(['input' => $transferObject->getBody()]); + $requestBody = $transferObject->getBody(); + + if (isset($requestBody['is_rvvup_express_payment_update']) + && $requestBody['is_rvvup_express_payment_update'] === true + ) { + $requestBody = $this->limitExpressPaymentUpdateRequestData($requestBody); + + return $this->sdkProxy->updateExpressOrder(['input' => $requestBody]); + } + + // Otherwise standard order payment. + return $this->sdkProxy->createOrder(['input' => $requestBody]); } catch (Exception $ex) { $this->logger->error( sprintf('Error placing payment request, original exception %s', $ex->getMessage()) @@ -52,4 +68,27 @@ public function placeRequest(TransferInterface $transferObject): array throw new ClientException(__('Something went wrong')); } } + + /** + * Remove any request fields that are not allowed for a payment express update request. + * + * ToDo: Refactor Order Builder so data can be built differently depending the request type. + * + * @param array $requestBody + * @return array + */ + private function limitExpressPaymentUpdateRequestData(array $requestBody): array + { + // Remove not required key values + unset( + $requestBody['type'], + $requestBody['is_rvvup_express_payment_update'], + $requestBody['redirectToStoreUrl'], + $requestBody['items'], + $requestBody['requiresShipping'], + $requestBody['method'] + ); + + return $requestBody; + } } diff --git a/Gateway/Method.php b/Gateway/Method.php index d51f5f06..a5b9c8bd 100644 --- a/Gateway/Method.php +++ b/Gateway/Method.php @@ -20,6 +20,14 @@ class Method extends Adapter */ public const PAYMENT_TITLE_PREFIX = 'rvvup_'; + /** + * Constant to be used as a key identifier for Rvvup payments. + */ + public const ORDER_ID = 'rvvup_order_id'; + public const DASHBOARD_URL = 'dashboard_url'; + public const EXPRESS_PAYMENT_KEY = 'is_rvvup_express_payment'; + public const EXPRESS_PAYMENT_DATA_KEY = 'rvvup_express_payment_data'; + /** * Curative list of available RVVUP Status constants. * diff --git a/Gateway/Request/InitializeDataBuilder.php b/Gateway/Request/InitializeDataBuilder.php index 0d01734f..e6e6ebca 100644 --- a/Gateway/Request/InitializeDataBuilder.php +++ b/Gateway/Request/InitializeDataBuilder.php @@ -6,7 +6,11 @@ use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Payment\Model\InfoInterface; use Magento\Quote\Api\CartRepositoryInterface; +use Psr\Log\LoggerInterface; +use Rvvup\Payments\Exception\QuoteValidationException; +use Rvvup\Payments\Gateway\Method; use Rvvup\Payments\Model\OrderDataBuilder; class InitializeDataBuilder implements BuilderInterface @@ -21,30 +25,118 @@ class InitializeDataBuilder implements BuilderInterface */ private $orderDataBuilder; + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + /** * @param \Magento\Quote\Api\CartRepositoryInterface $cartRepository * @param \Rvvup\Payments\Model\OrderDataBuilder $orderDataBuilder + * @param \Psr\Log\LoggerInterface $logger * @return void */ - public function __construct(CartRepositoryInterface $cartRepository, OrderDataBuilder $orderDataBuilder) - { + public function __construct( + CartRepositoryInterface $cartRepository, + OrderDataBuilder $orderDataBuilder, + LoggerInterface $logger + ) { $this->cartRepository = $cartRepository; $this->orderDataBuilder = $orderDataBuilder; + $this->logger = $logger; } /** * @param array $buildSubject * @return array - * @throws \Rvvup\Payments\Exception\QuoteValidationException|\Magento\Framework\Exception\NoSuchEntityException + * @throws \Rvvup\Payments\Exception\QuoteValidationException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function build(array $buildSubject): array { $paymentDataObject = SubjectReader::readPayment($buildSubject); - // Get the quote. - $cart = $this->cartRepository->get($paymentDataObject->getPayment()->getOrder()->getQuoteId()); + $payment = $paymentDataObject->getPayment(); + + // First check if we have an Order Payment model instance and return result if set. + $result = $this->handleOrderPayment($payment); + + if (is_array($result)) { + return $result; + } + + // Otherwise, we should have a Quote Payment model instance and return result if set. + $result = $this->handleQuotePayment($payment); + + if (is_array($result)) { + return $result; + } + + // Log that we reached this stage and throw exception. + $this->logger->error('There is no Rvvup Standard Payment for Order or Express Payment for Quote'); + + throw new QuoteValidationException(__('Invalid Payment method')); + } + + /** + * Create the request data for an Order Payment & flag if this is a Rvvup Express Update (on order place) + * + * @param \Magento\Payment\Model\InfoInterface $payment + * @return array|null + * @throws \Rvvup\Payments\Exception\QuoteValidationException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function handleOrderPayment(InfoInterface $payment): ?array + { + if (!method_exists($payment, 'getOrder')) { + return null; + } + + // Get the active quote (allow for exceptions to fall through). + $cart = $this->cartRepository->get($payment->getOrder()->getQuoteId()); + + // Build the Rvvup request data, regardless express or not. + $orderData = $this->orderDataBuilder->build($cart); + + // If this is an express payment getting completed, set payment type (express) & additional data. + // Setting the custom `is_rvvup_express_payment_update` flag allows the Transaction class to handle the update. + if ($this->isExpressPayment($payment)) { + $orderData['id'] = $payment->getAdditionalInformation(Method::ORDER_ID); + $orderData['is_rvvup_express_payment_update'] = true; + } + + return $orderData; + } - // Build the Rvvup request data. - return $this->orderDataBuilder->build($cart); + /** + * Handle initialization if this is a Quote Payment (not placed order yet). + * + * Currently, this is supported only for creating express payment orders. + * + * @param \Magento\Payment\Model\InfoInterface $payment + * @return array|null + * @throws \Rvvup\Payments\Exception\QuoteValidationException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function handleQuotePayment(InfoInterface $payment): ?array + { + if (!$this->isExpressPayment($payment) || !method_exists($payment, 'getQuote')) { + return null; + } + + // Get the active quote (allow for exceptions to fall through). + $cart = $this->cartRepository->getActive($payment->getQuote()->getId()); + + // Build the Rvvup request data for creating an express payment. + return $this->orderDataBuilder->build($cart, true); + } + + /** + * @param \Magento\Payment\Model\InfoInterface $payment + * @return bool + */ + private function isExpressPayment(InfoInterface $payment): bool + { + return $payment->getAdditionalInformation(Method::EXPRESS_PAYMENT_KEY) === true; } } diff --git a/Gateway/Response/InitializeResponseHandler.php b/Gateway/Response/InitializeResponseHandler.php index f29dab21..d54b37d4 100644 --- a/Gateway/Response/InitializeResponseHandler.php +++ b/Gateway/Response/InitializeResponseHandler.php @@ -4,11 +4,36 @@ namespace Rvvup\Payments\Gateway\Response; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Payment\Model\InfoInterface; +use Rvvup\Payments\Gateway\Method; class InitializeResponseHandler implements HandlerInterface { + /** + * @var \Magento\Framework\DataObjectFactory + */ + private $dataObjectFactory; + + /** + * Flag property key to identify whether the response is for an orderExpressUpdate call. + * + * @var string + */ + private $orderExpressUpdateFlag = 'is_order_express_update_flag'; + + /** + * @param \Magento\Framework\DataObjectFactory $dataObjectFactory + * @return void + */ + public function __construct(DataObjectFactory $dataObjectFactory) + { + $this->dataObjectFactory = $dataObjectFactory; + } + /** * @param array $handlingSubject * @param array $response @@ -16,39 +41,118 @@ class InitializeResponseHandler implements HandlerInterface */ public function handle(array $handlingSubject, array $response): void { - $paymentDataObject = SubjectReader::readPayment($handlingSubject); + $payment = SubjectReader::readPayment($handlingSubject)->getPayment(); + + $responseDataObject = $this->getResponseDataObject($response); + + $payment = $this->setPaymentAdditionalInformation($payment, $responseDataObject); - $paymentInfo = $paymentDataObject->getPayment(); + // If the payment method instance is not an Order Payment, no further actions. + // In that scenario, it is a Quote Payment instance for an Express Create. + if (!method_exists($payment, 'getOrder')) { + return; + } // Do not let magento set status to processing, this will be handled once back from the redirect. - $paymentInfo->setIsTransactionPending(true); - // do not close transaction so you can do a cancel() and void - $paymentInfo->setIsTransactionClosed(false); - $paymentInfo->setShouldCloseParentTransaction(false); + $payment->setIsTransactionPending(true); + // do not close transaction, so you can do a cancel() and void + $payment->setIsTransactionClosed(false); + $payment->setShouldCloseParentTransaction(false); // Set the Rvvup Order ID as the transaction ID - $paymentInfo->setTransactionId($response['data']['orderCreate']['id']); - $paymentInfo->setCcTransId($response['data']['orderCreate']['id']); - $paymentInfo->setLastTransId($response['data']['orderCreate']['id']); + $payment->setTransactionId($responseDataObject->getData('id')); + $payment->setCcTransId($responseDataObject->getData('id')); + $payment->setLastTransId($responseDataObject->getData('id')); - // Set data to the transaction - $paymentInfo->setAdditionalInformation('rvvup_order_id', $response['data']['orderCreate']['id']); + // Don't send customer email. + $payment->getOrder()->setCanSendNewEmailFlag(false); + } - $paymentInfo->setAdditionalInformation('status', $response['data']['orderCreate']['status'] ?? null); - $paymentInfo->setAdditionalInformation( - 'dashboardUrl', - $response['data']['orderCreate']['dashboardUrl'] ?? null - ); + /** + * Set the Payment's additional information. + * + * If it is an orderCreate with the express payment flag, then set additional information on unique key. + * This will allow us to remove the data if the express payment method is cancelled or changed. + * Otherwise, we have an orderCreate or orderExpressUpdate call during checkout. + * + * @param \Magento\Payment\Model\InfoInterface $payment + * @param \Magento\Framework\DataObject $responseDataObject + * @return \Magento\Payment\Model\InfoInterface + */ + private function setPaymentAdditionalInformation( + InfoInterface $payment, + DataObject $responseDataObject + ): InfoInterface { + $payment->setAdditionalInformation(Method::ORDER_ID, $responseDataObject->getData('id')); - // Add the payment actions (that include the redirect values). - $paymentInfo->setAdditionalInformation( - 'paymentActions', - $response['data']['orderCreate']['paymentSummary']['paymentActions'] ?? [] - ); + // Prepare & set the payment actions + $paymentActions = []; + $paymentSummary = $responseDataObject->getData('paymentSummary'); - // Don't send customer email. - if (method_exists($paymentInfo, 'getOrder')) { - $paymentInfo->getOrder()->setCanSendNewEmailFlag(false); + if (is_array($paymentSummary) + && isset($paymentSummary['paymentActions']) + && is_array($paymentSummary['paymentActions']) + ) { + $paymentActions = $paymentSummary['paymentActions']; } + + // If this is a createOrder call for an express payment, + // then set the data to separate key. + if ($this->isExpressPayment($payment) && $responseDataObject->getData($this->orderExpressUpdateFlag) !== true) { + $data = [ + 'status' => $responseDataObject->getData('status'), + 'dashboardUrl' => $responseDataObject->getData('dashboardUrl'), + 'paymentActions' => $paymentActions + ]; + + $payment->setAdditionalInformation(Method::EXPRESS_PAYMENT_DATA_KEY, $data); + + return $payment; + } + + // Otherwise, set normally. + $payment->setAdditionalInformation('status', $responseDataObject->getData('status')); + $payment->setAdditionalInformation('dashboardUrl', $responseDataObject->getData('dashboardUrl')); + $payment->setAdditionalInformation('paymentActions', $paymentActions); + + return $payment; + } + + /** + * Generate a response data object from the response array. + * + * The response is either an orderExpressUpdate or an orderCreate. + * This is already validated in the InitializeResponseValidator and also that the response is an array. + * The response is either + * + * @param array $response + * @return \Magento\Framework\DataObject + */ + private function getResponseDataObject(array $response): DataObject + { + $responseDataObject = $this->dataObjectFactory->create(); + + // If orderExpressUpdate, set data & flag and return. + if (isset($response['data']['orderExpressUpdate'])) { + $responseDataObject->setData($response['data']['orderExpressUpdate']); + $responseDataObject->setData($this->orderExpressUpdateFlag, true); + + return $responseDataObject; + + } + + // Otherwise it will be an orderCreate. + $responseDataObject->setData($response['data']['orderCreate']); + + return $responseDataObject; + } + + /** + * @param \Magento\Payment\Model\InfoInterface $payment + * @return bool + */ + private function isExpressPayment(InfoInterface $payment): bool + { + return $payment->getAdditionalInformation(Method::EXPRESS_PAYMENT_KEY) === true; } } diff --git a/Gateway/Validator/InitializeResponseValidator.php b/Gateway/Validator/InitializeResponseValidator.php index 7b99ee5d..9d795473 100644 --- a/Gateway/Validator/InitializeResponseValidator.php +++ b/Gateway/Validator/InitializeResponseValidator.php @@ -11,6 +11,12 @@ class InitializeResponseValidator extends AbstractValidator { /** + * Validate the Rvvup ID key exists in the result. + * + * The ID should exist either for an orderCreate or an orderExpressUpdate API request. + * Hence, check if both are missing for validation, regardless the request type. + * This also validates the relevant orderCreate & orderExpressUpdate keys are arrays. + * * @param array $validationSubject * @return \Magento\Payment\Gateway\Validator\ResultInterface */ @@ -20,7 +26,7 @@ public function validate(array $validationSubject): ResultInterface $fails = []; - if (!isset($response['data']['orderCreate']['id'])) { + if (!isset($response['data']['orderCreate']['id']) && !isset($response['data']['orderExpressUpdate']['id'])) { $fails[] = 'Rvvup order ID is not set'; } diff --git a/Model/ApiSettingsProvider.php b/Model/ApiSettingsProvider.php new file mode 100644 index 00000000..c62b33f4 --- /dev/null +++ b/Model/ApiSettingsProvider.php @@ -0,0 +1,49 @@ +sdk = $sdk; + parent::__construct($data); + } + + /** + * @param string $method + * @param string $path + * @return array|mixed|null + */ + public function getByPath(string $method, string $path) + { + $this->loadSdkData(); + $this->_data = $this->apiData[$method]; + return $this->getDataByPath($path); + } + + /** + * @return void + */ + private function loadSdkData(): void + { + if (!$this->apiData) { + $methods = $this->sdk->getMethods('0', 'GBP'); + foreach ($methods as $method) { + $this->apiData[$method['name']] = $method; + } + } + } +} diff --git a/Model/CartExpressPaymentRemove.php b/Model/CartExpressPaymentRemove.php new file mode 100644 index 00000000..dd57738f --- /dev/null +++ b/Model/CartExpressPaymentRemove.php @@ -0,0 +1,104 @@ +paymentMethodManagement = $paymentMethodManagement; + $this->paymentResource = $paymentResource; + $this->logger = $logger; + } + + /** + * Remove the payment data of express payment for the specified cart & rvvup payment method for a guest user. + * + * Return true on success or "dummy" true when the methods are not matching. + * + * @param string $cartId + * @return bool + */ + public function execute(string $cartId): bool + { + try { + $payment = $this->paymentMethodManagement->get($cartId); + + // No action if no payment or payment without a method set. + if ($payment === null || !$payment->getId() || $payment->getMethod() === null) { + return true; + } + + // No action if not Rvvup Payment. + if (strpos($payment->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0) { + return true; + } + + // No action if no express payment on quote. + if ($payment->getAdditionalInformation(Method::EXPRESS_PAYMENT_KEY) !== true) { + return true; + } + } catch (NoSuchEntityException $ex) { + // Return if no cart found. + return true; + } + + try { + // Unset express data. + $payment->unsAdditionalInformation(Method::ORDER_ID); + $payment->unsAdditionalInformation(Method::EXPRESS_PAYMENT_KEY); + $payment->unsAdditionalInformation(Method::EXPRESS_PAYMENT_DATA_KEY); + + $this->paymentResource->save($payment); + + return true; + } catch (Exception $ex) { + // Log any other error (on save) and return + $this->logger->error( + 'Error thrown on removing express payment information with message: ' . $ex->getMessage(), + [ + 'quote_id' => $cartId, + 'payment_id' => $payment->getId() + ] + ); + + return false; + } + } +} diff --git a/Model/CartPaymentActionsGet.php b/Model/CartPaymentActionsGet.php new file mode 100644 index 00000000..34c4c71e --- /dev/null +++ b/Model/CartPaymentActionsGet.php @@ -0,0 +1,155 @@ +paymentMethodManagement = $paymentMethodManagement; + $this->paymentActionInterfaceFactory = $paymentActionInterfaceFactory; + $this->logger = $logger; + } + + /** + * Get the payment actions for the specified cart ID. + * + * @param string $cartId + * @param bool $expressActions + * @return PaymentActionInterface[] + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(string $cartId, bool $expressActions = false): array + { + $payment = $this->paymentMethodManagement->get($cartId); + + if ($payment === null) { + return []; + } + + $paymentActions = $this->getAdditionalInformationPaymentActions($payment, $expressActions); + + // Check if payment actions are set as array & not empty + if (empty($paymentActions) || !is_array($paymentActions)) { + return []; + } + + $paymentActionsDataArray = []; + + try { + foreach ($paymentActions as $paymentAction) { + if (!is_array($paymentAction)) { + continue; + } + + $paymentActionData = $this->getPaymentActionDataObject($paymentAction); + + // Validate all all properties have values. + if ($paymentActionData->getType() !== null + && $paymentActionData->getMethod() !== null + && $paymentActionData->getValue() !== null + ) { + $paymentActionsDataArray[] = $paymentActionData; + } + } + } catch (Throwable $t) { + $this->logger->error( + 'Error loading Payment Actions. Failed return result with message: ' . $t->getMessage(), + [ + 'quote_id' => $cartId, + ] + ); + + throw new LocalizedException(__('Something went wrong')); + } + + return $paymentActionsDataArray; + } + + /** + * Get the additional information data that hold the payment actions. + * + * Get either standard or the ones saved in the express payment data field. + * + * @param \Magento\Quote\Api\Data\PaymentInterface $payment + * @param bool $expressActions + * @return array|mixed|null + */ + private function getAdditionalInformationPaymentActions(PaymentInterface $payment, bool $expressActions = false) + { + if (!$expressActions) { + return $payment->getAdditionalInformation('paymentActions'); + } + + $expressPaymentData = $payment->getAdditionalInformation(Method::EXPRESS_PAYMENT_DATA_KEY); + + return is_array($expressPaymentData) && isset($expressPaymentData['paymentActions']) + ? $expressPaymentData['paymentActions'] + : []; + } + + /** + * Create & return a PaymentActionInterface Data object. + * + * @param array $paymentAction + * @return \Rvvup\Payments\Api\Data\PaymentActionInterface + */ + private function getPaymentActionDataObject(array $paymentAction): PaymentActionInterface + { + /** @var PaymentActionInterface $paymentActionData */ + $paymentActionData = $this->paymentActionInterfaceFactory->create(); + + if (isset($paymentAction['type'])) { + $paymentActionData->setType(mb_strtolower($paymentAction['type'])); + } + + if (isset($paymentAction['method'])) { + $paymentActionData->setMethod(mb_strtolower($paymentAction['method'])); + } + + if (isset($paymentAction['value'])) { + // Don't lowercase value. + $paymentActionData->setValue($paymentAction['value']); + } + + return $paymentActionData; + } +} diff --git a/Model/CartReset.php b/Model/CartReset.php new file mode 100644 index 00000000..b4d7dbea --- /dev/null +++ b/Model/CartReset.php @@ -0,0 +1,45 @@ +cartRepository = $cartRepository; + } + + /** + * Reset the data of the specified cart, empties items & addresses. + * + * @param string $cartId + * @return string + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function execute(string $cartId): string + { + /** @var \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote $quote */ + $quote = $this->cartRepository->getActive((int) $cartId); + + $quote->removeAllItems(); + + $this->cartRepository->save($quote); + + return (string) $quote->getId(); + } +} diff --git a/Model/Checks/HasCartExpressPayment.php b/Model/Checks/HasCartExpressPayment.php new file mode 100644 index 00000000..0c26b84e --- /dev/null +++ b/Model/Checks/HasCartExpressPayment.php @@ -0,0 +1,72 @@ +paymentMethodManagement = $paymentMethodManagement; + $this->logger = $logger; + } + + /** + * Check whether a cart is for an express payment. + * + * @param int $cartId + * @return bool + */ + public function execute(int $cartId): bool + { + try { + $payment = $this->paymentMethodManagement->get($cartId); + + // No action if no payment or payment without a method set. + if ($payment === null || !$payment->getId() || $payment->getMethod() === null) { + return false; + } + } catch (NoSuchEntityException $ex) { + // On no such entity exception, just return false. + $this->logger->error( + 'Failed to check whether a cart has an express payment with error: ' . $ex->getMessage() + ); + + return false; + } + + // No action if not Rvvup Payment. + if (strpos($payment->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0) { + return false; + } + + // If we don't have a payment or express payment key is not set, return as is. + if ($payment->getadditionalInformation(Method::EXPRESS_PAYMENT_KEY) !== true) { + return false; + } + + return true; + } +} diff --git a/Model/Checks/HasCartExpressPaymentInterface.php b/Model/Checks/HasCartExpressPaymentInterface.php new file mode 100644 index 00000000..fc329332 --- /dev/null +++ b/Model/Checks/HasCartExpressPaymentInterface.php @@ -0,0 +1,16 @@ +config = $config; $this->sdkProxy = $sdkProxy; + $this->addressMetadata = $addressMetadata; + $this->customerRepository = $customerRepository; $this->checkoutSession = $checkoutSession; + $this->customerSession = $customerSession; $this->template = $template; + $this->addressFactory = $addressFactory; + $this->addressResourceModel = $addressResourceModel; $this->clearpayConfig = $clearpayConfig; } /** - * @inheritdoc + * Retrieve assoc array of checkout configuration + * + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException */ public function getConfig() { if (!$this->config->isActive()) { return []; } + $quote = $this->checkoutSession->getQuote(); - $grandTotal = $quote->getGrandTotal(); - $currency = $quote->getQuoteCurrencyCode(); - $methods = $this->sdkProxy->getMethods((string) $grandTotal, $currency); + + $methods = $this->sdkProxy->getMethods((string) $quote->getGrandTotal(), $quote->getQuoteCurrencyCode()); $items = []; + foreach ($methods as $method) { - $items['rvvup_' . $method['name']] = [ + $items[Method::PAYMENT_TITLE_PREFIX . $method['name']] = [ 'component' => 'Rvvup_Payments/js/view/payment/method-renderer/rvvup-method', 'isBillingAddressRequired' => true, 'description' => $method['description'], 'logo' => $this->getLogo($method['name']), - 'summary_url' => $method['summaryUrl'], + 'summary_url' => $this->getSummaryUrl( + $this->isExpressPaymentCart($quote), + $method['summaryUrl'] ?? null + ), 'assets' => $method['assets'], ]; } - return ['payment' => $items]; + + // We need to add the address data only if we have an express payment for logged in customers with addresses. + // If customer has addresses, the default address book is used, so we need to pass our custom address data. + if ($this->isGuest() || !$this->isExpressPaymentCart($quote) || !$this->hasCustomerAddresses()) { + return ['payment' => $items]; + } + + return array_merge( + ['payment' => $items], + $this->getCartShippingAddressData($quote), + $this->getCartBillingAddressData($quote) + ); } + /** + * @param string $code + * @return string + */ private function getLogo(string $code): string { $base = 'Rvvup_Payments::images/%s.svg'; @@ -86,6 +182,213 @@ private function getLogo(string $code): string default: $url = sprintf($base, 'rvvup'); } + return $this->template->getViewFileUrl($url); } + + /** + * Get the summary URL with the mode appended for express payments on checkout. + * + * @param bool $express + * @param string|null $summaryUrl + * @return string + */ + private function getSummaryUrl(bool $express = false, ?string $summaryUrl = null): string + { + if ($summaryUrl === null) { + return ''; + } + + if (!$express) { + return $summaryUrl; + } + + try { + $url = UriFactory::factory($summaryUrl); + } catch (InvalidArgumentException $ex) { + return $summaryUrl; + } + + // Add mode=express to query. + $queryArray = $url->getQueryAsArray(); + $queryArray['mode'] = 'express'; + + $url->setQuery($queryArray); + + try { + return $url->toString(); + } catch (InvalidUriException $ex) { + return $summaryUrl; + } + } + + /** + * Check whether current cart has express payment data. + * + * @param \Magento\Quote\Api\Data\CartInterface $cart + * @return bool + */ + private function isExpressPaymentCart(CartInterface $cart): bool + { + return $cart->getPayment() !== null + && $cart->getPayment()->getAdditionalInformation(Method::EXPRESS_PAYMENT_KEY) === true; + } + + /** + * @return bool + */ + private function isGuest(): bool + { + return !$this->customerSession->isLoggedIn(); + } + + /** + * Check whether current session customer has any addresses in their address book. + * + * @return bool + */ + private function hasCustomerAddresses(): bool + { + if ($this->isGuest()) { + return false; + } + + try { + $customer = $this->customerRepository->getById($this->customerSession->getCustomerId()); + } catch (NoSuchEntityException|LocalizedException $ex) { + // no log required. + return false; + } + + return $customer->getAddresses() !== null && !empty($customer->getAddresses()); + } + + /** + * Get cart shipping address data for checkout. + * + * For logged-in users, getShippingAddress returns the customer's default shipping address from the address book, + * even though the quote_address ID is correctly set. + * Hence, we load the address from the resource model by the address ID. + * + * @param \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote $cart + * @return array + */ + private function getCartShippingAddressData(CartInterface $cart): array + { + if ($cart->getShippingAddress() === null || !$cart->getShippingAddress()->getId()) { + return []; + } + + $shippingAddressFromData = $this->getAddressFromData($this->getCartAddressByAddressId( + (int) $cart->getShippingAddress()->getId() + )); + + if (empty($shippingAddressFromData)) { + return []; + } + + // Required so we can pick it up from JS mixin. + $shippingAddressFromData["type"] = "new-customer-address"; + + return [ + 'isShippingAddressFromDataValid' => $cart->getShippingAddress()->validate() === true, + 'shippingAddressFromData' => $shippingAddressFromData + ]; + } + + /** + * Get cart billing address data for checkout. + * + * For logged-in users, getBillingAddress returns the customer's default billing address from the address book, + * even though the quote_address ID is correctly set. + * Hence, we load the address from the resource model by the address ID. + * + * @param \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote $cart + * @return array + */ + private function getCartBillingAddressData(CartInterface $cart): array + { + if ($cart->getBillingAddress() === null || !$cart->getBillingAddress()->getId()) { + return []; + } + + $billingAddressFromData = $this->getAddressFromData($this->getCartAddressByAddressId( + (int) $cart->getBillingAddress()->getId() + )); + + if (empty($billingAddressFromData)) { + return []; + } + + // Required so we can pick it up from JS mixin. + $billingAddressFromData["type"] = "new-customer-address"; + + return [ + 'isBillingAddressFromDataValid' => $cart->getBillingAddress()->validate() === true, + 'billingAddressFromData' => $billingAddressFromData + ]; + } + + /** + * @param int $addressId + * @return \Magento\Quote\Api\Data\AddressInterface|\Magento\Quote\Model\Quote\Address + */ + private function getCartAddressByAddressId(int $addressId) + { + /** @var \Magento\Quote\Api\Data\AddressInterface|\Magento\Quote\Model\Quote\Address $address */ + $address = $this->addressFactory->create(); + + $this->addressResourceModel->load($address, $addressId); + + return $address; + } + + /** + * Create address data appropriate to fill checkout address form + * + * @param \Magento\Quote\Api\Data\AddressInterface $address + * @return array + */ + private function getAddressFromData(AddressInterface $address): array + { + if ($address->getEmail() === null || empty($address->getEmail())) { + return []; + } + + try { + $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); + } catch (LocalizedException $ex) { + // Silent return. + return []; + } + + $addressData = []; + + foreach ($attributesMetadata as $attributeMetadata) { + if (!$attributeMetadata->isVisible()) { + continue; + } + + $attributeCode = $attributeMetadata->getAttributeCode(); + $attributeData = $address->getData($attributeCode); + + if (!$attributeData) { + continue; + } + + if ($attributeMetadata->getFrontendInput() === Multiline::NAME) { + $attributeData = is_array($attributeData) ? $attributeData : explode("\n", $attributeData); + $attributeData = (object)$attributeData; + } + + if ($attributeMetadata->isUserDefined()) { + $addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES][$attributeCode] = $attributeData; + continue; + } + + $addressData[$attributeCode] = $attributeData; + } + + return $addressData; + } } diff --git a/Model/ExpressPaymentCreate.php b/Model/ExpressPaymentCreate.php new file mode 100644 index 00000000..b369a3d7 --- /dev/null +++ b/Model/ExpressPaymentCreate.php @@ -0,0 +1,90 @@ +cartRepository = $cartRepository; + $this->paymentMethodManagement = $paymentMethodManagement; + $this->cartPaymentActionsGet = $cartPaymentActionsGet; + $this->paymentExpressCreate = $paymentExpressCreate; + } + + /** + * Create a Rvvup Express order for the specified cart & rvvup payment method. + * + * @param string $cartId + * @param string $methodCode + * @return \Rvvup\Payments\Api\Data\PaymentActionInterface[] + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Rvvup\Payments\Exception\PaymentValidationException + */ + public function execute(string $cartId, string $methodCode): array + { + $quote = $this->cartRepository->getActive($cartId); + + $result = $this->paymentExpressCreate->execute($quote, $methodCode); + + if (!$result) { + throw new PaymentValidationException(__('Payment method not available')); + } + + $payment = $this->paymentMethodManagement->get($cartId); + + if ($payment === null || $payment->getAdditionalInformation(Method::EXPRESS_PAYMENT_KEY) !== true) { + throw new PaymentValidationException(__('Invalid payment method')); + } + + $rvvupOrderId = $payment->getAdditionalInformation(Method::ORDER_ID); + + if (!is_string($rvvupOrderId)) { + throw new PaymentValidationException(__('Invalid payment method')); + } + + return $this->cartPaymentActionsGet->execute($cartId, true); + } +} diff --git a/Model/GuestCartExpressPaymentRemove.php b/Model/GuestCartExpressPaymentRemove.php new file mode 100644 index 00000000..598caa66 --- /dev/null +++ b/Model/GuestCartExpressPaymentRemove.php @@ -0,0 +1,46 @@ +maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->cartExpressPaymentRemove = $cartExpressPaymentRemove; + } + + /** + * Remove the payment data of express payment for the specified cart & rvvup payment method for a guest user. + * + * @param string $cartId + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function execute(string $cartId): bool + { + return $this->cartExpressPaymentRemove->execute((string) $this->maskedQuoteIdToQuoteId->execute($cartId)); + } +} diff --git a/Model/GuestCartPaymentActionsGet.php b/Model/GuestCartPaymentActionsGet.php new file mode 100644 index 00000000..bbcb4f67 --- /dev/null +++ b/Model/GuestCartPaymentActionsGet.php @@ -0,0 +1,50 @@ +maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->cartPaymentActionsGet = $cartPaymentActionsGet; + } + + /** + * @param string $cartId + * @param bool $expressActions + * @return \Rvvup\Payments\Api\Data\PaymentActionInterface[] + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function execute(string $cartId, bool $expressActions = false): array + { + return $this->cartPaymentActionsGet->execute( + (string) $this->maskedQuoteIdToQuoteId->execute($cartId), + $expressActions + ); + } +} diff --git a/Model/GuestCartReset.php b/Model/GuestCartReset.php new file mode 100644 index 00000000..f496511b --- /dev/null +++ b/Model/GuestCartReset.php @@ -0,0 +1,50 @@ +maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->cartReset = $cartReset; + } + + /** + * Reset the data of the specified guest cart, empties items & addresses. + * + * @param string $cartId + * @return string + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function execute(string $cartId): string + { + $this->cartReset->execute((string) $this->maskedQuoteIdToQuoteId->execute($cartId)); + + // This is the masked ID. + return $cartId; + } +} diff --git a/Model/GuestExpressPaymentCreate.php b/Model/GuestExpressPaymentCreate.php new file mode 100644 index 00000000..eb245b6a --- /dev/null +++ b/Model/GuestExpressPaymentCreate.php @@ -0,0 +1,51 @@ +maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->expressPaymentCreate = $expressPaymentCreate; + } + + /** + * @param string $cartId + * @param string $methodCode + * @return \Rvvup\Payments\Api\Data\PaymentActionInterface[] + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Rvvup\Payments\Exception\PaymentValidationException + */ + public function execute(string $cartId, string $methodCode): array + { + return $this->expressPaymentCreate->execute( + (string) $this->maskedQuoteIdToQuoteId->execute($cartId), + $methodCode + ); + } +} diff --git a/Model/GuestPaymentActionsGet.php b/Model/GuestPaymentActionsGet.php index 94d5305e..e65ea1a0 100644 --- a/Model/GuestPaymentActionsGet.php +++ b/Model/GuestPaymentActionsGet.php @@ -4,18 +4,15 @@ namespace Rvvup\Payments\Model; -use Magento\Framework\Exception\LocalizedException; -use Magento\Quote\Model\QuoteIdMaskFactory; -use Psr\Log\LoggerInterface; -use Rvvup\Payments\Api\Data\PaymentActionInterfaceFactory; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; use Rvvup\Payments\Api\GuestPaymentActionsGetInterface; class GuestPaymentActionsGet implements GuestPaymentActionsGetInterface { /** - * @var \Magento\Quote\Model\QuoteIdMaskFactory + * @var \Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface */ - private $quoteIdMaskFactory; + private $maskedQuoteIdToQuoteId; /** * @var \Rvvup\Payments\Model\PaymentActionsGetInterface @@ -23,26 +20,16 @@ class GuestPaymentActionsGet implements GuestPaymentActionsGetInterface private $paymentActionsGet; /** - * Set via di.xml - * - * @var \Psr\Log\LoggerInterface|RvvupLog - */ - private $logger; - - /** - * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory + * @param \Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId * @param \Rvvup\Payments\Model\PaymentActionsGetInterface $paymentActionsGet - * @param \Psr\Log\LoggerInterface $logger * @return void */ public function __construct( - QuoteIdMaskFactory $quoteIdMaskFactory, - PaymentActionsGetInterface $paymentActionsGet, - LoggerInterface $logger + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + PaymentActionsGetInterface $paymentActionsGet ) { - $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; $this->paymentActionsGet = $paymentActionsGet; - $this->logger = $logger; } /** @@ -51,20 +38,10 @@ public function __construct( * @param string $cartId * @return \Rvvup\Payments\Api\Data\PaymentActionInterface[] * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function execute(string $cartId): array { - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); // @phpstan-ignore-line - - if ($quoteIdMask->getQuoteId() === null) { - $this->logger->error('Error loading Payment Actions for guest. No quote ID found.', [ - 'masked_quote_id' => $cartId - ]); - - throw new LocalizedException(__('Something went wrong')); - } - - return $this->paymentActionsGet->execute($quoteIdMask->getQuoteId()); + return $this->paymentActionsGet->execute((string) $this->maskedQuoteIdToQuoteId->execute($cartId)); } } diff --git a/Model/IsPaymentMethodAvailable.php b/Model/IsPaymentMethodAvailable.php index 310b2e28..d2b417ac 100644 --- a/Model/IsPaymentMethodAvailable.php +++ b/Model/IsPaymentMethodAvailable.php @@ -5,11 +5,12 @@ namespace Rvvup\Payments\Model; use Psr\Log\LoggerInterface; +use Rvvup\Payments\Gateway\Method; class IsPaymentMethodAvailable implements IsPaymentMethodAvailableInterface { /** - * @var \Rvvup\Payments\Model\PaymentMethodsAvailableGet + * @var \Rvvup\Payments\Model\PaymentMethodsAvailableGetInterface */ private $paymentMethodsAvailableGet; @@ -23,8 +24,10 @@ class IsPaymentMethodAvailable implements IsPaymentMethodAvailableInterface * @param \Psr\Log\LoggerInterface $logger * @return void */ - public function __construct(PaymentMethodsAvailableGet $paymentMethodsAvailableGet, LoggerInterface $logger) - { + public function __construct( + PaymentMethodsAvailableGetInterface $paymentMethodsAvailableGet, + LoggerInterface $logger + ) { $this->paymentMethodsAvailableGet = $paymentMethodsAvailableGet; $this->logger = $logger; } @@ -39,26 +42,17 @@ public function __construct(PaymentMethodsAvailableGet $paymentMethodsAvailableG */ public function execute(string $methodCode, string $value, string $currency): bool { - // We need a numeric value, so false if not such. - if (!is_numeric($value)) { - return false; - } - - $lowerCaseMethodName = mb_strtolower($methodCode); + $formattedMethodCode = mb_strtolower(Method::PAYMENT_TITLE_PREFIX . $methodCode); - foreach ($this->paymentMethodsAvailableGet->execute($value, $currency) as $method) { - if (!isset($method['name'])) { - continue; - } + $result = array_key_exists($formattedMethodCode, $this->paymentMethodsAvailableGet->execute($value, $currency)); - if (mb_strtolower($method['name']) === $lowerCaseMethodName) { - return true; - } + if ($result) { + return true; } // Log debug & Default to false if not found. $this->logger->debug('Rvvup payment method is not available', [ - 'method_code' => $lowerCaseMethodName, + 'method_code' => $formattedMethodCode, 'value' => $value, 'currency' => $currency ]); diff --git a/Model/OrderDataBuilder.php b/Model/OrderDataBuilder.php index dba00efa..98a35600 100644 --- a/Model/OrderDataBuilder.php +++ b/Model/OrderDataBuilder.php @@ -2,80 +2,68 @@ namespace Rvvup\Payments\Model; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\UrlInterface; use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\CartInterface; use Rvvup\Payments\Exception\QuoteValidationException; +use Rvvup\Payments\Gateway\Method; class OrderDataBuilder { - /** @var \Rvvup\Payments\Model\ConfigInterface */ - private $config; - /** @var \Magento\Framework\UrlInterface */ + /** + * @var \Magento\Customer\Api\AddressRepositoryInterface + */ + private $customerAddressRepository; + + /** + * @var \Magento\Framework\UrlInterface + */ private $urlBuilder; /** - * @param \Rvvup\Payments\Model\ConfigInterface $config + * @var \Rvvup\Payments\Model\ConfigInterface + */ + private $config; + + /** + * @param \Magento\Customer\Api\AddressRepositoryInterface $customerAddressRepository * @param \Magento\Framework\UrlInterface $urlBuilder + * @param \Rvvup\Payments\Model\ConfigInterface $config * @return void */ - public function __construct(ConfigInterface $config, UrlInterface $urlBuilder) - { - $this->config = $config; + public function __construct( + AddressRepositoryInterface $customerAddressRepository, + UrlInterface $urlBuilder, + ConfigInterface $config + ) { + $this->customerAddressRepository = $customerAddressRepository; $this->urlBuilder = $urlBuilder; + $this->config = $config; } /** - * @param \Magento\Quote\Api\Data\CartInterface $quote + * @param \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote $quote + * @param bool $express * @return array - * @throws QuoteValidationException + * @throws \Rvvup\Payments\Exception\QuoteValidationException */ - public function build(CartInterface $quote): array + public function build(CartInterface $quote, bool $express = false): array { - $discountTotal = $this->toCurrency($quote->getBaseSubtotal() - $quote->getBaseSubtotalWithDiscount()); - $taxTotal = $this->toCurrency($quote->getTotals()['tax']->getValue()); - $currencyCode = $quote->getQuoteCurrencyCode(); - $billingAddress = $quote->getBillingAddress(); - if ($billingAddress === null) { + // Validate that billing address exists if this is NOT a request to build express payment data. + if (!$express && $billingAddress === null) { $this->throwException('Billing Address is always required'); } - $orderDataArray = [ - "externalReference" => $quote->getReservedOrderId(), - "merchant" => [ - "id" => $this->config->getMerchantId(), - ], - "redirectToStoreUrl" => $this->urlBuilder->getUrl('rvvup/redirect/in'), - "total" => [ - "amount" => $this->toCurrency($quote->getGrandTotal()), - "currency" => $currencyCode, - ], - "discountTotal" => [ - "amount" => $discountTotal, - "currency" => $currencyCode, - ], - "shippingTotal" => [ - "amount" => '0.00', // Default to 0.00. - "currency" => $currencyCode, - ], - "taxTotal" => [ - "amount" => $taxTotal, - "currency" => $currencyCode, - ], - "items" => $this->renderItems($quote), - 'customer' => [ - "givenName" => $billingAddress->getFirstname() ?? $quote->getCustomerFirstName(), - "surname" => $billingAddress->getLastname() ?? $quote->getCustomerLastName(), - "phoneNumber" => $billingAddress->getTelephone(), - "email" => $billingAddress->getEmail() ?? $quote->getCustomerEmail(), - ], - 'billingAddress' => $this->renderAddress($quote->getBillingAddress()), - 'requiresShipping' => false // Default to false. - ]; + $orderDataArray = $this->renderBase($quote, $express); + $orderDataArray['customer'] = $this->renderCustomer($quote, $express, $billingAddress); - $orderDataArray['method'] = str_replace('rvvup_', '', $quote->getPayment()->getMethod()); + // Set external reference if this is NOT a request to build express payment data. + $orderDataArray['externalReference'] = $express ? null : $quote->getReservedOrderId(); + $orderDataArray['billingAddress'] = $this->renderBillingAddress($quote, $express, $billingAddress); // We do not require shipping data for virtual orders (orders without tangible items). if ($quote->getIsVirtual()) { @@ -84,18 +72,68 @@ public function build(CartInterface $quote): array $shippingAddress = $quote->getShippingAddress(); - if ($shippingAddress === null) { + // Validate that Shipping Address exists if this is NOT a request to build express payment data. + if (!$express && $shippingAddress === null) { $this->throwException('Shipping Address is required for this order'); } + $orderDataArray['shippingAddress'] = $this->renderShippingAddress($quote, $express, $shippingAddress); + // As we have tangible products, the order will require shipping. - $orderDataArray['requiresShipping'] = true; $orderDataArray['shippingTotal']['amount'] = $this->toCurrency($shippingAddress->getShippingAmount()); - $orderDataArray['shippingAddress'] = $this->renderAddress($quote->getShippingAddress()); return $orderDataArray; } + /** + * Get the base data, common for all Rvvup payment request types. + * + * @param \Magento\Quote\Api\Data\CartInterface $quote + * @param bool $express + * @return array + * @throws \Rvvup\Payments\Exception\QuoteValidationException + */ + private function renderBase(CartInterface $quote, bool $express = false): array + { + $payment = $quote->getPayment(); + + // Validate the quote/order is paid via Rvvup. + if ($payment === null + || $payment->getMethod() === null + || strpos($payment->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0 + ) { + $this->throwException('This order is not paid via Rvvup'); + } + + return [ + "merchant" => [ + "id" => $this->config->getMerchantId(), + ], + "type" => $express ? "EXPRESS" : "STANDARD", + "redirectToStoreUrl" => $this->urlBuilder->getUrl('rvvup/redirect/in'), + "total" => [ + "amount" => $this->toCurrency($quote->getGrandTotal()), + "currency" => $quote->getQuoteCurrencyCode(), + ], + "discountTotal" => [ + "amount" => $this->toCurrency($quote->getBaseSubtotal() - $quote->getBaseSubtotalWithDiscount()), + "currency" => $quote->getQuoteCurrencyCode(), + ], + "shippingTotal" => [ + "amount" => '0.00', // Default to 0.00. + "currency" => $quote->getQuoteCurrencyCode(), + ], + "taxTotal" => [ + "amount" => $this->toCurrency($quote->getTotals()['tax']->getValue()), + "currency" => $quote->getQuoteCurrencyCode(), + ], + "items" => $this->renderItems($quote), + "requiresShipping" => !$quote->getIsVirtual(), + // Quote should always have a payment method set and never null when this is called. + "method" => str_replace(Method::PAYMENT_TITLE_PREFIX, '', $payment->getMethod()) + ]; + } + /** * @param \Magento\Quote\Api\Data\CartInterface $quote * @return array @@ -142,6 +180,114 @@ private function renderItems(CartInterface $quote): array return $built; } + /** + * @param \Magento\Quote\Api\Data\CartInterface $quote + * @param bool $express + * @param \Magento\Quote\Api\Data\AddressInterface|null $billingAddress + * @return array|null + */ + private function renderCustomer( + CartInterface $quote, + bool $express = false, + ?AddressInterface $billingAddress = null + ): ?array { + // If we have an express payment and quote belongs to a customer, get customer data from customer object. + if ($express && $quote->getCustomer() !== null && $quote->getCustomer()->getId() !== null) { + $customerBillingAddress = $quote->getCustomer()->getDefaultBilling() !== null + ? $this->renderCustomerAddress((int) $quote->getCustomer()->getDefaultBilling()) + : null; + + return [ + 'givenName' => $quote->getCustomer()->getFirstname() ?? '', + 'surname' => $quote->getCustomer()->getLastname() ?? '', + 'phoneNumber' => $customerBillingAddress !== null ? $customerBillingAddress['phoneNumber'] : '', + 'email' => $quote->getCustomer()->getEmail() ?? '', + ]; + } + + // Otherwise, if we have a billing address, use it to set customer data. + if ($billingAddress !== null + && ($billingAddress->getFirstname() !== null || $billingAddress->getLastname() !== null) + ) { + return [ + 'givenName' => $billingAddress->getFirstname() ?? '', + 'surname' => $billingAddress->getLastname() ?? '', + 'phoneNumber' => $billingAddress->getTelephone() ?? '', + 'email' => $billingAddress->getEmail() ?? '', + ]; + } + + // If billing address null & we don't have quote data, return null. + if ($quote->getCustomerFirstName() === null + && $quote->getCustomerFirstName() === null + && $quote->getCustomerEmail() === null + ) { + return null; + } + + // Otherwise set the data. + return [ + 'givenName' => $quote->getCustomerFirstName() ?? '', + 'surname' => $quote->getCustomerLastName() ?? '', + 'phoneNumber' => '', + 'email' => $quote->getCustomerEmail() ?? '', + ]; + } + + /** + * @param \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote $quote + * @param bool $express + * @param \Magento\Quote\Api\Data\AddressInterface|null $billingAddress + * @return array|null + * @throws \Rvvup\Payments\Exception\QuoteValidationException + */ + private function renderBillingAddress( + CartInterface $quote, + bool $express = false, + ?AddressInterface $billingAddress = null + ): ?array { + // If not an express payment, return billing address data as normal. + if (!$express) { + return $billingAddress !== null ? $this->renderAddress($quote->getBillingAddress()) : null; + } + + // Otherwise generate the express payment create billing address from customer. + // If no default customer billing address, return null. + if ($quote->getCustomer()->getId() === null || $quote->getCustomer()->getDefaultBilling() === null) { + return null; + } + + // Otherwise, return customer billing address if full. + return $this->renderCustomerAddress((int) $quote->getCustomer()->getDefaultBilling()); + } + + /** + * @param \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote $quote + * @param bool $express + * @param \Magento\Quote\Api\Data\AddressInterface|null $shippingAddress + * @return array|null + * @throws \Rvvup\Payments\Exception\QuoteValidationException + */ + private function renderShippingAddress( + CartInterface $quote, + bool $express = false, + ?AddressInterface $shippingAddress = null + ): ?array { + // If not an express payment, return billing address data as normal. + if (!$express) { + return $shippingAddress !== null ? $this->renderAddress($quote->getShippingAddress()) : null; + } + + // Otherwise generate the express payment create billing address from customer. + // If no default customer billing address, return null. + if ($quote->getCustomer()->getId() === null || $quote->getCustomer()->getDefaultShipping() === null) { + return null; + } + + // Otherwise, return customer billing address if full. + return $this->renderCustomerAddress((int) $quote->getCustomer()->getDefaultShipping()); + } + /** * @param \Magento\Quote\Api\Data\AddressInterface $address * @return array @@ -166,6 +312,56 @@ private function renderAddress(AddressInterface $address): array ]; } + /** + * Get a customers address data by the customer's address id. + * + * @param int $customerAddressId + * @return array|null + */ + private function renderCustomerAddress(int $customerAddressId): ?array + { + try { + $address = $this->customerAddressRepository->getById($customerAddressId); + + // Return null if any required address data are missing. + if ($address->getFirstname() === null + || $address->getLastname() === null + || $address->getStreet() === null + || $address->getCity() === null + || $address->getPostcode() === null + || $address->getCountryId() === null + ) { + return null; + } + + $customerName = [ + $address->getFirstname(), + $address->getMiddlename() ?? '', + $address->getLastname() + ]; + + $street = $address->getStreet(); + + return [ + // Array filter removes empty values if no callback provided. + 'name' => implode(' ', array_filter(array_map('trim', $customerName))), + 'phoneNumber' => $address->getTelephone() ?? '', + 'company' => $address->getCompany() ?? '', + // We already validate that street property is not null. + 'line1' => is_array($street) && isset($street[0]) ? $street[0] : '', + 'line2' => is_array($street) && isset($street[1]) ? $street[1] : '', + 'city' => $address->getCity() ?? '', + 'state' => $address->getRegion() !== null && $address->getRegion()->getRegion() !== null + ? $address->getRegion()->getRegion() + : '', + 'postcode' => $address->getPostcode(), + 'countryCode' => $address->getCountryId() ?? '', + ]; + } catch (LocalizedException $ex) { + return null; + } + } + /** * @param float $amount * @return string diff --git a/Model/Payment/PaymentCreateExpress.php b/Model/Payment/PaymentCreateExpress.php new file mode 100644 index 00000000..537055cc --- /dev/null +++ b/Model/Payment/PaymentCreateExpress.php @@ -0,0 +1,177 @@ +dataObjectFactory = $dataObjectFactory; + $this->cartTotalRepository = $cartTotalRepository; + $this->paymentResource = $paymentResource; + $this->isPaymentMethodAvailable = $isPaymentMethodAvailable; + $this->logger = $logger; + } + + /** + * Instantiate (create) a Rvvup Express Payment through the API + * + * @param \Magento\Quote\Api\Data\CartInterface|\Magento\Quote\Model\Quote $quote + * @param string $methodCode + * @return bool + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Rvvup\Payments\Exception\PaymentValidationException + */ + public function execute(CartInterface $quote, string $methodCode): bool + { + // Strip the Magento Module's prefix on the payment method. + $this->validatePaymentMethodAvailableForQuote($quote, $methodCode); + + // Pass Rvvup method code in the expected naming convetion `rvvup_UPPERCASE-METHOD-CODE` + $payment = $this->getPaymentData($quote, Method::PAYMENT_TITLE_PREFIX . mb_strtoupper($methodCode)); + + $quote->setPayment($payment); + + $paymentMethod = $payment->getMethodInstance(); + + // This should be Rvvup which always requires initialization (to create the Rvvup Payment), so fail if not. + if (!$paymentMethod->isInitializeNeeded()) { + $this->logger->error('Initialization is required for Rvvup Payment methods', [ + 'quote_id' => $quote->getId(), + 'is_express' => true, + 'payment_method' => $methodCode + ]); + + throw new PaymentValidationException(__('Payment method not available')); + } + + $stateObject = $this->dataObjectFactory->create(); + $paymentMethod->initialize( + $paymentMethod->getConfigData('payment_action', $quote->getStoreId()), + $stateObject + ); + + $this->paymentResource->save($payment); + + return true; + } + + /** + * Validate the Rvvup payment method is available for the current quote. + * + * @param \Magento\Quote\Api\Data\CartInterface $quote + * @param string $methodCode + * @return void + * @throws \Rvvup\Payments\Exception\PaymentValidationException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function validatePaymentMethodAvailableForQuote(CartInterface $quote, string $methodCode): void + { + $totals = $this->cartTotalRepository->get($quote->getId()); + + if (!$this->isPaymentMethodAvailable->execute( + $methodCode, + $totals->getGrandTotal() === null ? '0' : (string) $totals->getGrandTotal(), + $quote->getCurrency() !== null && $quote->getCurrency()->getQuoteCurrencyCode() !== null + ? $quote->getCurrency()->getQuoteCurrencyCode() + : '' + )) { + throw new PaymentValidationException(__('Payment method not available')); + } + } + + /** + * @param \Magento\Quote\Api\Data\CartInterface $quote + * @param string $methodCode + * @return \Magento\Quote\Api\Data\PaymentInterface|\Magento\Quote\Model\Quote\Payment + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Rvvup\Payments\Exception\PaymentValidationException + */ + private function getPaymentData(CartInterface $quote, string $methodCode): PaymentInterface + { + $payment = $quote->getPayment(); + + try { + $payment->importData([ + PaymentInterface::KEY_METHOD => $methodCode, + // Set a flag that this is an Express Payment. This is saved in additional information by an observer. + PaymentInterface::KEY_ADDITIONAL_DATA => [ + Method::EXPRESS_PAYMENT_KEY => true + ], + 'checks' => [ + MethodInterface::CHECK_USE_CHECKOUT, + MethodInterface::CHECK_USE_FOR_CURRENCY, + MethodInterface::CHECK_ORDER_TOTAL_MIN_MAX, + MethodInterface::CHECK_ZERO_TOTAL, + ] + ]); + } catch (LocalizedException $ex) { + $this->logger->error( + 'Failed to import Rvvup payment method in Quote Payment with message: ' . $ex->getMessage(), + [ + 'quote_id' => $quote->getId(), + 'method_code' => $methodCode + ] + ); + + throw new PaymentValidationException(__('Payment method not available')); + } + + return $payment; + } +} diff --git a/Model/Payment/PaymentCreateExpressInterface.php b/Model/Payment/PaymentCreateExpressInterface.php new file mode 100644 index 00000000..13c85adc --- /dev/null +++ b/Model/Payment/PaymentCreateExpressInterface.php @@ -0,0 +1,21 @@ +sdkProxy = $sdkProxy; - $this->logger = $logger; + $this->paymentMethodsSettingsGet = $paymentMethodsSettingsGet; } /** @@ -44,38 +33,8 @@ public function __construct(SdkProxy $sdkProxy, LoggerInterface $logger) */ public function execute(string $value, string $currency, array $methodCodes = []): array { - $assets = []; - - $loadAll = empty($methodCodes); - - try { - foreach ($this->sdkProxy->getMethods($value, $currency) as $method) { - if (!isset($method['name']) || !is_string($method['name'])) { - continue; - } - - $methodName = mb_strtolower($method['name']); - - // If we wanted to load specific methods, check if method is one of the requested. - if (!$loadAll && !in_array($methodName, $methodCodes, true)) { - continue; - } - - $assets[Method::PAYMENT_TITLE_PREFIX . $methodName] = $method['settings']['assets'] ?? []; - } - } catch (Exception $ex) { - $this->logger->error( - 'Failed to load the payment method assets with message: ' . $ex->getMessage(), - [ - 'value' => $value, - 'currency' => $currency, - 'method_codes' => $methodCodes - ] - ); - - return $assets; - } - - return $assets; + return array_map(static function ($methodSettings) { + return $methodSettings['assets'] ?? []; + }, $this->paymentMethodsSettingsGet->execute($value, $currency, $methodCodes)); } } diff --git a/Model/PaymentMethodsAvailableGet.php b/Model/PaymentMethodsAvailableGet.php index c6156576..33d6c5f8 100644 --- a/Model/PaymentMethodsAvailableGet.php +++ b/Model/PaymentMethodsAvailableGet.php @@ -6,6 +6,7 @@ use Exception; use Psr\Log\LoggerInterface; +use Rvvup\Payments\Gateway\Method; class PaymentMethodsAvailableGet implements PaymentMethodsAvailableGetInterface { @@ -40,7 +41,13 @@ public function __construct(SdkProxy $sdkProxy, LoggerInterface $logger) public function execute(string $value, string $currency): array { try { - return $this->sdkProxy->getMethods($value, $currency); + $methods = []; + + foreach ($this->sdkProxy->getMethods($value, $currency) as $method) { + $methods[mb_strtolower(Method::PAYMENT_TITLE_PREFIX . $method['name'])] = $method; + } + + return $methods; } catch (Exception $ex) { $this->logger->error('Failed to load all available payment methods with message: ' . $ex->getMessage(), [ 'value' => $value, diff --git a/Model/PaymentMethodsSettingsGet.php b/Model/PaymentMethodsSettingsGet.php new file mode 100644 index 00000000..186baed6 --- /dev/null +++ b/Model/PaymentMethodsSettingsGet.php @@ -0,0 +1,65 @@ +paymentMethodsAvailableGet = $paymentMethodsAvailableGet; + } + + /** + * Get the settings for all/selected payment methods available for the value & currency. + * + * @param string $value + * @param string $currency + * @param array|string[] $methodCodes // Leave empty for all. + * @return array + */ + public function execute(string $value, string $currency, array $methodCodes = []): array + { + $methods = $this->paymentMethodsAvailableGet->execute($value, $currency); + + if (empty($methodCodes)) { + return $this->getSettingsArray($methods); + } + + // Format the methodCodes to be in the same format as in getAvailable method array keys. + $formattedMethodCodes = array_map(static function ($methodCodesValue) { + return mb_strtolower(Method::PAYMENT_TITLE_PREFIX . $methodCodesValue); + }, $methodCodes); + + // Get the methods with the matching array keys (we flip the formatted array values to keys for matching) + $filteredRequestedMethods = array_intersect_key($methods, array_flip($formattedMethodCodes)); + + return $this->getSettingsArray($filteredRequestedMethods); + } + + /** + * Return an array with preserved keys and settings value if present. + * + * @param array $methods + * @return array + */ + private function getSettingsArray(array $methods): array + { + return array_map(static function ($method) { + return $method['settings'] ?? []; + }, $methods); + } +} diff --git a/Model/ProcessOrder/Cancel.php b/Model/ProcessOrder/Cancel.php index 3be8e1e3..c3dfa2f0 100644 --- a/Model/ProcessOrder/Cancel.php +++ b/Model/ProcessOrder/Cancel.php @@ -125,7 +125,7 @@ public function execute(OrderInterface $order, array $rvvupData): ProcessOrderRe private function validateOrderPayment(OrderInterface $order): void { if ($order->getPayment() === null - || stripos($order->getPayment()->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0 + || strpos($order->getPayment()->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0 ) { throw new PaymentValidationException(__('Order is not paid via Rvvup')); } diff --git a/Model/ProcessOrder/Complete.php b/Model/ProcessOrder/Complete.php index 29d08adf..2d1d6782 100644 --- a/Model/ProcessOrder/Complete.php +++ b/Model/ProcessOrder/Complete.php @@ -68,7 +68,7 @@ public function __construct( public function execute(OrderInterface $order, array $rvvupData): ProcessOrderResultInterface { if ($order->getPayment() === null - || stripos($order->getPayment()->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0 + || strpos($order->getPayment()->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0 ) { throw new PaymentValidationException(__('Order is not paid via Rvvup')); } diff --git a/Model/ProcessOrder/Processing.php b/Model/ProcessOrder/Processing.php index 30609c55..c92b9012 100644 --- a/Model/ProcessOrder/Processing.php +++ b/Model/ProcessOrder/Processing.php @@ -60,7 +60,7 @@ public function execute(OrderInterface $order, array $rvvupData): ProcessOrderRe $processOrderResult = $this->processOrderResultFactory->create(); if ($order->getPayment() === null - || stripos($order->getPayment()->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0 + || strpos($order->getPayment()->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0 ) { throw new PaymentValidationException(__('Order is not paid via Rvvup')); } diff --git a/Model/Queue/Handler/Handler.php b/Model/Queue/Handler/Handler.php index d22d9882..bf6c010d 100644 --- a/Model/Queue/Handler/Handler.php +++ b/Model/Queue/Handler/Handler.php @@ -9,6 +9,7 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Psr\Log\LoggerInterface; use Rvvup\Payments\Api\WebhookRepositoryInterface; +use Rvvup\Payments\Gateway\Method; use Rvvup\Payments\Model\ConfigInterface; use Rvvup\Payments\Model\Payment\PaymentDataGetInterface; use Rvvup\Payments\Model\ProcessOrder\ProcessorPool; @@ -111,7 +112,7 @@ public function execute(int $id) $order = $this->orderRepository->get($payment->getParentId()); // if Payment method is not Rvvup, exit. - if (stripos($payment->getMethod(), 'rvvup_') !== 0) { + if (strpos($payment->getMethod(), Method::PAYMENT_TITLE_PREFIX) !== 0) { return; } diff --git a/Model/SdkProxy.php b/Model/SdkProxy.php index 8df46243..fa5c00c0 100644 --- a/Model/SdkProxy.php +++ b/Model/SdkProxy.php @@ -26,9 +26,16 @@ class SdkProxy */ private $getEnvironmentVersions; - /** @var array */ + /** + * @var array|null + */ private $methods; + /** + * @var array + */ + private $monetizedMethods = []; + /** * @param ConfigInterface $config * @param UserAgentBuilder $userAgent @@ -77,24 +84,29 @@ private function getSubject(): GraphQlSdk } /** - * @param string|null $cartTotal + * @param string|null $value * @param string|null $currency * @param array|null $inputOptions * @return array - * @throws \Exception */ - public function getMethods(string $cartTotal = null, string $currency = null, ?array $inputOptions = null): array + public function getMethods(string $value = null, string $currency = null, ?array $inputOptions = null): array { + // If value & currency are both not null, use separate method. + if ($value !== null && $currency !== null) { + return $this->getMethodsByValueAndCurrency($value, $currency, $inputOptions); + } + if (!$this->methods) { - $cartTotal = $cartTotal === null ? $cartTotal : (string) round((float) $cartTotal, 2); + $value = $value === null ? $value : (string) round((float) $value, 2); - $methods = $this->getSubject()->getMethods($cartTotal, $currency, $inputOptions); + $methods = $this->getSubject()->getMethods($value, $currency, $inputOptions); /** * Due to all Rvvup methods having the same `sort_order`values the way Magento sorts methods we need to * reverse the array so that they are presented in the order specified in the Rvvup dashboard */ - $this->methods = array_reverse($methods); + $this->methods = $this->filterApiMethods($methods); } + return $this->methods; } @@ -106,6 +118,27 @@ public function createOrder($orderData) return $this->getSubject()->createOrder($orderData); } + /** + * @param array $orderData + * @return false|mixed + * @throws \Exception + */ + public function updateExpressOrder(array $orderData) + { + $result = $this->getSubject()->updateExpressOrder($orderData); + + // The SDK strips the nested array keys out. Set them again for easier identification from Gateway Classes. + if (is_array($result)) { + return [ + 'data' => [ + 'orderExpressUpdate' => $result + ] + ]; + } + + return $result; + } + /** * {@inheritdoc} */ @@ -167,4 +200,39 @@ public function createEvent(string $eventType, string $reason, array $additional // Add any data send by the event, but keep core data untouched. $this->getSubject()->createEvent($eventType, $reason, array_merge($additionalData, $data)); } + + /** + * Get the Rvvup payment methods available for a specific value/productPrice/total & a specific currency. + * + * @param string $value + * @param string $currency + * @param array|null $inputOptions + * @return array + */ + private function getMethodsByValueAndCurrency(string $value, string $currency, ?array $inputOptions = null): array + { + if (!isset($this->monetizedMethods[$currency][$value])) { + $methods = $this->getSubject()->getMethods((string) round((float) $value, 2), $currency, $inputOptions); + + $this->monetizedMethods[$currency][$value] = $this->filterApiMethods($methods); + } + + return $this->monetizedMethods[$currency][$value]; + } + + /** + * Filter API returned methods. + * + * Rvvup methods having the same `sort_order`values in Magento, while Rvvup API call returns sorted methods. + * We get the array values & filter the results. Sorting order should be kept as in the Portal. + * + * @param array $methods + * @return array + */ + private function filterApiMethods(array $methods): array + { + return array_filter(array_values($methods), static function ($method) { + return isset($method['name']); + }); + } } diff --git a/Model/Validation/IsValidAddress.php b/Model/Validation/IsValidAddress.php new file mode 100644 index 00000000..1e74b930 --- /dev/null +++ b/Model/Validation/IsValidAddress.php @@ -0,0 +1,47 @@ +validationResultFactory = $validationResultFactory; + } + + /** + * @param \Magento\Customer\Model\Address\AbstractAddress $address + * @return \Magento\Framework\Validation\ValidationResult + */ + public function execute(AbstractAddress $address): ValidationResult + { + $validationResult = $address->validate(); + + $validationErrors = []; + + if ($validationResult !== true) { + $validationErrors = [__('Please check the shipping address information.')]; + } + + if (is_array($validationResult)) { + $validationErrors = array_merge($validationErrors, $validationResult); + } + + return $this->validationResultFactory->create(['errors' => $validationErrors]); + } +} diff --git a/Observer/DataAssignObserver.php b/Observer/DataAssignObserver.php index cb60a9ae..e1e8df05 100644 --- a/Observer/DataAssignObserver.php +++ b/Observer/DataAssignObserver.php @@ -5,18 +5,18 @@ use Magento\Framework\Event\Observer; use Magento\Payment\Observer\AbstractDataAssignObserver; use Magento\Quote\Api\Data\PaymentInterface; +use Rvvup\Payments\Gateway\Method; class DataAssignObserver extends AbstractDataAssignObserver { - private const PAYMENT_ID = 'id'; - private const DASHBOARD_URL = 'dashboard_url'; - /** * @var array */ protected $additionalInformationList = [ - self::PAYMENT_ID, - self::DASHBOARD_URL, + Method::ORDER_ID, + Method::DASHBOARD_URL, + Method::EXPRESS_PAYMENT_KEY, + Method::EXPRESS_PAYMENT_DATA_KEY ]; /** @@ -26,7 +26,7 @@ class DataAssignObserver extends AbstractDataAssignObserver public function execute(Observer $observer) { $method = $this->readMethodArgument($observer); - if (false === strpos($method->getCode(), 'rvvup_')) { + if (false === strpos($method->getCode(), Method::PAYMENT_TITLE_PREFIX)) { return; } diff --git a/Observer/Quote/Model/Quote/Item/RemoveExpressPaymentDataObserver.php b/Observer/Quote/Model/Quote/Item/RemoveExpressPaymentDataObserver.php new file mode 100644 index 00000000..e922d88b --- /dev/null +++ b/Observer/Quote/Model/Quote/Item/RemoveExpressPaymentDataObserver.php @@ -0,0 +1,63 @@ +cartExpressPaymentRemove = $cartExpressPaymentRemove; + $this->logger = $logger; + } + /** + * @param \Magento\Framework\Event\Observer $observer + * @return void + */ + public function execute(Observer $observer) + { + /** @var \Magento\Quote\Model\Quote\Item|null $item */ + $item = $observer->getData('item'); + + // No action if we don't have an item (should not happen as this is an event targeting items) or a quote ID. + if ($item === null || !$item->getQuoteId()) { + return; + } + + // Catch any errors not handled by service, so we don't interrupt customer journey. + try { + $this->cartExpressPaymentRemove->execute((string) $item->getQuoteId()); + } catch (Throwable $t) { + $this->logger->error( + 'Could not remove quote express payment data on quote item saved with message: ' . $t->getMessage(), + [ + 'quote_id' => $item->getQuoteId() + ] + ); + } + } +} diff --git a/Observer/Session/RemoveExpressPaymentDataObserver.php b/Observer/Session/RemoveExpressPaymentDataObserver.php new file mode 100644 index 00000000..c0f1e103 --- /dev/null +++ b/Observer/Session/RemoveExpressPaymentDataObserver.php @@ -0,0 +1,42 @@ +cartExpressPaymentRemove = $cartExpressPaymentRemove; + } + + /** + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer) + { + /** @var \Magento\Quote\Api\Data\CartInterface $quote */ + $quote = $observer->getData('quote'); + + if ($quote === null || !$quote->getId()) { + return; + } + + $this->cartExpressPaymentRemove->execute((string) $quote->getId()); + } +} diff --git a/Plugin/Checkout/ExpressPaymentValidateCustomerAddress.php b/Plugin/Checkout/ExpressPaymentValidateCustomerAddress.php new file mode 100644 index 00000000..f17508ab --- /dev/null +++ b/Plugin/Checkout/ExpressPaymentValidateCustomerAddress.php @@ -0,0 +1,83 @@ +hasCartExpressPayment = $hasCartExpressPayment; + $this->isValidAddress = $isValidAddress; + } + + /** + * Validate the shipping address on the method for Rvvup express payments. + * + * @param \Magento\Checkout\Api\ShippingInformationManagementInterface $subject + * @param int $cartId + * @param \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation + * @return null + * @throws \Magento\Framework\Validator\Exception + */ + public function beforeSaveAddressInformation( + ShippingInformationManagementInterface $subject, + $cartId, + ShippingInformationInterface $addressInformation + ) { + /** @var \Magento\Quote\Api\Data\AddressInterface|\Magento\Customer\Model\Address\AbstractAddress $shippingAddress */ + $shippingAddress = $addressInformation->getShippingAddress(); + + // Init validation shipping address is not null + if ($shippingAddress === null) { + return null; + } + + if (!$this->hasCartExpressPayment->execute((int) $cartId)) { + return null; + } + + // Now validate the address. + $validationResult = $this->isValidAddress->execute($shippingAddress); + + // No action if valid. + if ($validationResult->isValid()) { + return null; + } + + $messages = $validationResult->getErrors(); + $defaultMessage = array_shift($messages); + + if ($defaultMessage && !empty($messages)) { + $defaultMessage .= ' %1'; + } + + if ($defaultMessage) { + throw new Exception(__($defaultMessage, implode(' ', $messages))); + } + + return null; + } +} diff --git a/Plugin/JsLayout.php b/Plugin/JsLayout.php index bfc9acc4..5ceb2f02 100644 --- a/Plugin/JsLayout.php +++ b/Plugin/JsLayout.php @@ -8,6 +8,7 @@ use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; +use Rvvup\Payments\Gateway\Method; use Rvvup\Payments\Model\ConfigInterface; use Rvvup\Payments\Model\PaymentMethodsAvailableGetInterface; @@ -80,7 +81,7 @@ private function getRvvupMethods(): array $template = ['component' => 'Rvvup_Payments/js/view/payment/rvvup']; $template['methods'] = []; foreach ($loadedMethods as $method) { - $template['methods']['rvvup_' . $method['name']] = ['isBillingAddressRequired' => true]; + $template['methods'][Method::PAYMENT_TITLE_PREFIX . $method['name']] = ['isBillingAddressRequired' => true]; } return ['rvvup' => $template]; } diff --git a/Plugin/Quote/Api/PaymentMethodManagement/LimitCartExpressPayment.php b/Plugin/Quote/Api/PaymentMethodManagement/LimitCartExpressPayment.php new file mode 100644 index 00000000..a8fd8120 --- /dev/null +++ b/Plugin/Quote/Api/PaymentMethodManagement/LimitCartExpressPayment.php @@ -0,0 +1,44 @@ +get($cartId); + + if ($payment === null) { + return $result; + } + } catch (NoSuchEntityException $ex) { + return $result; + } + + // Do not limit if we don't have an express payment method set. + if ($payment->getadditionalInformation(Method::EXPRESS_PAYMENT_KEY) !== true) { + return $result; + } + + // Otherwise, filter it to limit to the express payment method. + $filteredResult = array_filter($result, static function ($value) use ($payment) { + return $value->getCode() === $payment->getMethod(); + }); + + return array_values($filteredResult); + } +} diff --git a/Plugin/Quote/Api/PaymentMethodManagement/RemoveExpressPaymentInfoOnMethodChange.php b/Plugin/Quote/Api/PaymentMethodManagement/RemoveExpressPaymentInfoOnMethodChange.php new file mode 100644 index 00000000..29eeebd3 --- /dev/null +++ b/Plugin/Quote/Api/PaymentMethodManagement/RemoveExpressPaymentInfoOnMethodChange.php @@ -0,0 +1,70 @@ +cartExpressPaymentRemove = $cartExpressPaymentRemove; + $this->logger = $logger; + } + + /** + * Remove express payment method additional information if payment method has changed. + * + * @param \Magento\Quote\Api\PaymentMethodManagementInterface $subject + * @param int $cartId + * @param \Magento\Quote\Api\Data\PaymentInterface $method + * @return null + */ + public function beforeSet(PaymentMethodManagementInterface $subject, $cartId, PaymentInterface $method) + { + // First get existing payment method for cart, return as is if none found. + try { + $payment = $subject->get($cartId); + } catch (NoSuchEntityException $ex) { + return null; + } + + if ($payment === null || !$payment->getId()) { + return null; + } + + // Then check if we handle the same payment method, if yes, we safely assume it's for the express payment. + if ($payment->getMethod() === $method->getMethod()) { + return null; + } + + // Finally, perform removing the express payment data. + if (!$this->cartExpressPaymentRemove->execute((string) $cartId)) { + $this->logger->error('Failed to remove express payment data on payment method change'); + } + + return null; + } +} diff --git a/ViewModel/Assets.php b/ViewModel/Assets.php index bd3ebd3d..0f0e3fdd 100644 --- a/ViewModel/Assets.php +++ b/ViewModel/Assets.php @@ -5,15 +5,23 @@ namespace Rvvup\Payments\ViewModel; use Exception; +use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\View\Element\Block\ArgumentInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface; use Rvvup\Payments\Api\PaymentMethodsAssetsGetInterface; +use Rvvup\Payments\Api\PaymentMethodsSettingsGetInterface; +use Rvvup\Payments\Gateway\Method; use Rvvup\Payments\Model\ConfigInterface; class Assets implements ArgumentInterface { + /** + * @var \Magento\Framework\Serialize\SerializerInterface + */ + private $serializer; + /** * @var \Rvvup\Payments\Model\ConfigInterface */ @@ -24,6 +32,11 @@ class Assets implements ArgumentInterface */ private $paymentMethodsAssetsGet; + /** + * @var \Rvvup\Payments\Api\PaymentMethodsSettingsGetInterface + */ + private $paymentMethodsSettingsGet; + /** * @var \Magento\Store\Model\StoreManagerInterface */ @@ -47,20 +60,31 @@ class Assets implements ArgumentInterface private $storeCurrency; /** + * @var array|null + */ + private $settings; + + /** + * @param \Magento\Framework\Serialize\SerializerInterface $serializer * @param \Rvvup\Payments\Model\ConfigInterface $config * @param \Rvvup\Payments\Api\PaymentMethodsAssetsGetInterface $paymentMethodsAssetsGet + * @param \Rvvup\Payments\Api\PaymentMethodsSettingsGetInterface $paymentMethodsSettingsGet * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Psr\Log\LoggerInterface|RvvupLog $logger * @return void */ public function __construct( + SerializerInterface $serializer, ConfigInterface $config, PaymentMethodsAssetsGetInterface $paymentMethodsAssetsGet, + PaymentMethodsSettingsGetInterface $paymentMethodsSettingsGet, StoreManagerInterface $storeManager, LoggerInterface $logger ) { + $this->serializer = $serializer; $this->config = $config; $this->paymentMethodsAssetsGet = $paymentMethodsAssetsGet; + $this->paymentMethodsSettingsGet = $paymentMethodsSettingsGet; $this->storeManager = $storeManager; $this->logger = $logger; } @@ -95,6 +119,26 @@ public function getPaymentMethodsScriptAssets(array $methodCodes = []): array return $scripts; } + /** + * Get the serialized Rvvup Parameters object. + * + * @return string + */ + public function getRvvupParametersJsObject(): string + { + $rvvupParameters = ['settings' => []]; + + foreach ($this->getPaymentMethodsSettings() as $key => $methodSettings) { + if (isset($methodSettings['assets'])) { + unset($methodSettings['assets']); + } + + $rvvupParameters['settings'][str_replace(Method::PAYMENT_TITLE_PREFIX, '', $key)] = $methodSettings; + } + + return $this->serializer->serialize($rvvupParameters); + } + /** * Get the generated ID for a script element by its method and index key. * @@ -129,6 +173,27 @@ public function getScriptDataAttributes(array $scriptData): array : $scriptData['attributes']; } + /** + * Get the settings of all payment methods if available. + * + * @return array + */ + private function getPaymentMethodsSettings(): array + { + if ($this->settings !== null) { + return $this->settings; + } + + // return empty array if we cannot get the store currency. + if ($this->getStoreCurrency() === null) { + return []; + } + + $this->settings = $this->paymentMethodsSettingsGet->execute('0', $this->getStoreCurrency()); + + return $this->settings; + } + /** * Get the assets of the requested (or all if none requested) payment methods if available. * diff --git a/ViewModel/CheckoutConfig.php b/ViewModel/CheckoutConfig.php new file mode 100644 index 00000000..e481c2ec --- /dev/null +++ b/ViewModel/CheckoutConfig.php @@ -0,0 +1,80 @@ +serializer = $serializer; + $this->storeManager = $storeManager; + $this->logger = $logger; + } + + /** + * @return bool|string + */ + public function getSerializedConfig() + { + $checkoutConfig = [ + 'storeCode' => $this->getCurrentStoreCode() + ]; + + foreach ($checkoutConfig as $key => $value) { + if (!isset($value)) { + unset($checkoutConfig[$key]); + } + } + + return $this->serializer->serialize($checkoutConfig); + } + + /** + * @return string + */ + private function getCurrentStoreCode(): string + { + try { + return $this->storeManager->getStore()->getCode(); + } catch (Exception $ex) { + // Should not happen on frontend but log error and return `default`. + $this->logger->error('Exception thrown when fetching current store with message: ' . $ex->getMessage()); + + return 'default'; + } + } +} diff --git a/ViewModel/PayPal.php b/ViewModel/PayPal.php new file mode 100644 index 00000000..14e0b2ce --- /dev/null +++ b/ViewModel/PayPal.php @@ -0,0 +1,146 @@ +config = $config; + $this->isPaymentMethodAvailable = $isPaymentMethodAvailable; + $this->storeManager = $storeManager; + $this->logger = $logger; + $this->apiSettingsProvider = $apiSettingsProvider; + } + + /** + * Sets the flag if paypal is available to use and returns it. + * It will always return either true/false. + * + * @param string $value + * @return bool + */ + public function isAvailable(string $value): bool + { + if (!$this->config->isActive()) { + return false; + } + + $storeCurrency = $this->getCurrentStoreCurrencyCode(); + + if ($storeCurrency === null) { + return false; + } + + return $this->isPaymentMethodAvailable->execute('paypal', $value, $storeCurrency); + } + + /** + * Can use PayPal on PDP for the current Product's Type. + * + * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @return bool + */ + public function canUseForProductType(ProductInterface $product): bool + { + switch ($product->getTypeId()) { + case 'grouped': + case 'bundle': + case null: + return false; + default: + return true; + } + } + + /** + * Each call should return a different ID, if no exception is thrown. + * Hence, save init call result in templates to reuse for same container id. + * + * @return string + */ + public function getButtonContainerId(): string + { + try { + return sprintf('rvvup-paypal-express-button-%s', random_int(PHP_INT_MIN, PHP_INT_MAX)); + } catch (Exception $e) { + /** + * Exception only thrown if an appropriate source of randomness cannot be found. + * https://www.php.net/manual/en/function.random-int.php + */ + return 'rvvup-paypal-express-button'; + } + } + + /** + * @return string|null + */ + private function getCurrentStoreCurrencyCode(): ?string + { + try { + $currency = $this->storeManager->getStore()->getCurrentCurrency(); + + return $currency === null ? null : $currency->getCode(); + } catch (Exception $ex) { + $this->logger->error( + 'Exception thrown when fetching current store\'s currency with message: ' . $ex->getMessage() + ); + + return null; + } + } + + public function getPayLaterMessagingValue(string $path) + { + if (in_array($path, ['enabled', 'textSize'])) { + return $this->apiSettingsProvider->getByPath('PAYPAL', "settings/product/payLaterMessaging/$path"); + } + return $this->apiSettingsProvider->getByPath('PAYPAL', "settings/product/payLaterMessaging/$path/value"); + } +} diff --git a/etc/di.xml b/etc/di.xml index 936b252f..068a6fcf 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -2,9 +2,29 @@ + + + + + + + + + + + + + + + @@ -108,44 +133,54 @@ - - + RvvupLog + - + RvvupLog - - - - - + - Magento\Checkout\Model\Session\Proxy RvvupLog - + - Magento\Checkout\Model\Session\Proxy RvvupLog - + - Magento\Checkout\Model\Session\Proxy + RvvupLog + + + + + + + + + + + + + + RvvupLog @@ -158,6 +193,12 @@ + + + RvvupLog + + + RvvupLog @@ -205,6 +246,7 @@ Magento\Checkout\Model\Session\Proxy + RvvupLog @@ -244,6 +286,12 @@ + + + RvvupLog + + + RvvupLog @@ -268,6 +316,12 @@ + + + RvvupLog + + + RvvupLog @@ -280,6 +334,18 @@ + + + RvvupLog + + + + + + RvvupLog + + + RvvupLog diff --git a/etc/events.xml b/etc/events.xml index a732cd03..cbb0f5fd 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -22,4 +22,10 @@ instance="Rvvup\Payments\Observer\Model\ProcessOrder\AddOrderHistoryCommentObserver" shared="false" /> + + + + diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml index a64211d8..a905c33c 100644 --- a/etc/frontend/di.xml +++ b/etc/frontend/di.xml @@ -1,6 +1,14 @@ + + + + + Rvvup\Payments\CustomerData\ExpressPayment + + + @@ -14,30 +22,73 @@ - + - Magento\Checkout\Model\Session\Proxy + Magento\Checkout\Model\Session\Proxy + Magento\Customer\Model\Session\Proxy + RvvupLog - + + + Magento\Checkout\Model\Session\Proxy + RvvupLog + + + + + Magento\Checkout\Model\Session\Proxy + RvvupLog + + + Magento\Checkout\Model\Session\Proxy + Magento\Customer\Model\Session\Proxy Magento\Checkout\Model\Session\Proxy + Magento\Customer\Model\Session\Proxy + + + + + + Magento\Checkout\Model\Session\Proxy + + + + + Magento\Checkout\Model\Session\Proxy + RvvupLog - - - captcha/refresh - customer/section/load - rvvup/redirect/in - - Magento\Checkout\Model\Session + + captcha/refresh + customer/section/load + rvvup/redirect/in + + Magento\Checkout\Model\Session\Proxy + + + + + + Magento\Checkout\Model\Session\Proxy + + + + + RvvupLog + + + + + RvvupLog diff --git a/etc/frontend/events.xml b/etc/frontend/events.xml index 99531046..5c0b91e2 100644 --- a/etc/frontend/events.xml +++ b/etc/frontend/events.xml @@ -1,9 +1,17 @@ - + - + - + + + + + diff --git a/etc/frontend/sections.xml b/etc/frontend/sections.xml new file mode 100644 index 00000000..b6ec5bda --- /dev/null +++ b/etc/frontend/sections.xml @@ -0,0 +1,92 @@ + + + + +
+ + +
+ + +
+ + + +
+ + + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + diff --git a/etc/webapi.xml b/etc/webapi.xml index 1882b47b..4f58090c 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -6,7 +6,71 @@ - + + + + + + + + + + + + + + %cart_id% + + + + + + + + + + + + + + + + %cart_id% + + + + + + + + + + + + + + + + %cart_id% + + + + + + + + + + + + + + + + %cart_id% + + + diff --git a/etc/webapi_rest/di.xml b/etc/webapi_rest/di.xml index 48f297e3..452e04b0 100644 --- a/etc/webapi_rest/di.xml +++ b/etc/webapi_rest/di.xml @@ -5,23 +5,30 @@ + + + + + - + - - - - RvvupLog - + + + + + RvvupLog diff --git a/i18n/en_US.csv b/i18n/en_US.csv index 868a223a..19b18578 100644 --- a/i18n/en_US.csv +++ b/i18n/en_US.csv @@ -1,2 +1,8 @@ "An error occurred while processing your payment.", "An error occurred while processing your payment.", module, Rvvup_Payments -"An error occurred while processing your payment (ID %1). Please contact us.", "An error occurred while processing your payment (ID %1). Please contact us.", module, Rvvup_Payments \ No newline at end of file +"An error occurred while processing your payment (ID %1). Please contact us.", "An error occurred while processing your payment (ID %1). Please contact us.", module, Rvvup_Payments +"Invalid payment method", "Invalid payment metxhod", module, Rvvup_Payments +"Payment method not available", "Payment method not available", module, Rvvup_Payments +"There was an error when processing your request", "There was an error when processing your request", module, Rvvup_Payments +"You cancelled the payment process", "You cancelled the payment process", module, Rvvup_Payments +"PayPal is not available at the moment", "PayPal is not available at the moment", module, Rvvup_Payments +"A numeric monetary value is required", "A numeric monetary value is required", module, Rvvup_Payments \ No newline at end of file diff --git a/view/frontend/layout/catalog_product_view.xml b/view/frontend/layout/catalog_product_view.xml index d01edea2..98b83b16 100644 --- a/view/frontend/layout/catalog_product_view.xml +++ b/view/frontend/layout/catalog_product_view.xml @@ -13,17 +13,42 @@ - + + ifconfig="payment/rvvup/active" + before="product.info"> Rvvup\Payments\ViewModel\Clearpay Rvvup\Payments\ViewModel\Restrictions + Rvvup\Payments\ViewModel\PayPal + + + + + + + + Rvvup\Payments\ViewModel\PayPal + + + + + Rvvup\Payments\ViewModel\CheckoutConfig + + + diff --git a/view/frontend/layout/catalog_product_view_type_configurable.xml b/view/frontend/layout/catalog_product_view_type_configurable.xml new file mode 100644 index 00000000..112cecd2 --- /dev/null +++ b/view/frontend/layout/catalog_product_view_type_configurable.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/view/frontend/layout/catalog_product_view_type_downloadable.xml b/view/frontend/layout/catalog_product_view_type_downloadable.xml index c90f8987..5c0e9caa 100644 --- a/view/frontend/layout/catalog_product_view_type_downloadable.xml +++ b/view/frontend/layout/catalog_product_view_type_downloadable.xml @@ -5,5 +5,20 @@ + + + + + + + + Rvvup\Payments\ViewModel\CheckoutConfig + + + diff --git a/view/frontend/requirejs-config.js b/view/frontend/requirejs-config.js index b5d1354f..9c4774f8 100755 --- a/view/frontend/requirejs-config.js +++ b/view/frontend/requirejs-config.js @@ -10,6 +10,12 @@ var config = { "Magento_Checkout/js/model/cart/totals-processor/default": { 'Rvvup_Payments/js/clearpay/clearpay-price-mixin' : true }, + 'Magento_Checkout/js/model/checkout-data-resolver': { + 'Rvvup_Payments/js/checkout-data-resolver-mixin': true + }, + 'Magento_Checkout/js/view/billing-address': { + 'Rvvup_Payments/js/view/billing-address-mixin': true + } } } }; diff --git a/view/frontend/templates/body/before-end/checkout-config.phtml b/view/frontend/templates/body/before-end/checkout-config.phtml new file mode 100644 index 00000000..7b796bda --- /dev/null +++ b/view/frontend/templates/body/before-end/checkout-config.phtml @@ -0,0 +1,29 @@ +getData('rvvup_payments_checkout_config_view_model'); + +if ($checkoutConfigViewModel === null) { + return; +} +?> + diff --git a/view/frontend/templates/head/additional/assets.phtml b/view/frontend/templates/head/additional/assets.phtml index 89afb6e2..0605e1dd 100644 --- a/view/frontend/templates/head/additional/assets.phtml +++ b/view/frontend/templates/head/additional/assets.phtml @@ -13,10 +13,11 @@ if (!isset($escaper)) { /** @var \Rvvup\Payments\ViewModel\Assets $assetsViewModel */ $assetsViewModel = $block->getData('rvvup_payments_assets_view_model'); -// Load specific payment methods if requested. -$paymentMethodsScriptAssets = $assetsViewModel !== null ? $assetsViewModel->getPaymentMethodsScriptAssets([]) : []; +if ($assetsViewModel === null) { + return; +} ?> - $scripts): ?> +getPaymentMethodsScriptAssets([]) as $method => $scripts): ?> $script): ?> @@ -34,3 +35,11 @@ $paymentMethodsScriptAssets = $assetsViewModel !== null ? $assetsViewModel->getP src="escapeUrl($assetsViewModel->getScriptElementSrc($script)) ?>"> + + diff --git a/view/frontend/templates/product/view/addtocart.phtml b/view/frontend/templates/product/view/addtocart.phtml index dd79c133..064427ad 100644 --- a/view/frontend/templates/product/view/addtocart.phtml +++ b/view/frontend/templates/product/view/addtocart.phtml @@ -1,72 +1,90 @@ getClearpay(); -$restrictions = $block->getRestrictions(); +/** @var \Rvvup\Payments\ViewModel\Clearpay $clearpay */ +$clearpay = $block->getData('clearpay'); +/** @var \Rvvup\Payments\ViewModel\Restrictions $restrictions */ +$restrictions = $block->getData('restrictions'); +/** @var \Rvvup\Payments\ViewModel\PayPal $paypalViewModel */ +$paypalViewModel = $block->getData('rvvup_payments_paypal_view_model'); + $product = $block->getProduct(); $thresholds = $clearpay->getThresholds($product); ?> showRestrictionMessage($product)): ?> - escapeHtml($restrictions->getMessages()->getPdpMessage()) ?> +

+ escapeHtml($restrictions->getMessages()->getPdpMessage()) ?> +

-isInThreshold($product, $thresholds)) { - return; -} -?> - -
- -
- -getTypeId() === Configurable::TYPE_CODE): // Load thresholds for configurable products ?> +isInThreshold($product, $thresholds)): ?> - -getTypeId() === Bundle::TYPE_CODE): // Load thresholds for bundle products ?> - + + + getTypeId() === 'bundle'): // Load thresholds for bundle products?> + + + +canUseForProductType($product) + && $paypalViewModel->isAvailable((string) $product->getFinalPrice()) + && $paypalViewModel->getPayLaterMessagingValue('enabled') === true +): ?> +
+
+
diff --git a/view/frontend/templates/product/view/info/addtocart.phtml b/view/frontend/templates/product/view/info/addtocart.phtml new file mode 100644 index 00000000..15f75dc9 --- /dev/null +++ b/view/frontend/templates/product/view/info/addtocart.phtml @@ -0,0 +1,33 @@ +getData('rvvup_payments_paypal_view_model'); +$product = $block->getProduct(); +?> + +canUseForProductType($product) + && $paypalViewModel->isAvailable((string) $product->getFinalPrice()) +): ?> + getButtonContainerId(); ?> +
+ + diff --git a/view/frontend/web/css/source/_module.less b/view/frontend/web/css/source/_module.less index 758f4ebf..4667671c 100644 --- a/view/frontend/web/css/source/_module.less +++ b/view/frontend/web/css/source/_module.less @@ -39,6 +39,10 @@ .clearpay { max-width: 100%; } + + .rvvup-paypal-paylater-messaging-container { + margin-top: 20px; + } } .rvvup-summary { @@ -73,6 +77,10 @@ margin-top: 10px; } } + + .rvvup-pp-message { + margin-bottom: 20px; + } } } } diff --git a/view/frontend/web/js/action/add-to-cart.js b/view/frontend/web/js/action/add-to-cart.js new file mode 100644 index 00000000..1f560c69 --- /dev/null +++ b/view/frontend/web/js/action/add-to-cart.js @@ -0,0 +1,35 @@ +define([ + 'mage/storage', + 'Magento_Checkout/js/model/url-builder', + 'Rvvup_Payments/js/helper/get-store-code', + 'Rvvup_Payments/js/helper/is-logged-in', +], function (storage, checkoutUrlBuilder, getStoreCode, isLoggedIn) { + 'use strict'; + + /** + * Add product to cart. + * + * @param {String} cartId + * @param {Object} payload + * @return {*} + */ + return function (cartId, payload) { + /* Set the store code for the checkout URL Builder */ + checkoutUrlBuilder.storeCode = getStoreCode(); + + let serviceUrl = isLoggedIn() ? + checkoutUrlBuilder.createUrl('/carts/mine/items', {}): + checkoutUrlBuilder.createUrl('/guest-carts/:cartId/items', { + cartId: cartId + }); + + return storage.post( + serviceUrl, + JSON.stringify(payload), + false, + 'application/json' + ).done((response) => { + return response; + }); + }; +}); diff --git a/view/frontend/web/js/action/checkout/payment/get-order-payment-actions.js b/view/frontend/web/js/action/checkout/payment/get-order-payment-actions.js new file mode 100644 index 00000000..d8c70824 --- /dev/null +++ b/view/frontend/web/js/action/checkout/payment/get-order-payment-actions.js @@ -0,0 +1,103 @@ +define([ + 'jquery', + 'underscore', + 'mage/storage', + 'Magento_Customer/js/model/customer', + 'Magento_Checkout/js/model/error-processor', + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/model/url-builder', + 'Rvvup_Payments/js/model/checkout/payment/order-payment-action', + 'Rvvup_Payments/js/model/checkout/payment/rvvup-method-properties', + ], function ( + $, + _, + storage, + customer, + errorProcessor, + quote, + urlBuilder, + orderPaymentAction, + rvvupMethodProperties + ) { + 'use strict'; + + /** + * API request to get Order Payment Actions for Rvvup Payments. + */ + return function (messageContainer) { + let serviceUrl = customer.isLoggedIn() ? + urlBuilder.createUrl('/rvvup/payments/mine/:cartId/payment-actions', { + cartId: quote.getQuoteId() + }) : + urlBuilder.createUrl('/rvvup/payments/:cartId/payment-actions', { + cartId: quote.getQuoteId() + }); + + return storage.get( + serviceUrl, + true, + 'application/json' + ).done(function (data) { + /* First check get the authorization action & throw error if we don't. */ + const paymentAction = _.find(data, function (action) { + return action.type === 'authorization' + }); + + if (typeof paymentAction === 'undefined') { + errorProcessor.process('There was an error when placing the order!', messageContainer) + + return; + } + + /* Set cancelUrl from cancelAction method */ + const cancelAction = _.find(data, function (action) { + return action.type === 'cancel' + }); + + orderPaymentAction.setCancelUrl( + typeof cancelAction !== 'undefined' && cancelAction.method === 'redirect_url' + ? cancelAction.value + : null + ); + + /* + * If we have a token authorization type method, then we should have a capture action. + * Return either the payment token or the capture URL for Express Payment Session + */ + if (paymentAction.method === 'token') { + const captureAction = _.find(data, function (action) { + return action.type === 'capture' + }); + + orderPaymentAction.setCaptureUrl( + typeof captureAction !== 'undefined' && captureAction.method === 'redirect_url' + ? captureAction.value + : null + ); + + orderPaymentAction.setPaymentToken(paymentAction.value); + + /* If it is an express payment complete, set the capture URL as the redirect URL. */ + if (rvvupMethodProperties.getIsExpressPaymentCheckout()) { + orderPaymentAction.setRedirectUrl(orderPaymentAction.getCaptureUrl()); + + return orderPaymentAction.getRedirectUrl(); + } + + return orderPaymentAction.getPaymentToken(); + } + + /* Otherwise, this should be standard redirect authorization. */ + if (paymentAction.method === 'redirect_url') { + orderPaymentAction.setRedirectUrl(paymentAction.value); + + return orderPaymentAction.getRedirectUrl(); + } + + throw 'Error placing order'; + }).fail(function (response) { + errorProcessor.process(response, messageContainer); + }); + }; + } +); diff --git a/view/frontend/web/js/action/checkout/payment/remove-express-payment.js b/view/frontend/web/js/action/checkout/payment/remove-express-payment.js new file mode 100644 index 00000000..d6e7feaf --- /dev/null +++ b/view/frontend/web/js/action/checkout/payment/remove-express-payment.js @@ -0,0 +1,31 @@ +define([ + 'mage/storage', + 'Magento_Customer/js/model/customer', + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/model/url-builder' +], function (storage, customer, quote, urlBuilder) { + 'use strict'; + + /** + * Remove express payment data. + * + * @param {String} cartId + * @param {Object} payload + * @return {*} + */ + return function () { + let serviceUrl = customer.isLoggedIn() ? + urlBuilder.createUrl('/rvvup/payments/carts/mine/express', {}): + urlBuilder.createUrl('/rvvup/payments/guest-carts/:cartId/express', { + cartId: quote.getQuoteId() + }); + + return storage.delete( + serviceUrl, + true, + 'application/json' + ).done((response) => { + return response; + }); + }; +}); diff --git a/view/frontend/web/js/action/create-cart.js b/view/frontend/web/js/action/create-cart.js new file mode 100644 index 00000000..e8036ef3 --- /dev/null +++ b/view/frontend/web/js/action/create-cart.js @@ -0,0 +1,31 @@ +define([ + 'mage/storage', + 'Magento_Checkout/js/model/url-builder', + 'Rvvup_Payments/js/helper/get-store-code', + 'Rvvup_Payments/js/helper/is-logged-in' +], function (storage, checkoutUrlBuilder, getStoreCode, isLoggedIn) { + 'use strict'; + + /** + * Create a new empty quote. + * + * Implement done only, allow failures to be captured by calling function. + * API Call response is the quoteId (or Masked Quote ID for guest). + * + * @return {*} + */ + return function () { + /* Set the store code for the checkout URL Builder */ + checkoutUrlBuilder.storeCode = getStoreCode(); + + let serviceUrl = isLoggedIn() ? + checkoutUrlBuilder.createUrl('/carts/mine', {}) : + checkoutUrlBuilder.createUrl('/guest-carts', {}); + + return storage.post( + serviceUrl + ).done((response) => { + return response; + }); + }; +}); diff --git a/view/frontend/web/js/action/create-express-payment.js b/view/frontend/web/js/action/create-express-payment.js new file mode 100644 index 00000000..7a890607 --- /dev/null +++ b/view/frontend/web/js/action/create-express-payment.js @@ -0,0 +1,35 @@ +define([ + 'jquery', + 'underscore', + 'mage/storage', + 'mage/url', + 'mage/cookies' +], function ($, _, storage, url) { + 'use strict'; + + /** + * Create Rvvup express payment order. + * + * @param {String} cartId + * @param {String} method + * @return {Object} + */ + return function (cartId, method) { + const payload = { + cart_id: cartId, + method_code: method, + form_key: $.mage.cookies.get('form_key') + } + + url.setBaseUrl(window.checkout.baseUrl); + + return storage.post( + url.build('rvvup/express/create'), + JSON.stringify(payload), + false, + 'application/json' + ).done((response) => { + return response; + }); + }; +}); diff --git a/view/frontend/web/js/action/empty-cart.js b/view/frontend/web/js/action/empty-cart.js new file mode 100644 index 00000000..d8aaa3fb --- /dev/null +++ b/view/frontend/web/js/action/empty-cart.js @@ -0,0 +1,35 @@ +define([ + 'mage/storage', + 'Magento_Checkout/js/model/url-builder', + 'Rvvup_Payments/js/helper/get-store-code', + 'Rvvup_Payments/js/helper/is-logged-in' +], function (storage, checkoutUrlBuilder, getStoreCode, isLoggedIn) { + 'use strict'; + + /** + * Empty existing quote. + * + * Implement done only, allow failures to be captured by calling function. + * API Call response is the quoteId (or Masked Quote ID for guest). + * + * @return {string} + */ + return function (cartId) { + /* Set the store code for the checkout URL Builder */ + checkoutUrlBuilder.storeCode = getStoreCode(); + + let serviceUrl = isLoggedIn() ? + checkoutUrlBuilder.createUrl('/rvvup/payments/carts/mine', {}): + checkoutUrlBuilder.createUrl('/rvvup/payments/guest-carts/:cartId', { + cartId: cartId + }); + + return storage.delete( + serviceUrl, + true, + 'application/json' + ).done((cartId) => { + return cartId; + }); + }; +}); diff --git a/view/frontend/web/js/action/remove-express-payment.js b/view/frontend/web/js/action/remove-express-payment.js new file mode 100644 index 00000000..20541264 --- /dev/null +++ b/view/frontend/web/js/action/remove-express-payment.js @@ -0,0 +1,34 @@ +define([ + 'mage/storage', + 'Magento_Checkout/js/model/url-builder', + 'Rvvup_Payments/js/helper/get-store-code', + 'Rvvup_Payments/js/helper/is-logged-in' +], function (storage, checkoutUrlBuilder, getStoreCode, isLoggedIn) { + 'use strict'; + + /** + * Remove express payment data. + * + * @param {string} cartId + * @param {Object} payload + * @return {*} + */ + return function (cartId) { + /* Set the store code for the checkout URL Builder */ + checkoutUrlBuilder.storeCode = getStoreCode(); + + let serviceUrl = isLoggedIn() ? + checkoutUrlBuilder.createUrl('/rvvup/payments/carts/mine/express', {}): + checkoutUrlBuilder.createUrl('/rvvup/payments/guest-carts/:cartId/express', { + cartId: cartId + }); + + return storage.delete( + serviceUrl, + true, + 'application/json' + ).done((response) => { + return response; + }); + }; +}); diff --git a/view/frontend/web/js/action/set-cart-billing-address.js b/view/frontend/web/js/action/set-cart-billing-address.js new file mode 100644 index 00000000..22984ad9 --- /dev/null +++ b/view/frontend/web/js/action/set-cart-billing-address.js @@ -0,0 +1,35 @@ +define([ + 'mage/storage', + 'Magento_Checkout/js/model/url-builder', + 'Rvvup_Payments/js/helper/get-store-code', + 'Rvvup_Payments/js/helper/is-logged-in' +], function (storage, checkoutUrlBuilder, getStoreCode, isLoggedIn) { + 'use strict'; + + /** + * Set billing address to the quote. + * + * @param {String} cartId + * @param {Object} payload + * @return {*} + */ + return function (cartId, payload) { + /* Set the store code for the checkout URL Builder */ + checkoutUrlBuilder.storeCode = getStoreCode(); + + let serviceUrl = isLoggedIn() ? + checkoutUrlBuilder.createUrl('/carts/mine/billing-address', {}): + checkoutUrlBuilder.createUrl('/guest-carts/:cartId/billing-address', { + cartId: cartId + }); + + return storage.post( + serviceUrl, + JSON.stringify(payload), + true, + 'application/json' + ).done((response) => { + return response; + }); + }; +}); diff --git a/view/frontend/web/js/action/set-session-message.js b/view/frontend/web/js/action/set-session-message.js new file mode 100644 index 00000000..f6e78902 --- /dev/null +++ b/view/frontend/web/js/action/set-session-message.js @@ -0,0 +1,37 @@ +define([ + 'mage/translate', + 'Magento_Customer/js/customer-data' +], function ($t, customerData) { + 'use strict'; + + /** + * Set message to session message. + * + * @param {String} message + * @return {void} + */ + return function (message, type) { + let customerMessages = customerData.get('messages')() || {}, + messages = customerMessages.messages || [], + messagesContainer = document.getElementsByClassName("page messages"); + + messages.push({ + text: $t(message), + type: type + }); + + customerMessages.messages = messages; + + customerData.set('messages', customerMessages); + + window.scrollTo({ + top: messagesContainer, + left: 0, + behavior: 'smooth' + }); + + setTimeout(function() { + customerData.set('messages', {}); + },6000); + }; +}); diff --git a/view/frontend/web/js/checkout-data-resolver-mixin.js b/view/frontend/web/js/checkout-data-resolver-mixin.js new file mode 100644 index 00000000..e2731019 --- /dev/null +++ b/view/frontend/web/js/checkout-data-resolver-mixin.js @@ -0,0 +1,80 @@ +/** + * Populating the billing and shipping addresses in the checkout + */ +define([ + 'mage/utils/wrapper', + 'Magento_Checkout/js/checkout-data', + 'Magento_Checkout/js/action/select-shipping-address', + 'Magento_Checkout/js/action/create-shipping-address', + 'Magento_Checkout/js/action/create-billing-address', + 'Magento_Checkout/js/model/address-converter', + 'Magento_Checkout/js/model/quote', + 'Rvvup_Payments/js/helper/checkout-data-helper', + 'Rvvup_Payments/js/helper/is-express-payment' +], function ( + wrapper, + checkoutData, + selectShippingAddress, + createShippingAddress, + createBillingAddress, + addressConverter, + quote, + checkoutDataHelper, + isExpressPayment +) { + 'use strict'; + + return function (checkoutDataResolver) { + + checkoutDataResolver.getShippingAddressFromCustomerAddressList = wrapper.wrapSuper( + checkoutDataResolver.getShippingAddressFromCustomerAddressList, + function () { + if (!isExpressPayment()) { + return this._super(); + } + let shippingAddressFromData = window.checkoutConfig.shippingAddressFromData; + if (shippingAddressFromData) { + checkoutData.setShippingAddressFromData(shippingAddressFromData); + checkoutData.setSelectedShippingAddress('new-customer-address'); + let shippingAddress = addressConverter.formAddressDataToQuoteAddress( + checkoutData.getShippingAddressFromData() + ); + checkoutData.setNewCustomerShippingAddress(shippingAddress); + createShippingAddress(shippingAddress); + selectShippingAddress(shippingAddress); + return shippingAddress; + } + return this._super(); + }); + + checkoutDataResolver.applyBillingAddress = wrapper.wrapSuper( + checkoutDataResolver.applyBillingAddress, + function () { + if (isExpressPayment() && quote.isVirtual()) { + let billingAddressFromData = checkoutDataHelper.getRvvupBillingAddress() || window.checkoutConfig.shippingAddressFromData; + if (billingAddressFromData) { + var newBillingAddress = createBillingAddress(billingAddressFromData); + checkoutData.setBillingAddressFromData(newBillingAddress); + } + } + + return this._super(); + }); + + checkoutDataResolver.applyBillingAddress = wrapper.wrapSuper( + checkoutDataResolver.applyBillingAddress, + function () { + // If we are on an express payment not on a virtual product without a billing address then use the + // shipping address. + if (isExpressPayment() && !quote.isVirtual() && !quote.billingAddress()) { + var shippingAddress = quote.shippingAddress(); + quote.billingAddress(shippingAddress); + } + + this._super(); + } + ) + + return checkoutDataResolver; + }; +}); diff --git a/view/frontend/web/js/helper/checkout-data-helper.js b/view/frontend/web/js/helper/checkout-data-helper.js new file mode 100644 index 00000000..328f73ed --- /dev/null +++ b/view/frontend/web/js/helper/checkout-data-helper.js @@ -0,0 +1,62 @@ +define([ + 'jquery', + 'Magento_Customer/js/customer-data' +], function ($, storage) { + 'use strict'; + + var cacheKey = 'checkout-data'; + + return { + /** + * @param {Object} data + */ + saveData: function (data) { + storage.set(cacheKey, data); + }, + + /** + * @return {*} + */ + initData: function () { + return { + 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage + 'shippingAddressFromData': null, //Shipping address pulled from persistence storage + 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer + 'selectedShippingRate': null, //Shipping rate pulled from persistence storage + 'selectedPaymentMethod': null, //Payment method pulled from persistence storage + 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage + 'billingAddressFromData': null, //Billing address pulled from persistence storage + 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer + }; + }, + + /** + * @return {*} + */ + getData: function () { + var data = storage.get(cacheKey)(); + + if ($.isEmptyObject(data)) { + data = $.initNamespaceStorage('mage-cache-storage').localStorage.get(cacheKey); + + if ($.isEmptyObject(data)) { + data = initData(); + saveData(data); + } + } + + return data; + }, + + setRvvupBillingAddress: function (data) { + var obj = this.getData(); + + obj.rvvupBillingAddress = data; + this.saveData(obj); + }, + + getRvvupBillingAddress: function () { + return this.getData().rvvupBillingAddress; + }, + } +}); diff --git a/view/frontend/web/js/helper/get-add-to-cart-payload.js b/view/frontend/web/js/helper/get-add-to-cart-payload.js new file mode 100644 index 00000000..57cd5ef1 --- /dev/null +++ b/view/frontend/web/js/helper/get-add-to-cart-payload.js @@ -0,0 +1,38 @@ +define(['Rvvup_Payments/js/helper/get-form-data'], function (getFormData) { + 'use strict'; + + /** + * @param {Element} form + * @param {string} cartId + * @return {Object} + */ + return function (form, cartId) { + const formData = getFormData(form); + + let itemData = { + sku: formData.productSku, + qty: formData.qty, + quote_id: cartId + }; + + if (formData.super_attribute) { + let optionsArray = []; + for (const option in formData.super_attribute) { + optionsArray.push({ + option_id: option, + option_value: formData.super_attribute[option] + }); + } + itemData.product_type = "configurable"; + itemData.product_option = { + extension_attributes: { + configurable_item_options: optionsArray + } + }; + } + + return { + cartItem: itemData + }; + }; +}); diff --git a/view/frontend/web/js/helper/get-current-quote-id.js b/view/frontend/web/js/helper/get-current-quote-id.js new file mode 100644 index 00000000..63bd7c83 --- /dev/null +++ b/view/frontend/web/js/helper/get-current-quote-id.js @@ -0,0 +1,14 @@ +define([ + 'Rvvup_Payments/js/model/customer-data/express-payment' +], function (expressPaymentData) { + 'use strict'; + + /** + * Get the current store code from either the section data or the window checkoutConfig object. + * + * @return {string|null} + */ + return function () { + return expressPaymentData.getQuoteId(); + }; +}); diff --git a/view/frontend/web/js/helper/get-form-data.js b/view/frontend/web/js/helper/get-form-data.js new file mode 100644 index 00000000..13a36a2a --- /dev/null +++ b/view/frontend/web/js/helper/get-form-data.js @@ -0,0 +1,43 @@ +define(function () { + 'use strict'; + + /** + * @param {Element} form + * @return {Object} + */ + return function (form) { + const object = {}; + const formData = new FormData(form); + + formData.forEach((value, key) => { + // Special case for super attributes. + if (key.includes('super_attribute')) { + object['super_attribute'] = object['super_attribute'] || {}; + const test = /\[(.+)\]/.exec(key); + + if (test && test[1]) { + object['super_attribute'][test[1]] = parseInt(value, 10); + } + return; + } + + if (!Reflect.has(object, key)) { + object[key] = value; + return; + } + + if (!Array.isArray(object[key])) { + object[key] = [object[key]]; + } + object[key].push(value); + }); + + if (typeof object.product === 'string') { + object.product = parseInt(object.product, 10); + } + + object.productSku = typeof form.dataset.productSku !== 'undefined' ? form.dataset.productSku : ''; + + return object; + }; +}); diff --git a/view/frontend/web/js/helper/get-paypal-checkout-button-style.js b/view/frontend/web/js/helper/get-paypal-checkout-button-style.js new file mode 100644 index 00000000..1d72dcf2 --- /dev/null +++ b/view/frontend/web/js/helper/get-paypal-checkout-button-style.js @@ -0,0 +1,45 @@ +define([ + '!domReady' +], function () { + 'use strict'; + + /** + * Get PayPal's button styling for Checkout from the window rvvup_parameters object. + * + * Fallback to Rvvup's Checkout default, if rvvup_parameters object is not set. + * + * @return {Object} + */ + return function () { + if (typeof rvvup_parameters !== 'object') { + return { + layout: 'vertical', + color: 'blue', + shape: 'rect', + label: 'paypal', + tagline: false + } + } + + const layout = rvvup_parameters?.settings?.paypal?.checkout?.button?.layout?.value || 'vertical'; + const color = rvvup_parameters?.settings?.paypal?.checkout?.button?.color?.value || 'blue'; + const shape = rvvup_parameters?.settings?.paypal?.checkout?.button?.shape?.value || 'rect'; + const label = rvvup_parameters?.settings?.paypal?.checkout?.button?.label?.value || 'paypal'; + const tagline = rvvup_parameters?.settings?.paypal?.checkout?.button?.tagline || false; + const size = rvvup_parameters?.settings?.paypal?.checkout?.button?.size || null; + + let style = { + layout: layout, + color: color, + shape: shape, + label: label, + tagline: tagline, + }; + + if (size !== null) { + style.height = size; + } + + return style; + }; +}); \ No newline at end of file diff --git a/view/frontend/web/js/helper/get-paypal-pdp-button-style.js b/view/frontend/web/js/helper/get-paypal-pdp-button-style.js new file mode 100644 index 00000000..c91e483d --- /dev/null +++ b/view/frontend/web/js/helper/get-paypal-pdp-button-style.js @@ -0,0 +1,45 @@ +define([ + '!domReady' +], function () { + 'use strict'; + + /** + * Get PayPal's button styling for PDP from the window rvvup_parameters object. + * + * Fallback to Rvvup's PDP default, if rvvup_parameters object is not set. + * + * @return {Object} + */ + return function () { + if (typeof rvvup_parameters !== 'object') { + return { + layout: 'vertical', + color: 'gold', + shape: 'rect', + label: 'paypal', + tagline: false, + }; + } + + const layout = rvvup_parameters?.settings?.paypal?.product?.button?.layout?.value || 'vertical'; + const color = rvvup_parameters?.settings?.paypal?.product?.button?.color?.value || 'gold'; + const shape = rvvup_parameters?.settings?.paypal?.product?.button?.shape?.value || 'rect'; + const label = rvvup_parameters?.settings?.paypal?.product?.button?.label?.value || 'paypal'; + const tagline = rvvup_parameters?.settings?.paypal?.product?.button?.tagline || false; + const size = rvvup_parameters?.settings?.paypal?.product?.button?.size || null; + + let style = { + layout: layout, + color: color, + shape: shape, + label: label, + tagline: tagline, + }; + + if (size !== null) { + style.height = size; + } + + return style; + }; +}); \ No newline at end of file diff --git a/view/frontend/web/js/helper/get-pdp-form.js b/view/frontend/web/js/helper/get-pdp-form.js new file mode 100644 index 00000000..8f066876 --- /dev/null +++ b/view/frontend/web/js/helper/get-pdp-form.js @@ -0,0 +1,10 @@ +define(function () { + 'use strict'; + + /** + * @param {Element} element + */ + return function (element) { + return element.closest('form#product_addtocart_form'); + }; +}); diff --git a/view/frontend/web/js/helper/get-store-code.js b/view/frontend/web/js/helper/get-store-code.js new file mode 100644 index 00000000..65829d1f --- /dev/null +++ b/view/frontend/web/js/helper/get-store-code.js @@ -0,0 +1,24 @@ +define([ + 'Rvvup_Payments/js/model/customer-data/express-payment' +], function (expressPaymentData) { + 'use strict'; + + /** + * Get the current store code from either the section data or the window checkoutConfig object. + * + * @return {String} + */ + return function () { + let storeCode = expressPaymentData.getStoreCode(); + + if (storeCode !== null) { + return storeCode; + } + + if (typeof window.checkoutConfig !== 'undefined' && window.checkoutConfig.hasOwnProperty('storeCode')) { + return window.checkoutConfig.storeCode; + } + + return ''; + }; +}); diff --git a/view/frontend/web/js/helper/is-express-payment.js b/view/frontend/web/js/helper/is-express-payment.js new file mode 100644 index 00000000..03ce0f4d --- /dev/null +++ b/view/frontend/web/js/helper/is-express-payment.js @@ -0,0 +1,9 @@ +define([ + 'Rvvup_Payments/js/model/customer-data/express-payment' +], function (expressPaymentData) { + 'use strict'; + + return function () { + return expressPaymentData.isExpressPayment(); + }; +}); diff --git a/view/frontend/web/js/helper/is-logged-in.js b/view/frontend/web/js/helper/is-logged-in.js new file mode 100644 index 00000000..f74c232a --- /dev/null +++ b/view/frontend/web/js/helper/is-logged-in.js @@ -0,0 +1,9 @@ +define([ + 'Rvvup_Payments/js/model/customer-data/express-payment' +], function (expressPaymentData) { + 'use strict'; + + return function () { + return expressPaymentData.getIsLoggedIn(); + }; +}); diff --git a/view/frontend/web/js/helper/is-paypal-pdp-button-enabled.js b/view/frontend/web/js/helper/is-paypal-pdp-button-enabled.js new file mode 100644 index 00000000..deec0781 --- /dev/null +++ b/view/frontend/web/js/helper/is-paypal-pdp-button-enabled.js @@ -0,0 +1,12 @@ +define([ + '!domReady' +], function (expressPaymentData) { + 'use strict'; + + /** + * Is PDP button enabled for PayPal? + */ + return function () { + return rvvup_parameters?.settings?.paypal?.product?.button?.enabled || false; + }; +}); diff --git a/view/frontend/web/js/helper/validate-pdp-form.js b/view/frontend/web/js/helper/validate-pdp-form.js new file mode 100644 index 00000000..0895f697 --- /dev/null +++ b/view/frontend/web/js/helper/validate-pdp-form.js @@ -0,0 +1,17 @@ +define(['jquery'], function ($) { + 'use strict'; + + return function (form, clearError) { + if (!form || !form.length) { + return false; + } + + const isValid = $(form).valid(); + + if (clearError) { + $(form).validation('clearError'); + } + + return isValid; + }; +}); diff --git a/view/frontend/web/js/method/paypal/button.js b/view/frontend/web/js/method/paypal/button.js new file mode 100644 index 00000000..4eff3d08 --- /dev/null +++ b/view/frontend/web/js/method/paypal/button.js @@ -0,0 +1,392 @@ +define( + [ + 'uiRegistry', + 'uiComponent', + 'jquery', + 'underscore', + 'mage/storage', + 'mage/translate', + 'Rvvup_Payments/js/action/add-to-cart', + 'Rvvup_Payments/js/action/create-cart', + 'Rvvup_Payments/js/action/create-express-payment', + 'Rvvup_Payments/js/action/empty-cart', + 'Rvvup_Payments/js/action/remove-express-payment', + 'Rvvup_Payments/js/action/set-cart-billing-address', + 'Rvvup_Payments/js/action/set-session-message', + 'Rvvup_Payments/js/helper/get-add-to-cart-payload', + 'Rvvup_Payments/js/helper/get-current-quote-id', + 'Rvvup_Payments/js/helper/get-paypal-pdp-button-style', + 'Rvvup_Payments/js/helper/get-pdp-form', + 'Rvvup_Payments/js/helper/is-paypal-pdp-button-enabled', + 'Rvvup_Payments/js/helper/validate-pdp-form', + 'domReady!' + ], + function ( + registry, + Component, + $, + _, + storage, + $t, + addToCart, + createCart, + createExpressPayment, + emptyCart, + removeExpressPayment, + setCartBillingAddress, + setSessionMessage, + getAddToCartPayload, + getCurrentQuoteId, + getPayPalPdpButtonStyle, + getPdpForm, + isPayPalPdpButtonEnabled, + validatePdpForm + ) { + 'use strict'; + return Component.extend({ + defaults: { + buttonId: null, + cartId: null + }, + /** + * Component initialization function. + * + * We expect the buttonId to be provided when component is initialized, if none, no action. + * Also, no action if paypal button is disabled on pdp from Rvvup. + */ + initialize: function (config) { + this.buttonId = config.buttonId; + + if (this.buttonId === null || !isPayPalPdpButtonEnabled()) { + return this; + } + + this.renderPayPalButton(this.buttonId); + + return this; + }, + /** + * Instantiate PayPal Button on the related container. + * + * If button element does not exist or already has children elements (PayPal already loaded), no action. + * + * @param buttonId + */ + renderPayPalButton: function (buttonId) { + let self = this, + buttonElement = document.getElementById(buttonId); + + if (!buttonElement) { + console.error(buttonId + ' not found in DOM'); + return; + } + + if (buttonElement.childElementCount > 0) { + console.log('button already rendered'); + return; + } + + if (!window.rvvup_paypal) { + console.error('PayPal SDK not loaded'); + return; + } + + let form = getPdpForm(buttonElement); + + /* No action if no form is found */ + if (form.length === 0) { + return; + } + + rvvup_paypal.Buttons({ + style: getPayPalPdpButtonStyle(), + /** + * On PayPal button click instantiate and validate the steps allowing for errors. + * 1 - Validate form data + * 2 - Empty existing quote or create new quote + * 3 - Add to cart + * 4 - Create express payment + * + * Use async validation as per PayPal button docs + * @see https://developer.paypal.com/docs/checkout/standard/customize/validate-user-input/ + * + * @param data + * @param actions + * @returns {Promise|*} + */ + onClick: function(data, actions) { + $('body').trigger('processStart'); + return new Promise((resolve, reject) => { + return getCurrentQuoteId() === null + ? createCart() + .done((cartId) => { + self.cartId = cartId; + + return resolve(cartId); + }) + : emptyCart(getCurrentQuoteId()) + .done((cartId) => { + self.cartId = cartId; + + return resolve(cartId); + }); + }).then((cartId) => { + if (!validatePdpForm(form)) { + $('body').trigger('processStop'); + return actions.reject(); + } + else { + return addToCart(cartId, getAddToCartPayload(form, cartId)).done(() => { + return actions.resolve(); + }); + } + }).catch(() => { + $('body').trigger('processStop'); + setSessionMessage($t('Something went wrong'), 'error'); + return actions.reject(); + }); + }, + /** + * On create Order + * + * 1 - Create Express Payment Order, get the token from the order payment actions. + * + * @returns {Promise} + */ + createOrder: function() { + return new Promise((resolve, reject) => { + return createExpressPayment(self.cartId, 'paypal') + .done((response) => { + if (response.success === true) { + /* First check get the authorization action */ + let paymentAction = _.find( + response.data, + function(action) { + return action.type === 'authorization' + } + ); + + if (typeof paymentAction === 'undefined' + || !paymentAction.hasOwnProperty('method') + || paymentAction.method !== 'token' + ) { + return reject('PayPal is not available at the moment'); + } else { + return resolve(response); + } + } else { + return reject( + response.error_message.length > 0 + ? response.error_message + : 'Something went wrong' + ); + } + }).fail((error) => { + return reject(error); + }) + }).then((response) => { + const paymentObject = self.getResponsePaymentObject(response.data); + $('body').trigger('processStop'); + return paymentObject.paymentToken; + }).catch((error) => { + $('body').trigger('processStop'); + setSessionMessage($t(error), 'error'); + }); + }, + /** + * On PayPal approved, + * + * 1 - Update cart's billing address, combining shipping & billing data from PayPal. + * 2 - Redirect to the checkout page. + * + * @returns {Promise} + */ + onApprove: function (data, actions) { + return actions.order.get().then(function (orderData) { + /* Set billing to be used for shipping as well. */ + let shippingAddressPayload = { + address: self.getShippingAddressFromOrderData(orderData) + }, billingAddressPayload = { + address: self.getBillingAddressFromOrderData(orderData, shippingAddressPayload), + useForShipping: true + }; + + return new Promise((resolve, reject) => { + if (_.isEmpty(billingAddressPayload)) { + return true; + } + + return setCartBillingAddress(self.cartId, billingAddressPayload) + .done(() => { + return resolve(); + }).fail(() => { + return reject(); + }); + }).then(() => { + $('body').trigger('processStart'); + window.location.href = window.checkout.checkoutUrl; + }).catch((error) => { + console.log(error); + setSessionMessage($t('Something went wrong'), 'error'); + }); + }); + }, + /** + * On PayPal cancelled, cancel express payment (if cart is created) and close modal. + */ + onCancel: function () { + let bodyElement = $('body'); + bodyElement.trigger('processStart'); + + if (self.cartId === null) { + bodyElement.trigger('processStop'); + setSessionMessage($t('You cancelled the payment process'), 'error'); + return; + } + + $.when(removeExpressPayment(self.cartId)) + .done(() => { + bodyElement.trigger('processStop'); + setSessionMessage($t('You cancelled the payment process'), 'error'); + }); + + }, + /** + * On error, display error message in the container. + */ + onError: function () { + $('body').trigger('processStop'); + setSessionMessage($t('Something went wrong'), 'error'); + }, + }).render('#' + buttonId); + }, + /** + * Get a structured Payment Object from the Express Payment create response. + * + * @param response + * @return {Object} + */ + getResponsePaymentObject: function (response) { + const paymentObject = { + captureUrl: null, + cancelUrl: null, + paymentToken: null + }, + paymentAction = _.find(response, (action) => { + return action.type === 'authorization' + }), + captureAction = _.find(response, (action) => { + return action.type === 'capture' + }), + cancelAction = _.find(response, (action) => { + return action.type === 'cancel' + }); + + paymentObject.paymentToken = typeof paymentAction !== 'undefined' && paymentAction.method === 'token' + ? paymentAction.value + : null; + paymentObject.captureUrl = typeof captureAction !== 'undefined' && captureAction.method === 'redirect_url' + ? captureAction.value + : null; + paymentObject.cancelUrl = typeof cancelAction !== 'undefined' && cancelAction.method === 'redirect_url' + ? cancelAction.value + : null; + + return paymentObject; + }, + /** + * Get Billing address request data from PayPal order data & already set shipping address data. + * + * @param {Object} orderData + * @param {Object} shippingAddress + * @return {Object} + */ + getBillingAddressFromOrderData: function (orderData, shippingAddress) { + return { + firstname: orderData.payer.name.given_name, + lastname: orderData.payer.name.surname, + email: orderData.payer.email_address, + telephone: shippingAddress.address.telephone, + company: '', + street: shippingAddress.address.street, + city: shippingAddress.address.city, + region: shippingAddress.address.region, + postcode: shippingAddress.address.postcode, + country_id: shippingAddress.address.country_id, + } + }, + /** + * Get Shipping address request data from PayPal order data. + * + * @param {Object} orderData + * @return {Object} + */ + getShippingAddressFromOrderData: function (orderData) { + let address = { + firstname: '', + lastname: '', + email: '', + telephone: '', + street: [], + city: '', + region: '', + postcode: '', + country_id: '', + } + + /* Return empty object if no shipping property */ + if (orderData.purchase_units.length === 0 || + !orderData.purchase_units[0].hasOwnProperty("shipping") + ) { + return address; + } + + let shippingFullName = + orderData.purchase_units[0].shipping.hasOwnProperty('name') && + orderData.purchase_units[0].shipping.name.hasOwnProperty('full_name') + ? orderData.purchase_units[0].shipping.name.full_name + : ''; + let shippingFullNameArray = shippingFullName.split(' '); + + address.firstname = shippingFullNameArray.shift(); + + if (shippingFullNameArray.length > 0) { + address.lastname = shippingFullNameArray.join(' '); + } + + /* Return object if no address property */ + if (!orderData.purchase_units[0].shipping.hasOwnProperty('address')) { + return address; + } + + address.street.push(orderData.purchase_units[0].shipping.address.hasOwnProperty('address_line_1') + ? orderData.purchase_units[0].shipping.address.address_line_1 + : '' + ); + + address.street.push(orderData.purchase_units[0].shipping.address.hasOwnProperty('address_line_2') + ? orderData.purchase_units[0].shipping.address.address_line_2 + : '' + ); + + address.city = orderData.purchase_units[0].shipping.address.hasOwnProperty('admin_area_2') + ? orderData.purchase_units[0].shipping.address.admin_area_2 + : ''; + + address.region = orderData.purchase_units[0].shipping.address.hasOwnProperty('admin_area_1') + ? orderData.purchase_units[0].shipping.address.admin_area_1 + : ''; + + address.postcode = orderData.purchase_units[0].shipping.address.hasOwnProperty('postal_code') + ? orderData.purchase_units[0].shipping.address.postal_code + : ''; + + address.country_id = orderData.purchase_units[0].shipping.address.hasOwnProperty('country_code') + ? orderData.purchase_units[0].shipping.address.country_code + : ''; + + return address; + } + }); + } +); diff --git a/view/frontend/web/js/model/checkout/payment/order-payment-action.js b/view/frontend/web/js/model/checkout/payment/order-payment-action.js new file mode 100644 index 00000000..d84d9119 --- /dev/null +++ b/view/frontend/web/js/model/checkout/payment/order-payment-action.js @@ -0,0 +1,84 @@ +define([ + 'ko', + 'domReady!' +], function (ko) { + 'use strict'; + + let paymentToken = ko.observable(null), + redirectUrl = ko.observable(null), + captureUrl = ko.observable(null), + cancelUrl = ko.observable(null); + + return { + paymentToken: paymentToken, + redirectUrl: redirectUrl, + captureUrl: captureUrl, + cancelUrl: cancelUrl, + + /** + * @return {String|null} + */ + getPaymentToken: function () { + return paymentToken(); + }, + + /** + * @param {String|null} value + */ + setPaymentToken: function (value) { + paymentToken(value) + }, + + /** + * @return {String|null} + */ + getRedirectUrl: function () { + return redirectUrl(); + }, + + /** + * @param {String|null} value + */ + setRedirectUrl: function (value) { + redirectUrl(value) + }, + + /** + * @return {String|null} + */ + getCaptureUrl: function () { + return captureUrl(); + }, + + /** + * @param {String|null} value + */ + setCaptureUrl: function (value) { + captureUrl(value) + }, + + /** + * @return {String|null} + */ + getCancelUrl: function () { + return cancelUrl(); + }, + + /** + * @param {String|null} value + */ + setCancelUrl: function (value) { + cancelUrl(value) + }, + + /** + * Reset data to default. + */ + resetDefaultData: function () { + this.setPaymentToken(null); + this.setRedirectUrl(null); + this.setCaptureUrl(null); + this.setCancelUrl(null); + } + }; +}); diff --git a/view/frontend/web/js/model/checkout/payment/rvvup-method-properties.js b/view/frontend/web/js/model/checkout/payment/rvvup-method-properties.js new file mode 100644 index 00000000..c5860a81 --- /dev/null +++ b/view/frontend/web/js/model/checkout/payment/rvvup-method-properties.js @@ -0,0 +1,67 @@ +define([ + 'ko', + 'domReady!' +], function (ko) { + 'use strict'; + + let placedOrderId = ko.observable(null), + isCancellationTriggered = ko.observable(false), + isExpressPaymentCheckout = ko.observable(false); + + return { + placedOrderId: placedOrderId, + isCancellationTriggered: isCancellationTriggered, + isExpressPaymentCheckout: isExpressPaymentCheckout, + + /** + * @return {*} + */ + getPlacedOrderId: function () { + return placedOrderId(); + }, + + /** + * @param {*} value + */ + setPlacedOrderId: function (value) { + placedOrderId(value) + }, + + /** + * @return {Boolean} + */ + getIsCancellationTriggered: function () { + return isCancellationTriggered(); + }, + + /** + * @param {Boolean} value + */ + setIsCancellationTriggered: function (value) { + isCancellationTriggered(value) + }, + + /** + * @return {Boolean} + */ + getIsExpressPaymentCheckout: function () { + return isExpressPaymentCheckout(); + }, + + /** + * @param {Boolean} value + */ + setIsExpressPaymentCheckout: function (value) { + isExpressPaymentCheckout(value) + }, + + /** + * Reset data to default. + */ + resetDefaultData: function () { + this.setPlacedOrderId(null); + this.setIsCancellationTriggered(false); + this.setIsExpressPaymentCheckout(false); + } + }; +}); diff --git a/view/frontend/web/js/model/customer-data/express-payment.js b/view/frontend/web/js/model/customer-data/express-payment.js new file mode 100644 index 00000000..d927b9c6 --- /dev/null +++ b/view/frontend/web/js/model/customer-data/express-payment.js @@ -0,0 +1,49 @@ +define(['Magento_Customer/js/customer-data'], function (customerData) { + 'use strict'; + + return { + /** + * Get current session store code. + * + * @returns {string} + */ + getStoreCode: function() { + const expressPaymentData = customerData.get('rvvup-express-payment')(); + + return expressPaymentData.store_code ? expressPaymentData.store_code : null; + }, + + /** + * Get current session quote id. Masked ID if Guest user. + * + * @returns {string} + */ + getQuoteId: function () { + const expressPaymentData = customerData.get('rvvup-express-payment')(); + + return expressPaymentData.quote_id ? expressPaymentData.quote_id : null; + }, + + /** + * Get whether the current session user is logged in or not. + * + * @returns {boolean} + */ + getIsLoggedIn: function () { + const expressPaymentData = customerData.get('rvvup-express-payment')(); + + return expressPaymentData.is_logged_in ? expressPaymentData.is_logged_in : null; + }, + + /** + * Whether the current session quote is for an express Payment. + * + * @return {boolean} + */ + isExpressPayment: function() { + const expressPaymentData = customerData.get('rvvup-express-payment')(); + + return expressPaymentData.is_express_payment ? expressPaymentData.is_express_payment : false; + } + }; +}); diff --git a/view/frontend/web/js/view/billing-address-mixin.js b/view/frontend/web/js/view/billing-address-mixin.js new file mode 100644 index 00000000..6bd23bb7 --- /dev/null +++ b/view/frontend/web/js/view/billing-address-mixin.js @@ -0,0 +1,29 @@ +define([ + 'Magento_Checkout/js/model/quote', + 'Rvvup_Payments/js/helper/checkout-data-helper', + 'Rvvup_Payments/js/helper/is-express-payment', +],function (quote, checkoutDataHelper, isExpressPayment) { + 'use strict'; + + var mixin = { + initObservable: function () { + this._super(); + + var paymentMethod = quote.paymentMethod(); + + if (paymentMethod && this.dataScopePrefix.includes(paymentMethod.method)) { + quote.billingAddress.subscribe(function (newAddress) { + if (isExpressPayment() && quote.isVirtual()) { + checkoutDataHelper.setRvvupBillingAddress(newAddress); + } + }, this); + } + + return this; + } + }; + + return function (target) { + return target.extend(mixin); + }; +}); diff --git a/view/frontend/web/js/view/payment/method-renderer/rvvup-method.js b/view/frontend/web/js/view/payment/method-renderer/rvvup-method.js index 018af03e..0b1b83dd 100644 --- a/view/frontend/web/js/view/payment/method-renderer/rvvup-method.js +++ b/view/frontend/web/js/view/payment/method-renderer/rvvup-method.js @@ -1,46 +1,62 @@ define([ 'Magento_Checkout/js/view/payment/default', 'jquery', + 'mage/translate', 'Magento_Ui/js/modal/modal', 'text!Rvvup_Payments/template/modal.html', 'Magento_Checkout/js/model/totals', 'Magento_Checkout/js/model/full-screen-loader', - 'Magento_Checkout/js/model/url-builder', - 'mage/storage', - 'Magento_Customer/js/model/customer', 'Magento_Checkout/js/model/payment/additional-validators', - 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/model/error-processor', - 'underscore' + 'Magento_Checkout/js/model/quote', + 'Rvvup_Payments/js/action/checkout/payment/get-order-payment-actions', + 'Rvvup_Payments/js/action/checkout/payment/remove-express-payment', + 'Rvvup_Payments/js/helper/get-paypal-checkout-button-style', + 'Rvvup_Payments/js/helper/is-express-payment', + 'Rvvup_Payments/js/model/checkout/payment/order-payment-action', + 'Rvvup_Payments/js/model/checkout/payment/rvvup-method-properties' ], function ( Component, $, + $t, modal, popupTpl, totals, loader, - urlBuilder, - storage, - customer, additionalValidators, - quote, errorProcessor, - _ + quote, + getOrderPaymentActions, + removeExpressPayment, + getPayPalCheckoutButtonStyle, + isExpressPayment, + orderPaymentAction, + rvvupMethodProperties ) { 'use strict'; + return Component.extend({ defaults: { template: 'Rvvup_Payments/payment/rvvup', - redirectAfterPlaceOrder: false, - placedOrderId: null, - paymentToken: null, - redirectUrl: null, - captureUrl: null, - cancelUrl: null, - cancelledUrlTriggered: false, + redirectAfterPlaceOrder: false }, initialize: function () { this._super(); + /* Set express payment Checkout flag on component initialization */ + rvvupMethodProperties.setIsExpressPaymentCheckout(isExpressPayment()); + + quote.paymentMethod.subscribe(function (data) { + // If we move away from Paypal method and we already have an order ID then trigger cancel. + if (data.method !== 'rvvup_PAYPAL' && rvvupMethodProperties.getPlacedOrderId() !== null) { + this.cancelPayPalPayment(); + } + + // Make sure Data Method is paypal before we setup the event listener. + if (data.method === 'rvvup_PAYPAL') { + document.addEventListener('click', this.checkDomElement.bind(this)); + } + }.bind(this)); + window.addEventListener("message", (event) => { // Prevent listener firing on every component if (this.getCode() !== this.isChecked()) { @@ -87,27 +103,69 @@ define([ * Set placedOrderId attribute to use if required from the component. We expect an integer. * Set the value only if the payment was done via a Rvvup payment component. */ - $(document).ajaxSuccess(function(event, xhr, settings) { + $(document).ajaxSuccess(function (event, xhr, settings) { if (settings.type !== 'POST' || - xhr.statusCode !== 200 || + xhr.status !== 200 || !settings.url.includes('/payment-information') || !xhr.hasOwnProperty('responseJSON') ) { return; } - /* Check we are in component's property exist */ - if (!this.hasOwnProperty('placedOrderId') || typeof placedOrderId === 'undefined') { + /* Check we are in current component, by our model is defined */ + if (typeof rvvupMethodProperties === 'undefined') { return; } /* if response is a positive integer, set it as the order ID. */ - this.placedOrderId = /^\d+$/.test(xhr.responseJSON) ? xhr.responseJSON : null; + rvvupMethodProperties.setPlacedOrderId(/^\d+$/.test(xhr.responseJSON) ? xhr.responseJSON : null); }); + /* Cancel Express Payment on click event. */ + $(document).on('click', 'a#' + this.getCancelExpressPaymentLinkId(), (e) => { + e.preventDefault(); + + if (!rvvupMethodProperties.getIsExpressPaymentCheckout()) { + return; + } + + loader.startLoader(); + $.when(removeExpressPayment()) + .done(() => { + window.location.reload(); + }); + }) + }, + + checkDomElement: function(event) { + // Setup elements we want to make sure we cancel on. + const elements = document.querySelectorAll('button.action, span[id="block-discount-heading"], span[id="block-giftcard-heading"], .opc-progress-bar-item, input[id="billing-address-same-as-shipping-rvvup_PAYPAL"]'); + // Only check if we have a placeOrderID this shows if we have clicked on the cards + if (rvvupMethodProperties.getPlacedOrderId() !== null) { + // If we are not in the boundary and have clicked on the elements above cancel payment. + if(Array.from(elements).some(element => element.contains(event.target))) { + this.cancelPayPalPayment(); + document.removeEventListener("click", this.checkDomElement); + } + } + }, + + cancelPayPalPayment: function () { + var url = orderPaymentAction.getCancelUrl(); + this.resetDefaultData(); + loader.stopLoader(); + this.showModal(url); }, + + /** + * Render the PayPal button if the PayPal container is in place. + */ renderPayPalButton: function () { let self = this; + if (!this.getPayPalId()) { + return; + } + if (!document.getElementById(this.getPayPalId())) { console.error(this.getPayPalId() + ' not found in DOM'); return; @@ -124,12 +182,7 @@ define([ } rvvup_paypal.Buttons({ - style: { - layout: 'vertical', - color: 'blue', - shape: 'rect', - label: 'paypal' - }, + style: getPayPalCheckoutButtonStyle(), /** * On PayPal button click replicate core Magento JS Place Order functionality. * Use async validation as per PayPal button docs @@ -141,7 +194,7 @@ define([ * @param actions * @returns {Promise|*} */ - onClick: function(data, actions) { + onClick: function (data, actions) { if (self.validate() && additionalValidators.validate() && self.isPlaceOrderActionAllowed() === true @@ -149,12 +202,12 @@ define([ self.isPlaceOrderActionAllowed(false); return self.getPlaceOrderDeferredObject() .done(function () { - if (self.placedOrderId !== null) { + if (rvvupMethodProperties.getPlacedOrderId() !== null) { return actions.resolve(); } return actions.reject(); - }).fail(function() { + }).fail(function () { return actions.reject(); }).always(function () { self.isPlaceOrderActionAllowed(true); @@ -168,17 +221,18 @@ define([ * * @returns {Promise} */ - createOrder: function() { + createOrder: function () { loader.startLoader(); return new Promise((resolve, reject) => { - return $.when(self.getOrderPaymentActions()) - .done(function() { + return $.when(getOrderPaymentActions(self.messageContainer)) + .done(function () { return resolve(); - }).fail(function() { + }).fail(function () { return reject(); }); }).then(() => { - return self.paymentToken; + loader.stopLoader(); + return orderPaymentAction.getPaymentToken(); }); }, /** @@ -188,7 +242,7 @@ define([ */ onApprove: function () { return new Promise((resolve, reject) => { - resolve(self.captureUrl); + resolve(orderPaymentAction.getCaptureUrl()); }).then((url) => { self.resetDefaultData(); loader.stopLoader(); @@ -202,7 +256,7 @@ define([ */ onCancel: function () { return new Promise((resolve, reject) => { - resolve(self.cancelUrl); + resolve(orderPaymentAction.getCancelUrl()); }).then((url) => { self.resetDefaultData(); loader.stopLoader(); @@ -222,53 +276,106 @@ define([ }, }).render('#' + this.getPayPalId()); }, + + /** + * Get the paypal component's paypal button ID. + * + * @return {string} + */ getPayPalId: function () { if (this.isPayPalComponent()) { return 'paypalPlaceholder'; } }, + /** - * Validate if this is the PayPal component. + * Check whether we should display the PayPal Button. * - * @returns {boolean} + * @return {boolean} */ - isPayPalComponent: function() { - return this.index === 'rvvup_PAYPAL'; + shouldDisplayPayPalButton() { + return this.isPayPalComponent() && !rvvupMethodProperties.getIsExpressPaymentCheckout(); }, + /** - * After Place order actions. - * If PayPal payment, allow paypal buttons to handle logic. + * Check whether we should display the cancel Express Payment Link. Currently limited to PayPal. + * + * @return {false} */ - afterPlaceOrder: function () { - let self = this; - loader.startLoader(); + shouldDisplayCancelExpressPaymentLink() { + return rvvupMethodProperties.getIsExpressPaymentCheckout(); + }, - if (self.isPayPalComponent()) { - return; - } + /** + * Get the express payment cancellation link. + * + * @return {string} + */ + getCancelExpressPaymentLink() { + let cancelLink = '' + $t('here') + ''; + return $t('You are currently paying with %1. If you want to cancel this process, please click %2') + .replace('%1', this.getTitle()) + .replace('%2', cancelLink); + }, - $.when(self.getOrderPaymentActions()) - .done(function() { - if (self.redirectUrl !== null) { - self.showRvvupModal(self.redirectUrl); - } - }); + /** + * Get the ID for the express payment cancellation link + * + * @return {string} + */ + getCancelExpressPaymentLinkId() { + return 'cancel-express-payment-link-' + this.getCode(); + }, + + /** + * Validate if this is the PayPal component. + * + * @returns {Boolean} + */ + isPayPalComponent: function () { + return this.index === 'rvvup_PAYPAL'; }, + + /** + * Get the component's iframe with the related payment method summary_url. + * + * @return {string} + */ getIframe: function () { let grandTotal = parseFloat(totals.getSegment('grand_total').value); let url = window.checkoutConfig.payment[this.index].summary_url; return url.replace(/amount=(\d+\.\d+)&/, 'amount=' + grandTotal + '&') }, + + /** + * Get the component's logo URL. + * + * @return {string} + */ getLogoUrl: function () { return window.checkoutConfig.payment[this.index].logo; }, + + /** + * Get the component's description. + * + * @return {string} + */ getDescription: function () { return window.checkoutConfig.payment[this.index].description; }, + + /** + * Show the Rvvup's modal with the injected specified URL for the iframe's src attribute. + * + * This method seems redundant but Modal was not called after successful payment otherwise + * @param {string} url + */ showRvvupModal: function (url) { - /* Seems redundant but Modal was not called after successful payment otherwise */ this.showModal(url) }, + /** * Handle event when user clicks outside the modal. * @@ -278,18 +385,25 @@ define([ outerClickHandler: function (event) { this.triggerModalCancelUrl() }, + /** * Handle setting cancel URL in the modal, prevents multiple clicks. */ triggerModalCancelUrl: function () { - if (!this.cancelUrl || this.cancelledUrlTriggered === true) { + if (!orderPaymentAction.getCancelUrl() || rvvupMethodProperties.getIsCancellationTriggered() === true) { return; } - this.cancelledUrlTriggered = true; + rvvupMethodProperties.setIsCancellationTriggered(true); - this.setIframeUrl(this.cancelUrl); + this.setIframeUrl(orderPaymentAction.getCancelUrl()); }, + + /** + * Show the modal injecting the specified URL in the iframe's src attribute. + * @param {string} url + * @return {Boolean|void} + */ showModal: function (url) { if (!this.modal) { let options = { @@ -310,10 +424,11 @@ define([ this.modal.openModal(); }, + /** - * Set the iFrame's URL. + * Set the component's iFrame element src attribute URL. * - * @param url + * @param {string} url */ setIframeUrl: function (url) { let iframe = document.getElementById(this.getIframeId()) @@ -326,92 +441,71 @@ define([ return true; }, + + /** + * Get the component's modal element ID. + * + * @return {string} + */ getModalId: function () { return 'rvvup_modal-' + this.getCode() }, + + /** + * Get the component's iframe element ID. + * + * @return {string} + */ getIframeId: function () { return 'rvvup_iframe-' + this.getCode() }, + /** * Reset Rvvup data to default */ resetDefaultData: function () { - this.placedOrderId = null; - this.paymentToken = null; - this.redirectUrl = null; - this.captureUrl = null; - this.cancelUrl = null; - this.cancelledUrlTriggered = false; + rvvupMethodProperties.resetDefaultData(); + orderPaymentAction.resetDefaultData(); }, + /** - * API request to get Order Payment Actions for Rvvup Payments. + * After Place order actions. + * If PayPal payment, allow paypal buttons to handle logic. */ - getOrderPaymentActions: function () { - let self = this, - serviceUrl = customer.isLoggedIn() ? - urlBuilder.createUrl('/rvvup/payments/mine/:cartId/payment-actions', { - cartId: quote.getQuoteId() - }) : - urlBuilder.createUrl('/rvvup/payments/:cartId/payment-actions', { - cartId: quote.getQuoteId() - }); - - return storage.get( - serviceUrl, - true, - 'application/json' - ).done(function (data) { - /* First check get the authorization action & throw error if we don't. */ - let paymentAction = _.find(data, function(action) {return action.type === 'authorization'}); - - if (typeof paymentAction === 'undefined') { - errorProcessor.process('There was an error when placing the order!', self.messageContainer) - - return; - } - - /* Set cancelUrl from cancelAction method */ - let cancelAction = _.find(data, function(action) {return action.type === 'cancel'}); - - self.cancelUrl = typeof cancelAction !== 'undefined' && cancelAction.method === 'redirect_url' - ? cancelAction.value - : null; - - /* - * If we have a token authorization type method, then we should have a capture action. - */ - if (paymentAction.method === 'token') { - let captureAction = _.find(data, function(action) {return action.type === 'capture'}); - - self.captureUrl = typeof captureAction !== 'undefined' && captureAction.method === 'redirect_url' - ? captureAction.value - : null; - - self.paymentToken = paymentAction.value; - - return self.paymentToken; - } - - /* Otherwise, this should be standard redirect authorization, so show the modal */ - if (paymentAction.method === 'redirect_url') { - self.redirectUrl = paymentAction.value; + afterPlaceOrder: function () { + let self = this; + loader.startLoader(); - return self.redirectUrl; - } + if (self.shouldDisplayPayPalButton()) { + return; + } - throw 'Error placing order'; - }).fail(function(response) { - errorProcessor.process(response, self.messageContainer); - }); + $.when(getOrderPaymentActions(self.messageContainer)) + .done(function () { + if (orderPaymentAction.getRedirectUrl() !== null) { + self.showRvvupModal(orderPaymentAction.getRedirectUrl()); + } + }); }, + /** - * After Render for the paypal component's placeholder. - * It handles auto-loading the button after knockout.js has finished all processes. + * After Render for the PayPal component's placeholder. + * It handles autoloading of the button after knockout.js has finished all processes. */ - afterRenderPaypalComponentProcessor: function(target, viewModel) { - if (this.isPayPalComponent()) { + afterRenderPaypalComponentProcessor: function (target, viewModel) { + if (this.shouldDisplayPayPalButton()) { this.renderPayPalButton(); } + }, + getPayLaterTotal: function () { + return totals.totals().grand_total + }, + getPayLaterConfigValue: function (key) { + let values = rvvup_parameters + if (['enabled', 'textSize'].includes(key)) { + return values.settings.paypal.checkout.payLaterMessaging[key] + } + return values.settings.paypal.checkout.payLaterMessaging[key].value } }); } diff --git a/view/frontend/web/template/payment/rvvup.html b/view/frontend/web/template/payment/rvvup.html index 3a567795..5d600211 100644 --- a/view/frontend/web/template/payment/rvvup.html +++ b/view/frontend/web/template/payment/rvvup.html @@ -35,13 +35,32 @@ + +
+ -
+
-
+
+ +
+

+