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
+
+
+