diff --git a/Block/Adminhtml/System/Config/Form/Field/CBTAvailableCurrencies.php b/Block/Adminhtml/System/Config/Form/Field/CBTAvailableCurrencies.php index 5aa2ecf..fb50036 100644 --- a/Block/Adminhtml/System/Config/Form/Field/CBTAvailableCurrencies.php +++ b/Block/Adminhtml/System/Config/Form/Field/CBTAvailableCurrencies.php @@ -4,8 +4,47 @@ namespace Afterpay\Afterpay\Block\Adminhtml\System\Config\Form\Field; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\Serialize\SerializerInterface; +use Psr\Log\LoggerInterface; + class CBTAvailableCurrencies extends \Magento\Config\Block\System\Config\Form\Field { + private $serializer; + private $logger; + + public function __construct( + LoggerInterface $logger, + SerializerInterface $serializer, + Context $context, + array $data = [] + ) { + $this->serializer = $serializer; + $this->logger = $logger; + parent::__construct($context, $data); + } + + protected function _renderValue(\Magento\Framework\Data\Form\Element\AbstractElement $element) + { + try { + $CbtAvailableCurrencies = $this->serializer->unserialize($element->getValue()); + $newValue = ''; + if (!$CbtAvailableCurrencies) { + return parent::_renderValue($element); + } + + foreach ($CbtAvailableCurrencies as $currencyCode => $currency) { + $newValue .= $currencyCode . '(min:' . $currency['minimumAmount']['amount'] + . ',max:' . $currency['maximumAmount']['amount'] . ') '; + } + $element->setValue($newValue); + } catch (\Exception $e) { + $this->logger->critical($e); + } + + return parent::_renderValue($element); + } + public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) { /** @phpstan-ignore-next-line */ diff --git a/Controller/Express/PlaceOrder.php b/Controller/Express/PlaceOrder.php index 7574e03..cdf44f4 100644 --- a/Controller/Express/PlaceOrder.php +++ b/Controller/Express/PlaceOrder.php @@ -2,10 +2,20 @@ namespace Afterpay\Afterpay\Controller\Express; -class PlaceOrder implements \Magento\Framework\App\Action\HttpPostActionInterface -{ - const CANCELLED_STATUS = 'CANCELLED'; +use Afterpay\Afterpay\Controller\Payment\Capture; +use Afterpay\Afterpay\Gateway\Config\Config; +use Afterpay\Afterpay\Model\Payment\Capture\PlaceOrderProcessor; +use Magento\Checkout\Model\Session; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\Payment\Gateway\CommandInterface; +class PlaceOrder implements HttpPostActionInterface +{ private $request; private $messageManager; private $checkoutSession; @@ -15,13 +25,13 @@ class PlaceOrder implements \Magento\Framework\App\Action\HttpPostActionInterfac private $syncCheckoutDataCommand; public function __construct( - \Magento\Framework\App\RequestInterface $request, - \Magento\Framework\Message\ManagerInterface $messageManager, - \Magento\Checkout\Model\Session $checkoutSession, - \Magento\Framework\Controller\Result\JsonFactory $jsonFactory, - \Magento\Framework\UrlInterface $url, - \Afterpay\Afterpay\Model\Payment\Capture\PlaceOrderProcessor $placeOrderProcessor, - \Magento\Payment\Gateway\CommandInterface $syncCheckoutDataCommand + RequestInterface $request, + ManagerInterface $messageManager, + Session $checkoutSession, + JsonFactory $jsonFactory, + UrlInterface $url, + PlaceOrderProcessor $placeOrderProcessor, + CommandInterface $syncCheckoutDataCommand ) { $this->request = $request; $this->messageManager = $messageManager; @@ -40,25 +50,29 @@ public function execute() $afterpayOrderToken = $this->request->getParam('orderToken'); $status = $this->request->getParam('status'); - if ($status === static::CANCELLED_STATUS) { + if ($status === Capture::CHECKOUT_STATUS_CANCELLED) { return $jsonResult; } + if ($status !== Capture::CHECKOUT_STATUS_SUCCESS) { + $errorMessage = (string)__('Afterpay payment is declined. Please select an alternative payment method.'); + $this->messageManager->addErrorMessage($errorMessage); + + return $jsonResult->setData(['redirectUrl' => $this->url->getUrl('checkout/cart')]); + } + try { $quote->getPayment() - ->setMethod(\Afterpay\Afterpay\Gateway\Config\Config::CODE) + ->setMethod(Config::CODE) ->setAdditionalInformation('afterpay_express', true); $this->placeOrderProcessor->execute($quote, $this->syncCheckoutDataCommand, $afterpayOrderToken); } catch (\Throwable $e) { - $errorMessage = $e instanceof \Magento\Framework\Exception\LocalizedException + $errorMessage = $e instanceof LocalizedException ? $e->getMessage() - : (string)__('Afterpay payment declined. Please select an alternative payment method.'); + : (string)__('Afterpay payment is declined. Please select an alternative payment method.'); + $this->messageManager->addErrorMessage($errorMessage); - return $jsonResult->setData(['error' => $errorMessage, 'redirectUrl' => $this->url->getUrl( - 'checkout/cart', - ['_scope' => $quote->getStore()] - )] - ); + return $jsonResult->setData(['redirectUrl' => $this->url->getUrl('checkout/cart')]); } return $jsonResult->setData(['redirectUrl' => $this->url->getUrl('checkout/onepage/success')]); diff --git a/Gateway/Request/Checkout/CheckoutDataBuilder.php b/Gateway/Request/Checkout/CheckoutDataBuilder.php index dc2ff6a..2a9d211 100644 --- a/Gateway/Request/Checkout/CheckoutDataBuilder.php +++ b/Gateway/Request/Checkout/CheckoutDataBuilder.php @@ -101,18 +101,26 @@ protected function getItems(\Magento\Quote\Model\Quote $quote): array foreach ($quoteItems as $item) { $productId = $item->getProduct()->getId(); + $amount = $isCBTCurrencyAvailable ? $item->getPriceInclTax() : $item->getBasePriceInclTax(); + $currencyCode = $isCBTCurrencyAvailable ? $quote->getQuoteCurrencyCode() : $quote->getBaseCurrencyCode(); + $qty = $item->getQty(); + $isIntQty = floor($qty) == $qty; + if ($isIntQty) { + $qty = (int)$item->getQty(); + } else { + $amount *= $item->getQty(); + $qty = 1; + } $formattedItem = [ 'name' => $item->getName(), 'sku' => $item->getSku(), - 'quantity' => $item->getQty(), + 'quantity' => $qty, 'pageUrl' => $item->getProduct()->getProductUrl(), 'categories' => [array_values($this->getQuoteItemCategoriesNames($item))], 'price' => [ - 'amount' => $this->formatPrice( - $isCBTCurrencyAvailable ? $item->getPriceInclTax() : $item->getBasePriceInclTax() - ), - 'currency' => $isCBTCurrencyAvailable ? $quote->getQuoteCurrencyCode() : $quote->getBaseCurrencyCode() + 'amount' => $this->formatPrice($amount), + 'currency' => $currencyCode ] ]; @@ -132,10 +140,7 @@ protected function getQuoteItemCategoriesNames(\Magento\Quote\Model\Quote\Item $ /** @var \Magento\Catalog\Model\ResourceModel\AbstractCollection $categoryCollection */ $categoryCollection = $item->getProduct()->getCategoryCollection(); $itemCategories = $categoryCollection->addAttributeToSelect('name')->getItems(); - return array_map( - function ($cat) {return $cat->getData('name');}, - $itemCategories - ); + return array_map(static function ($cat) {return $cat->getData('name');}, $itemCategories); } /** @@ -146,11 +151,8 @@ protected function getItemsImages(array $items): array { $itemsImages = []; $searchCriteria = $this->searchCriteriaBuilder - ->addFilter( - 'entity_id', - array_map(function ($item) {return $item->getProduct()->getId();}, $items), - 'in' - )->create(); + ->addFilter('entity_id', array_map(static function ($item) {return $item->getProduct()->getId();}, $items), 'in') + ->create(); $products = $this->productRepository->getList($searchCriteria)->getItems(); foreach ($products as $product) { @@ -184,13 +186,14 @@ protected function getShippingAmount(\Magento\Quote\Model\Quote $quote): ?array return null; } $isCBTCurrencyAvailable = $this->checkCBTCurrencyAvailability->checkByQuote($quote); + $amount = $isCBTCurrencyAvailable + ? $quote->getShippingAddress()->getShippingAmount() + : $quote->getShippingAddress()->getBaseShippingAmount(); + $currencyCode = $isCBTCurrencyAvailable ? $quote->getQuoteCurrencyCode() : $quote->getBaseCurrencyCode(); return [ - 'amount' => $this->formatPrice($isCBTCurrencyAvailable - ? $quote->getShippingAddress()->getShippingAmount() - : $quote->getShippingAddress()->getBaseShippingAmount() - ), - 'currency' => $isCBTCurrencyAvailable ? $quote->getQuoteCurrencyCode() : $quote->getBaseCurrencyCode() + 'amount' => $this->formatPrice($amount), + 'currency' => $currencyCode ]; } @@ -211,13 +214,16 @@ protected function getDiscounts(\Magento\Quote\Model\Quote $quote): ?array return null; } $isCBTCurrencyAvailable = $this->checkCBTCurrencyAvailability->checkByQuote($quote); + $amount = $isCBTCurrencyAvailable + ? $quote->getDiscountAmount() + : $quote->getBaseDiscountAmount(); + $currencyCode = $isCBTCurrencyAvailable ? $quote->getQuoteCurrencyCode() : $quote->getBaseCurrencyCode(); + return [ 'displayName' => __('Discount'), 'amount' => [ - 'amount' => $this->formatPrice($isCBTCurrencyAvailable - ? $quote->getDiscountAmount() - : $quote->getBaseDiscountAmount()), - 'currency' => $isCBTCurrencyAvailable ? $quote->getQuoteCurrencyCode() : $quote->getBaseCurrencyCode() + 'amount' => $this->formatPrice($amount), + 'currency' => $currencyCode ] ]; } diff --git a/Gateway/Response/Checkout/CheckoutItemsAmountValidationHandler.php b/Gateway/Response/Checkout/CheckoutItemsAmountValidationHandler.php index 6196156..30792d8 100644 --- a/Gateway/Response/Checkout/CheckoutItemsAmountValidationHandler.php +++ b/Gateway/Response/Checkout/CheckoutItemsAmountValidationHandler.php @@ -35,6 +35,14 @@ public function handle(array $handlingSubject, array $response) throw new \Magento\Framework\Exception\LocalizedException($invalidCartItemsExceptionMessage); } if ($item->getQty() != $responseItems[$itemIndex]['quantity']) { + $qty = $item->getQty(); + $isIntQty = floor($qty) == $qty; + if (!$isIntQty && $responseItems[$itemIndex]['quantity'] == 1) { + $amount = $isCBTCurrency ? $item->getPriceInclTax() : $item->getBasePriceInclTax(); + if ($amount * $qty == $responseItems[$itemIndex]['price']['amount']) { + continue; + } + } throw new \Magento\Framework\Exception\LocalizedException($invalidCartItemsExceptionMessage); } } diff --git a/Gateway/Response/MerchantConfiguration/CBTAvailableCurrenciesConfigurationHandler.php b/Gateway/Response/MerchantConfiguration/CBTAvailableCurrenciesConfigurationHandler.php index 617ac6d..e27148e 100644 --- a/Gateway/Response/MerchantConfiguration/CBTAvailableCurrenciesConfigurationHandler.php +++ b/Gateway/Response/MerchantConfiguration/CBTAvailableCurrenciesConfigurationHandler.php @@ -2,30 +2,35 @@ namespace Afterpay\Afterpay\Gateway\Response\MerchantConfiguration; -class CBTAvailableCurrenciesConfigurationHandler implements \Magento\Payment\Gateway\Response\HandlerInterface +use Afterpay\Afterpay\Model\Config; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Framework\Serialize\SerializerInterface; + +class CBTAvailableCurrenciesConfigurationHandler implements HandlerInterface { private $config; + private $serializer; public function __construct( - \Afterpay\Afterpay\Model\Config $config + Config $config, + SerializerInterface $serializer ) { $this->config = $config; + $this->serializer = $serializer; } public function handle(array $handlingSubject, array $response): void { $websiteId = (int)$handlingSubject['websiteId']; - $cbtAvailableCurrencies = []; + $cbtAvailableCurrencies = ''; + if (isset($response['CBT']['enabled']) && isset($response['CBT']['limits']) && is_array($response['CBT']['limits']) ) { - foreach ($response['CBT']['limits'] as $limit) { - if (isset($limit['maximumAmount']['currency']) && isset($limit['maximumAmount']['amount'])) { - $cbtAvailableCurrencies[] = $limit['maximumAmount']['currency'] . ':' . $limit['maximumAmount']['amount']; - } - } + $cbtAvailableCurrencies = $this->serializer->serialize($response['CBT']['limits']); } - $this->config->setCbtCurrencyLimits(implode(",", $cbtAvailableCurrencies), $websiteId); + + $this->config->setCbtCurrencyLimits($cbtAvailableCurrencies, $websiteId); } } diff --git a/Gateway/Validator/Method/CurrencyValidator.php b/Gateway/Validator/Method/CurrencyValidator.php index 3a635db..7d3cd98 100644 --- a/Gateway/Validator/Method/CurrencyValidator.php +++ b/Gateway/Validator/Method/CurrencyValidator.php @@ -20,7 +20,7 @@ public function __construct( public function validate(array $validationSubject): \Magento\Payment\Gateway\Validator\ResultInterface { $quote = $this->checkoutSession->getQuote(); - $currentCurrency = $quote->getQuoteCurrencyCode(); + $currentCurrency = $quote->getStore()->getCurrentCurrencyCode(); $allowedCurrencies = $this->config->getAllowedCurrencies(); $cbtCurrencies = array_keys($this->config->getCbtCurrencyLimits()); diff --git a/Model/Checks/IsCBTAvailable.php b/Model/Checks/IsCBTAvailable.php index 942ab13..2b99b7f 100644 --- a/Model/Checks/IsCBTAvailable.php +++ b/Model/Checks/IsCBTAvailable.php @@ -30,7 +30,7 @@ public function checkByQuote(\Magento\Quote\Model\Quote $quote): bool return $this->canUseCurrentCurrency; } - $currentCurrencyCode = $quote->getQuoteCurrencyCode(); + $currentCurrencyCode = $quote->getQuoteCurrencyCode() ?? $quote->getStore()->getCurrentCurrencyCode(); $amount = (float) $quote->getGrandTotal(); $this->canUseCurrentCurrency = $this->check($currentCurrencyCode, $amount); diff --git a/Model/Config.php b/Model/Config.php index 1e16326..22be487 100644 --- a/Model/Config.php +++ b/Model/Config.php @@ -1,10 +1,12 @@ -scopeConfig = $scopeConfig; $this->writer = $writer; $this->resourceConnection = $resourceConnection; + $this->serializer = $serializer; } public function getIsPaymentActive(?int $scopeCode = null): bool @@ -172,7 +178,6 @@ public function getMinOrderTotal(?int $scopeCode = null): ?string public function getCbtCurrencyLimits(?int $scopeCode = null): array { - $data = []; $value = $this->scopeConfig->getValue( self::XML_PATH_CBT_CURRENCY_LIMITS, ScopeInterface::SCOPE_WEBSITE, @@ -183,15 +188,7 @@ public function getCbtCurrencyLimits(?int $scopeCode = null): array return []; } - $list = explode(',', $value); - foreach ($list as $item) { - $currencyLimit = explode(':', $item); - if (isset($currencyLimit[0]) && isset($currencyLimit[1])) { - $data[$currencyLimit[0]] = (float) $currencyLimit[1]; - } - } - - return $data; + return $this->serializer->unserialize($value); } public function getExcludeCategories(?int $scopeCode = null): array @@ -205,6 +202,15 @@ public function getExcludeCategories(?int $scopeCode = null): array return $excludeCategories ? explode(',', $excludeCategories) : []; } + public function getIsReversalEnabled(?int $scopeCode = null): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ENABLE_REVERSAL, + ScopeInterface::SCOPE_WEBSITE, + $scopeCode + ); + } + public function setMaxOrderTotal(string $value, int $scopeId = 0): self { if ($scopeId) { @@ -347,7 +353,7 @@ public function setSpecificCountries(string $value, int $scopeId = 0): self public function getMerchantCountry( string $scope = ScopeInterface::SCOPE_WEBSITES, - ?int $scopeCode = null + ?int $scopeCode = null ): ?string { if ($countryCode = $this->scopeConfig->getValue( self::XML_PATH_PAYPAL_MERCHANT_COUNTRY, @@ -368,7 +374,7 @@ public function getMerchantCountry( public function getMerchantCurrency( string $scope = ScopeInterface::SCOPE_WEBSITES, - ?int $scopeCode = null + ?int $scopeCode = null ): ?string { return $this->scopeConfig->getValue( \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, @@ -401,7 +407,7 @@ public function websiteHasOwnConfig(int $websiteId): bool \Afterpay\Afterpay\Observer\Adminhtml\ConfigSaveAfter::AFTERPAY_CONFIGS, \Afterpay\Afterpay\Observer\Adminhtml\ConfigSaveAfter::CONFIGS_PATHS_TO_TRACK ); - $selectQuery = $connection->select()->from($coreConfigData, ['path','value']) + $selectQuery = $connection->select()->from($coreConfigData, ['path', 'value']) ->where("scope = ?", \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES) ->where("scope_id = ?", $websiteId) ->where("path in (?)", $configsExistToCheck); diff --git a/Model/Config/CategorySourceRegistry.php b/Model/Config/CategorySourceRegistry.php new file mode 100644 index 0000000..3d10fda --- /dev/null +++ b/Model/Config/CategorySourceRegistry.php @@ -0,0 +1,18 @@ +showAllCategories; + } + + public function setShowAllCategories(bool $value): void + { + $this->showAllCategories = $value; + } +} diff --git a/Model/Config/Source/Category.php b/Model/Config/Source/Category.php index c5be0cb..7ab914b 100644 --- a/Model/Config/Source/Category.php +++ b/Model/Config/Source/Category.php @@ -7,15 +7,18 @@ class Category implements \Magento\Framework\Data\OptionSourceInterface private $storeManager; private $request; private $categoryHelper; + private $categorySourceRegistry; public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\RequestInterface $request, - \Magento\Catalog\Helper\Category $categoryHelper + \Magento\Catalog\Helper\Category $categoryHelper, + \Afterpay\Afterpay\Model\Config\CategorySourceRegistry $categorySourceRegistry ) { $this->storeManager = $storeManager; $this->request = $request; $this->categoryHelper = $categoryHelper; + $this->categorySourceRegistry = $categorySourceRegistry; } public function toOptionArray(): array @@ -45,8 +48,10 @@ private function getCategoriesTree(): array $currentStoreId = $this->storeManager->getStore()->getId(); $this->storeManager->setCurrentStore($this->getStoreIdByRequest() ?? $currentStoreId); + $this->categorySourceRegistry->setShowAllCategories(true); /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categories */ $categories = $this->categoryHelper->getStoreCategories(false, true); + $this->categorySourceRegistry->setShowAllCategories(false); $this->storeManager->setCurrentStore($currentStoreId); return $this->convertToTree($categories); diff --git a/Model/Order/Payment/Auth/TokenValidator.php b/Model/Order/Payment/Auth/TokenValidator.php new file mode 100644 index 0000000..cc51b0b --- /dev/null +++ b/Model/Order/Payment/Auth/TokenValidator.php @@ -0,0 +1,30 @@ +resourceConnection = $resourceConnection; + } + + public function checkIsUsed(string $token): bool + { + $salesOrderPaymentTable = $this->resourceConnection->getConnection()->getTableName('sales_order_payment'); + $checkSelect = $this->resourceConnection->getConnection()->select() + ->from($salesOrderPaymentTable) + ->where('method = ?', Config::CODE) + ->where('base_amount_paid_online IS NOT NULL') + ->where('last_trans_id IS NOT NULL') + ->where('additional_information like ?', '%"' . CheckoutInterface::AFTERPAY_TOKEN . '":"' . $token . '%'); + + return (bool)$this->resourceConnection->getConnection()->fetchOne($checkSelect); + } +} diff --git a/Model/Payment/Capture/CancelOrderProcessor.php b/Model/Payment/Capture/CancelOrderProcessor.php index ec07f18..9e6ca38 100644 --- a/Model/Payment/Capture/CancelOrderProcessor.php +++ b/Model/Payment/Capture/CancelOrderProcessor.php @@ -18,8 +18,7 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Afterpay\Afterpay\Model\Config $config, \Afterpay\Afterpay\Model\Order\Payment\QuotePaidStorage $quotePaidStorage - ) - { + ) { $this->paymentDataObjectFactory = $paymentDataObjectFactory; $this->reversalCommand = $reversalCommand; $this->voidCommand = $voidCommand; @@ -28,18 +27,23 @@ public function __construct( $this->quotePaidStorage = $quotePaidStorage; } - /** - * @throws \Magento\Payment\Gateway\Command\CommandException - * @throws \Magento\Framework\Exception\LocalizedException - */ public function execute(\Magento\Quote\Model\Quote\Payment $payment, int $quoteId): void { + if (!$this->config->getIsReversalEnabled()) { + return; + } + $commandSubject = ['payment' => $this->paymentDataObjectFactory->create($payment)]; if (!$this->isDeferredPaymentFlow()) { $this->reversalCommand->execute($commandSubject); - return; + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'There was a problem placing your order. Your Afterpay order %1 is refunded.', + $payment->getAdditionalInformation(\Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_ORDER_ID) + ) + ); } $afterpayPayment = $this->quotePaidStorage->getAfterpayPaymentIfQuoteIsPaid($quoteId); diff --git a/Model/Payment/Capture/PlaceOrderProcessor.php b/Model/Payment/Capture/PlaceOrderProcessor.php index 81d51ae..49f6fc5 100644 --- a/Model/Payment/Capture/PlaceOrderProcessor.php +++ b/Model/Payment/Capture/PlaceOrderProcessor.php @@ -2,72 +2,62 @@ namespace Afterpay\Afterpay\Model\Payment\Capture; -use Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface; +use Afterpay\Afterpay\Api\Data\CheckoutInterface; +use Afterpay\Afterpay\Model\CBT\CheckCBTCurrencyAvailabilityInterface; +use Afterpay\Afterpay\Model\Order\Payment\Auth\TokenValidator; +use Afterpay\Afterpay\Model\Payment\PaymentErrorProcessor; +use Magento\Customer\Api\Data\GroupInterface; use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectFactoryInterface; +use Magento\Quote\Api\CartManagementInterface; use Magento\Quote\Model\Quote; class PlaceOrderProcessor { private $cartManagement; - private $cancelOrderProcessor; private $paymentDataObjectFactory; private $checkCBTCurrencyAvailability; - private $logger; + private $tokenValidator; + private $paymentErrorProcessor; public function __construct( - \Magento\Quote\Api\CartManagementInterface $cartManagement, - \Afterpay\Afterpay\Model\Payment\Capture\CancelOrderProcessor $cancelOrderProcessor, - \Magento\Payment\Gateway\Data\PaymentDataObjectFactoryInterface $paymentDataObjectFactory, - \Afterpay\Afterpay\Model\CBT\CheckCBTCurrencyAvailabilityInterface $checkCBTCurrencyAvailability, - \Psr\Log\LoggerInterface $logger - ) - { + CartManagementInterface $cartManagement, + PaymentDataObjectFactoryInterface $paymentDataObjectFactory, + CheckCBTCurrencyAvailabilityInterface $checkCBTCurrencyAvailability, + TokenValidator $tokenValidator, + PaymentErrorProcessor $paymentErrorProcessor + ) { $this->cartManagement = $cartManagement; - $this->cancelOrderProcessor = $cancelOrderProcessor; $this->paymentDataObjectFactory = $paymentDataObjectFactory; $this->checkCBTCurrencyAvailability = $checkCBTCurrencyAvailability; - $this->logger = $logger; + $this->tokenValidator = $tokenValidator; + $this->paymentErrorProcessor = $paymentErrorProcessor; } public function execute(Quote $quote, CommandInterface $checkoutDataCommand, string $afterpayOrderToken): void { + if ($this->tokenValidator->checkIsUsed($afterpayOrderToken)) { + return; + } + + $payment = $quote->getPayment(); try { - $payment = $quote->getPayment(); - $payment->setAdditionalInformation( - \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_TOKEN, - $afterpayOrderToken - ); + $payment->setAdditionalInformation(CheckoutInterface::AFTERPAY_TOKEN, $afterpayOrderToken); $isCBTCurrencyAvailable = $this->checkCBTCurrencyAvailability->checkByQuote($quote); - $payment->setAdditionalInformation( - \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY, - $isCBTCurrencyAvailable - ); - $payment->setAdditionalInformation( - \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_CBT_CURRENCY, - $quote->getQuoteCurrencyCode() - ); + $payment->setAdditionalInformation(CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY, $isCBTCurrencyAvailable); + $payment->setAdditionalInformation(CheckoutInterface::AFTERPAY_CBT_CURRENCY, $quote->getQuoteCurrencyCode()); if (!$quote->getCustomerId()) { $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()) ->setCustomerIsGuest(true) - ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); + ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID); } $checkoutDataCommand->execute(['payment' => $this->paymentDataObjectFactory->create($payment)]); - $this->cartManagement->placeOrder($quote->getId()); } catch (\Throwable $e) { - $this->logger->critical('Order placement is failed with error: ' . $e->getMessage()); - $quoteId = (int)$quote->getId(); - $this->cancelOrderProcessor->execute($payment, $quoteId); - - throw new \Magento\Framework\Exception\LocalizedException( - __( - '%1 payment declined. Please select an alternative payment method.', - $quote->getPayment()->getMethodInstance()->getTitle() - ) - ); + $this->paymentErrorProcessor->execute($quote, $e, $payment); } } } diff --git a/Model/Payment/PaymentErrorProcessor.php b/Model/Payment/PaymentErrorProcessor.php new file mode 100644 index 0000000..4feac5b --- /dev/null +++ b/Model/Payment/PaymentErrorProcessor.php @@ -0,0 +1,58 @@ +checkoutSession = $checkoutSession; + $this->orderRepository = $orderRepository; + $this->cancelOrderProcessor = $cancelOrderProcessor; + $this->logger = $logger; + } + + public function execute(Quote $quote, \Throwable $e, Payment $payment) + { + $this->logger->critical('Order placement is failed with error: ' . PHP_EOL . $e); + if (($this->checkoutSession->getLastSuccessQuoteId() == $quote->getId()) && $this->checkoutSession->getLastOrderId()) { + try { + $order = $this->orderRepository->get((int)$this->checkoutSession->getLastOrderId()); + $order->addCommentToStatusHistory( + 'Afterpay detected a Magento exception during order creation. Please review:' . $e->getMessage(), + self::ORDER_STATUS_CODE, + false + ); + $this->orderRepository->save($order); + + return $order->getEntityId(); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } + } + + $this->cancelOrderProcessor->execute($payment, (int)$quote->getId()); + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'There was a problem placing your order. Please make sure your Afterpay %1 order has been refunded.', + $payment->getAdditionalInformation(\Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_ORDER_ID) + ) + ); + } +} diff --git a/Plugin/Catalog/Model/ResourceModel/Category/Tree.php b/Plugin/Catalog/Model/ResourceModel/Category/Tree.php new file mode 100644 index 0000000..561464a --- /dev/null +++ b/Plugin/Catalog/Model/ResourceModel/Category/Tree.php @@ -0,0 +1,30 @@ +categorySourceRegistry = $categorySourceRegistry; + } + + public function beforeAddCollectionData( + \Magento\Catalog\Model\ResourceModel\Category\Tree $subject, + $collection = null, + $sorted = false, + $exclude = [], + $toLoad = true, + $onlyActive = false + ): array { + return [ + $collection, + $sorted, + $exclude, + $toLoad, + $this->categorySourceRegistry->getShowAllCategories() ? false : $onlyActive + ]; + } +} diff --git a/Plugin/Order/Payment/State/CaptureCommand.php b/Plugin/Order/Payment/State/CaptureCommand.php new file mode 100644 index 0000000..c52b6f1 --- /dev/null +++ b/Plugin/Order/Payment/State/CaptureCommand.php @@ -0,0 +1,68 @@ +statusResolver = $statusResolver; + $this->config = $config; + } + + public function aroundExecute( + \Magento\Sales\Model\Order\Payment\State\CaptureCommand $subject, + callable $proceed, + \Magento\Sales\Api\Data\OrderPaymentInterface $payment, + $amount, + \Magento\Sales\Api\Data\OrderInterface $order + ): \Magento\Framework\Phrase { + if ($payment->getMethod() === \Afterpay\Afterpay\Gateway\Config\Config::CODE) { + $state = Order::STATE_PROCESSING; + $status = null; + $message = $this->config->getPaymentFlow() == \Afterpay\Afterpay\Model\Config\Source\PaymentFlow::DEFERRED ? + 'Authorized and open to capture amount of %1 online.' : + 'Captured amount of %1 online.'; + + if ($payment->getIsTransactionPending()) { + $state = Order::STATE_PAYMENT_REVIEW; + $message = 'An amount of %1 will be captured after being approved at the payment gateway.'; + } + + if ($payment->getIsFraudDetected()) { + $state = Order::STATE_PAYMENT_REVIEW; + $status = Order::STATUS_FRAUD; + $message .= ' Order is suspended as its capturing amount %1 is suspected to be fraudulent.'; + } + + if (!isset($status)) { + $status = $this->statusResolver->getOrderStatusByState($order, $state); + } + + $order->setState($state); + $order->setStatus($status); + + return __($message, $order->getBaseCurrency()->formatTxt($amount)); + } + + return $proceed($payment, $amount, $order); + } +} diff --git a/Setup/Patch/Data/AfterpayPendingExceptionReviewOrderStatus.php b/Setup/Patch/Data/AfterpayPendingExceptionReviewOrderStatus.php new file mode 100644 index 0000000..7f8246a --- /dev/null +++ b/Setup/Patch/Data/AfterpayPendingExceptionReviewOrderStatus.php @@ -0,0 +1,49 @@ +statusFactory = $statusFactory; + $this->statusResource = $statusResource; + } + + public function getAliases(): array + { + return []; + } + + public static function getDependencies(): array + { + return []; + } + + public function apply(): self + { + $status = $this->statusFactory->create(); + $status->setData([ + 'status' => PaymentErrorProcessor::ORDER_STATUS_CODE, + 'label' => 'Pending Exception Review (Afterpay)', + ]); + + try { + $this->statusResource->save($status); + } catch (AlreadyExistsException $exception) { + return $this; + } + + $status->assignState('processing', false, false); + + return $this; + } +} diff --git a/Setup/Patch/Data/UpdateCbtInfoPatch.php b/Setup/Patch/Data/UpdateCbtInfoPatch.php new file mode 100644 index 0000000..b979315 --- /dev/null +++ b/Setup/Patch/Data/UpdateCbtInfoPatch.php @@ -0,0 +1,56 @@ +merchantConfigurationCommand = $merchantConfigurationCommand; + $this->storeManager = $storeManager; + $this->typeList = $typeList; + $this->logger = $logger; + } + + public function getAliases(): array + { + return []; + } + + public static function getDependencies(): array + { + return []; + } + + public function apply() + { + $websites = $this->storeManager->getWebsites(true); + foreach ($websites as $website) { + $websiteId = (int)$website->getId(); + try { + $this->merchantConfigurationCommand->execute([ + 'websiteId' => $websiteId + ]); + $this->typeList->cleanType(\Magento\PageCache\Model\Cache\Type::TYPE_IDENTIFIER); + } catch (\Exception $e) { + $this->logger->critical($e); + } + } + + return $this; + } +} diff --git a/ViewModel/Container/Container.php b/ViewModel/Container/Container.php index 681eead..84369de 100644 --- a/ViewModel/Container/Container.php +++ b/ViewModel/Container/Container.php @@ -84,9 +84,15 @@ protected function updateContainer(array $jsLayout, bool $remove, string $contai private function isCurrentCurrencyAvailable(): bool { - $currentCurrencyCode = $this->storeManager->getStore()->getCurrentCurrency(); + $currentCurrencyCode = $this->storeManager->getStore()->getCurrentCurrencyCode(); + $baseCurrencyCode = $this->storeManager->getStore()->getBaseCurrencyCode(); $allowedCurrencies = $this->config->getAllowedCurrencies(); + $validCurrencies = array_keys($this->config->getCbtCurrencyLimits()); - return in_array($currentCurrencyCode->getCode(), $allowedCurrencies); + if (in_array($baseCurrencyCode, $allowedCurrencies)) { + $validCurrencies[] = $baseCurrencyCode; + } + + return in_array($currentCurrencyCode, $validCurrencies); } } diff --git a/ViewModel/Container/Cta/Lib.php b/ViewModel/Container/Cta/Lib.php index 8556b8a..9cdf23d 100644 --- a/ViewModel/Container/Cta/Lib.php +++ b/ViewModel/Container/Cta/Lib.php @@ -4,15 +4,43 @@ namespace Afterpay\Afterpay\ViewModel\Container\Cta; +use Afterpay\Afterpay\Model\Config; +use Afterpay\Afterpay\Model\Url\Lib\LibUrlProvider; +use Magento\Store\Model\StoreManagerInterface; + class Lib extends \Afterpay\Afterpay\ViewModel\Container\Lib { + private $storeManager; + + public function __construct( + StoreManagerInterface $storeManager, + Config $config, + LibUrlProvider $libUrlProvider, + ?string $containerConfigPath = null + ) { + $this->storeManager = $storeManager; + parent::__construct($config, $libUrlProvider, $containerConfigPath); + } + public function getMinTotalValue(): ?string { + $currencyCode = $this->storeManager->getStore()->getCurrentCurrencyCode(); + $cbtLimits = $this->config->getCbtCurrencyLimits(); + if (isset($cbtLimits[$currencyCode])) { + return $cbtLimits[$currencyCode]['minimumAmount']['amount']; + } + return $this->config->getMinOrderTotal(); } public function getMaxTotalValue(): ?string { + $currencyCode = $this->storeManager->getStore()->getCurrentCurrencyCode(); + $cbtLimits = $this->config->getCbtCurrencyLimits(); + if (isset($cbtLimits[$currencyCode])) { + return $cbtLimits[$currencyCode]['maximumAmount']['amount']; + } + return $this->config->getMaxOrderTotal(); } } diff --git a/composer.json b/composer.json index 99666df..2c0376b 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "license": "Apache-2.0", "type": "magento2-module", "description": "Magento 2 Afterpay Payment Module", - "version": "4.0.5", + "version": "4.1.0", "require": { "php": "~7.1.3||~7.2.0||~7.3.0||~7.4.0", "magento/framework": "^102.0", diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index 6f97991..202f703 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -30,4 +30,7 @@ + + + diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 751690a..55a070a 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -94,7 +94,13 @@ Afterpay\Afterpay\Model\Config\Source\PaymentFlow payment/afterpay/payment_flow - here]]> + here]]> + + + + Magento\Config\Model\Config\Source\Yesno + payment/afterpay/enable_reversal + diff --git a/etc/di.xml b/etc/di.xml index 6992391..cf54aa7 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -350,6 +350,11 @@ Afterpay\Afterpay\Gateway\Command\GetMerchantConfigurationCommandWrapper + + + Afterpay\Afterpay\Gateway\Command\GetMerchantConfigurationCommandWrapper + + Afterpay\Afterpay\Gateway\Command\GetMerchantConfigurationCommandWrapper @@ -369,4 +374,7 @@ + + + diff --git a/etc/module.xml b/etc/module.xml index 673402a..d5c2cf1 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,7 +1,7 @@ - + diff --git a/view/frontend/web/js/view/container/express-checkout/product/button.js b/view/frontend/web/js/view/container/express-checkout/product/button.js index c172bb5..c75a1fa 100644 --- a/view/frontend/web/js/view/container/express-checkout/product/button.js +++ b/view/frontend/web/js/view/container/express-checkout/product/button.js @@ -38,10 +38,13 @@ define([ return this._super(); }, _getOnCommenceCheckoutAfterpayMethod: function () { + let isBundle = $('#product_addtocart_form').find('#bundleSummary').length; const parentOnCommenceCheckoutAfterpayMethod = this._super(); - return (actions) => { - const productSubmitForm = $('#product_addtocart_form'); - productSubmitForm.submit(); + return (actions) => { + if (!isBundle) { + const productSubmitForm = $('#product_addtocart_form'); + productSubmitForm.submit(); + } this.onCartUpdated = $.Deferred(); this.onCartUpdated.done(() => parentOnCommenceCheckoutAfterpayMethod(actions)) .fail(() => this._fail(actions, AfterPay.constants.SERVICE_UNAVAILABLE));