├── .gitignore ├── Docs └── img │ ├── rest-sequence-diagram.png │ └── graphql-sequence-diagram.png ├── view ├── frontend │ ├── web │ │ ├── template │ │ │ ├── payment │ │ │ │ └── info │ │ │ │ │ ├── checkout-note.html │ │ │ │ │ └── terms-and-conditions.html │ │ │ ├── express-checkout │ │ │ │ ├── button.html │ │ │ │ └── button-minicart.html │ │ │ └── cta │ │ │ │ ├── pdp │ │ │ │ └── cta.html │ │ │ │ └── cta.html │ │ ├── js │ │ │ ├── view │ │ │ │ ├── payment │ │ │ │ │ ├── payments.js │ │ │ │ │ └── info │ │ │ │ │ │ └── terms-and-conditions.js │ │ │ │ └── container │ │ │ │ │ ├── express-checkout │ │ │ │ │ └── check-message.js │ │ │ │ │ ├── cta │ │ │ │ │ ├── cta.js │ │ │ │ │ └── pdp │ │ │ │ │ │ └── cta.js │ │ │ │ │ └── container.js │ │ │ ├── service │ │ │ │ └── container │ │ │ │ │ ├── cta │ │ │ │ │ └── modal-options-updater.js │ │ │ │ │ ├── cart │ │ │ │ │ ├── mini-cart-data-retriever.js │ │ │ │ │ ├── cart-data-retriever.js │ │ │ │ │ └── abstract-data-retriever.js │ │ │ │ │ ├── configurable-mixin.js │ │ │ │ │ └── pricebox-widget-mixin.js │ │ │ ├── action │ │ │ │ └── create-afterpay-checkout.js │ │ │ └── model │ │ │ │ └── container │ │ │ │ └── container-model-holder.js │ │ └── css │ │ │ ├── afterpay-checkout.less │ │ │ └── afterpay-express-checkout.less │ ├── layout │ │ ├── checkout_onepage_success.xml │ │ ├── catalog_product_view_type_bundle.xml │ │ └── catalog_product_view_type_grouped.xml │ ├── templates │ │ ├── express-checkout │ │ │ ├── lib.phtml │ │ │ ├── button.phtml │ │ │ ├── minicart-headless.phtml │ │ │ ├── cart-headless.phtml │ │ │ └── headless.phtml │ │ └── cta │ │ │ ├── lib.phtml │ │ │ ├── container.phtml │ │ │ ├── minicart │ │ │ └── headless.phtml │ │ │ ├── pdp │ │ │ └── headless.phtml │ │ │ └── cart │ │ │ └── headless.phtml │ └── requirejs-config.js └── adminhtml │ ├── web │ └── js │ │ └── limits.js │ └── templates │ └── system │ └── config │ └── button │ └── update.phtml ├── registration.php ├── Model ├── Log │ ├── Handler │ │ └── Debug.php │ └── Method │ │ └── Logger.php ├── CBT │ └── CheckCBTCurrencyAvailabilityInterface.php ├── Spi │ ├── CheckoutValidatorInterface.php │ ├── SourceValidatorServiceInterface.php │ └── StockItemsValidatorInterface.php ├── Checks │ ├── PaymentMethod.php │ ├── PaymentMethodInterface.php │ ├── CanUseForQuoteItems.php │ └── IsCBTAvailable.php ├── PaymentStateInterface.php ├── Order │ ├── OrderItemInterface.php │ ├── Payment │ │ ├── QuotePaidStorage.php │ │ └── Auth │ │ │ ├── TokenValidator.php │ │ │ ├── TokenSaver.php │ │ │ └── ExpiryDate.php │ ├── OrderItem.php │ └── CreditMemo │ │ ├── StatusChanger.php │ │ ├── OrderUpdater.php │ │ ├── CaptureProcessor.php │ │ ├── PaymentUpdater.php │ │ └── CreditMemoInitiator.php ├── Url │ ├── Lib │ │ ├── CtaLibUrlProvider.php │ │ ├── ExpressCheckoutLibUrlProvider.php │ │ ├── WidgetCheckoutLibUrlProvider.php │ │ └── LibUrlProvider.php │ ├── UrlBuilder.php │ └── UrlBuilder │ │ └── UrlFactory.php ├── Config │ ├── CategorySourceRegistry.php │ └── Source │ │ ├── ApiMode.php │ │ └── PaymentFlow.php ├── GraphQl │ ├── Payment │ │ └── AfterpayDataProvider.php │ └── Resolver │ │ └── AfterpayConfigMiniCart.php ├── Token.php ├── ResourceModel │ ├── Token │ │ └── Collection.php │ ├── NotAllowedProductsProvider.php │ └── Token.php ├── Payment │ ├── AdditionalInformationInterface.php │ └── AmountProcessor │ │ ├── Shipment.php │ │ └── VirtualProducts.php ├── RedirectPath.php ├── CheckoutManagement │ └── ExpressCheckoutValidator.php ├── FeatureFlag │ └── Service │ │ └── GetCreditMemoOnGrandTotalEnabled.php ├── Checkout.php ├── Shipment │ └── Express │ │ └── ShippingListProvider.php └── CheckoutConfigProvider.php ├── Api ├── Data │ ├── TokenInterface.php │ ├── RedirectPathInterface.php │ └── CheckoutInterface.php └── CheckoutManagementInterface.php ├── etc ├── payment.xml ├── adminhtml │ ├── routes.xml │ ├── events.xml │ └── di.xml ├── frontend │ ├── routes.xml │ └── sections.xml ├── webapi.xml ├── db_schema_whitelist.json ├── module.xml ├── crontab.xml ├── cron_groups.xml ├── events.xml ├── graphql │ └── di.xml └── db_schema.xml ├── Gateway ├── Config │ └── Config.php ├── Validator │ ├── ReversalResponseValidator.php │ ├── CheckoutResponseValidator.php │ ├── MerchantConfigurationResponseValidator.php │ ├── RefundResponseValidator.php │ ├── CaptureResponseValidator.php │ └── Method │ │ ├── CurrencyValidator.php │ │ └── NotAllowedProductsValidator.php ├── Request │ ├── GetMerchantConfigurationDataBuilder.php │ ├── Checkout │ │ └── GetCheckoutDataBuilder.php │ ├── GetPaymentDataBuilder.php │ ├── PaymentAction │ │ ├── ReversalDataBuilder.php │ │ ├── CaptureDataBuilder.php │ │ ├── AuthCaptureDataBuilder.php │ │ └── RefundAndVoidDataBuilder.php │ └── ExpressCheckoutDataBuilder.php ├── Response │ ├── Checkout │ │ ├── CheckoutDataResultHandler.php │ │ └── CheckoutResultHandler.php │ ├── MerchantConfiguration │ │ ├── MpidConfigurationHandler.php │ │ ├── CreditMemoOnGrandTotalConfigurationHandler.php │ │ ├── ConsumerLendingConfigurationHandler.php │ │ ├── LimitConfigurationHandler.php │ │ ├── CBTAvailableCurrenciesConfigurationHandler.php │ │ ├── SpecificCountriesConfigurationHandler.php │ │ └── ChannelsConfigurationHandler.php │ └── DiscountHandler.php ├── ErrorMessageMapper │ ├── MerchantConfigurationErrorMessageMapper.php │ └── CaptureErrorMessageMapper.php ├── Command │ ├── CommandPoolProxy.php │ └── RefundAndVoidCommand.php └── Http │ └── TransferFactory │ └── UserAgentProvider.php ├── Cron ├── OfflineCreditMemo.php └── MerchantConfigurationUpdater.php ├── Test ├── ApiFunctional │ └── GraphQl │ │ └── README.md └── Unit │ ├── Model │ ├── Order │ │ └── Payment │ │ │ └── QuotePaidStorageTest.php │ └── Url │ │ └── UrlBuilderTest.php │ └── Service │ └── Payment │ └── Auth │ └── ExpiryDateTest.php ├── Plugin ├── Checkout │ ├── CustomerData │ │ └── Cart.php │ └── Block │ │ └── Cart │ │ └── Sidebar.php ├── Block │ ├── Order │ │ └── Totals.php │ └── Adminhtml │ │ ├── Order │ │ ├── View.php │ │ └── Creditmemo │ │ │ └── Create │ │ │ └── Items.php │ │ └── CustomerBalance │ │ └── Order │ │ └── Creditmemo │ │ └── Controls.php ├── Catalog │ └── Model │ │ └── ResourceModel │ │ └── Category │ │ └── Tree.php ├── Payment │ └── Checks │ │ └── CanUseForCountryPlugin.php ├── Quote │ └── CheckoutManagement.php ├── QuoteGraphQl │ └── Cart │ │ └── PlaceOrderPlugin.php └── Sales │ └── Model │ └── Service │ └── CreditmemoService │ └── AdjustmentAmountValidation.php ├── Block ├── Payment │ └── Info.php ├── Cta │ ├── Cart.php │ ├── Product.php │ ├── ProductHeadless.php │ ├── CartHeadless.php │ └── MiniCartHeadless.php ├── Adminhtml │ └── System │ │ └── Config │ │ ├── Form │ │ └── Field │ │ │ ├── Disable.php │ │ │ ├── Version.php │ │ │ └── CBTAvailableCurrencies.php │ │ ├── Button │ │ └── LimitUpdate.php │ │ └── Fieldset │ │ └── AllowedByCountry.php └── ExpressCheckout │ ├── Cart.php │ ├── Product.php │ ├── ProductHeadless.php │ ├── CartHeadless.php │ └── MiniCartHeadless.php ├── composer.json ├── Observer ├── SetQuoteIsPaidByAfterpay.php ├── Payment │ └── DataAssignObserver.php ├── AuthCaptureBeforeShipment.php └── AuthCaptureBeforeCompleteOrder.php ├── Setup └── Patch │ └── Data │ ├── AdaptPayments.php │ ├── AfterpayPendingExceptionReviewOrderStatus.php │ ├── UpdateCbtInfoPatch.php │ └── UpdateMPIDInfoPatch.php ├── ViewModel └── Container │ ├── Cta │ ├── Lib.php │ ├── Cta.php │ └── Headless.php │ ├── Lib.php │ └── ExpressCheckout │ └── Headless.php └── Controller ├── Adminhtml └── MerchantConfiguration │ └── Update.php └── Express └── CreateCheckout.php /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /Docs/img/rest-sequence-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afterpay/afterpay-magento-2/HEAD/Docs/img/rest-sequence-diagram.png -------------------------------------------------------------------------------- /Docs/img/graphql-sequence-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afterpay/afterpay-magento-2/HEAD/Docs/img/graphql-sequence-diagram.png -------------------------------------------------------------------------------- /view/frontend/web/template/payment/info/checkout-note.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Model/Log/Handler/Debug.php: -------------------------------------------------------------------------------- 1 | getMethod() == \Afterpay\Afterpay\Gateway\Config\Config::CODE; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /view/frontend/layout/checkout_onepage_success.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Model/Checks/PaymentMethodInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 0 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /etc/frontend/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Model/PaymentStateInterface.php: -------------------------------------------------------------------------------- 1 | urlBuilder->build( 10 | \Afterpay\Afterpay\Model\Url\UrlBuilder::TYPE_JS_LIB, 11 | 'square-marketplace.js' 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /view/frontend/web/template/express-checkout/button.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /Gateway/Config/Config.php: -------------------------------------------------------------------------------- 1 | createResult(true); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Gateway/Request/GetMerchantConfigurationDataBuilder.php: -------------------------------------------------------------------------------- 1 | $buildSubject['websiteId'], 11 | ]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Model/Url/Lib/ExpressCheckoutLibUrlProvider.php: -------------------------------------------------------------------------------- 1 | urlBuilder->build( 10 | \Afterpay\Afterpay\Model\Url\UrlBuilder::TYPE_JS_LIB, 11 | 'square-marketplace.js' 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /view/frontend/web/js/view/payment/payments.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'uiComponent', 3 | 'Magento_Checkout/js/model/payment/renderer-list' 4 | ], function (Component, rendererList) { 5 | 'use strict'; 6 | 7 | rendererList.push( 8 | { 9 | type: 'afterpay', 10 | component: 'Afterpay_Afterpay/js/view/payment/method-renderer/afterpay' 11 | } 12 | ); 13 | 14 | return Component.extend({}); 15 | }); 16 | -------------------------------------------------------------------------------- /view/frontend/web/js/service/container/cta/modal-options-updater.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'jquery' 3 | ], function ($) { 4 | 'use strict'; 5 | return function (id, modalOptions) { 6 | const elem = $('#' + id); 7 | if (elem.length > 0 && elem[0].modalOptions) { 8 | elem[0].modalOptions.locale = modalOptions.locale; 9 | elem[0].modalOptions.cbtEnabled = modalOptions.cbtEnabled; 10 | } 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /etc/webapi.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Model/Config/CategorySourceRegistry.php: -------------------------------------------------------------------------------- 1 | showAllCategories; 12 | } 13 | 14 | public function setShowAllCategories(bool $value): void 15 | { 16 | $this->showAllCategories = $value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /view/frontend/layout/catalog_product_view_type_bundle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Model/GraphQl/Payment/AfterpayDataProvider.php: -------------------------------------------------------------------------------- 1 | getViewModel(); 6 | ?> 7 | 8 | isContainerEnable() && !$viewModel->getIsLibLoadedAlready()): ?> 9 | 10 | 11 | -------------------------------------------------------------------------------- /view/frontend/web/template/express-checkout/button-minicart.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /etc/db_schema_whitelist.json: -------------------------------------------------------------------------------- 1 | { 2 | "afterpay_tokens_log": { 3 | "column": { 4 | "log_id": true, 5 | "order_id": true, 6 | "token": true, 7 | "expiration_date": true 8 | }, 9 | "constraint": { 10 | "PRIMARY": true, 11 | "AFTERPAY_TOKENS_LOG_ORDER_ID_SALES_ORDER_ENTITY_ID": true, 12 | "AFTERPAY_TOKENS_LOG_TOKEN": true, 13 | "AFTERPAY_TOKENS_LOG_ORDER_ID": true 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Model/Token.php: -------------------------------------------------------------------------------- 1 | _init(ResourceModel::class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /view/frontend/web/js/service/container/cart/mini-cart-data-retriever.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'Afterpay_Afterpay/js/service/container/cart/abstract-data-retriever', 3 | 'Magento_Customer/js/customer-data', 4 | ], function (AbstractDataRetriever, customerData) { 5 | return AbstractDataRetriever.extend({ 6 | defaults: { 7 | isVisible: false, 8 | dataAmount: "0", 9 | modelWithPrice: customerData.get('cart'), 10 | priceKey: "subtotalAmount" 11 | }, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /Gateway/Response/Checkout/CheckoutDataResultHandler.php: -------------------------------------------------------------------------------- 1 | getPayment(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Cron/OfflineCreditMemo.php: -------------------------------------------------------------------------------- 1 | statusChanger = $statusChanger; 13 | } 14 | 15 | public function execute(): void 16 | { 17 | $this->statusChanger->execute(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Model/Spi/SourceValidatorServiceInterface.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | ``` 10 | 11 | ## Run 12 | ``` 13 | vendor/bin/phpunit -c dev/tests/api-functional/phpunit_graphql.xml app/code/Afterpay/Afterpay/Test/ApiFunctional/ 14 | ``` 15 | 16 | Please also refer to the documentation: 17 | 18 | https://devdocs.magento.com/guides/v2.4/graphql/functional-testing.html 19 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Plugin/Checkout/CustomerData/Cart.php: -------------------------------------------------------------------------------- 1 | getProduct()->getIsVirtual(); 18 | return $result; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /view/frontend/web/js/service/container/cart/cart-data-retriever.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'Afterpay_Afterpay/js/service/container/cart/abstract-data-retriever', 3 | 'Magento_Checkout/js/model/totals' 4 | ], function (AbstractDataRetriever, totalsModel) { 5 | return AbstractDataRetriever.extend({ 6 | defaults: { 7 | isVisible: false, 8 | dataAmount: "0", 9 | modelWithPrice: totalsModel.totals, 10 | priceKey: (checkoutConfig.payment.afterpay.isCBTCurrency === true ) ? "grand_total" : "base_grand_total" 11 | }, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /Model/ResourceModel/Token/Collection.php: -------------------------------------------------------------------------------- 1 | _init(Model::class, ResourceModel::class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Model/Spi/StockItemsValidatorInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 |
6 |
7 |
8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /etc/crontab.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0 6 * * * 6 | 7 | 8 | */5 * * * * 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Model/Payment/AdditionalInformationInterface.php: -------------------------------------------------------------------------------- 1 | quotesOrderPayments[$quoteId] = $afterpayPayment; 14 | return $this; 15 | } 16 | 17 | public function getAfterpayPaymentIfQuoteIsPaid(int $quoteId): ?Payment 18 | { 19 | return $this->quotesOrderPayments[$quoteId] ?? null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /view/frontend/templates/cta/lib.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 6 | ?> 7 | 8 | isContainerEnable() && !$viewModel->getIsLibLoadedAlready()): ?> 9 | 14 | 15 | -------------------------------------------------------------------------------- /view/frontend/templates/cta/container.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 5 | ?> 6 | 7 | isContainerEnable()): ?> 8 |
9 | 10 |
11 | 18 | 19 | -------------------------------------------------------------------------------- /Block/Payment/Info.php: -------------------------------------------------------------------------------- 1 | setData('is_pdf', true); 10 | return parent::toPdf(); 11 | } 12 | 13 | public function getSpecificInformation(): array 14 | { 15 | if ($this->getData('is_pdf')) { 16 | return []; 17 | } 18 | return parent::getSpecificInformation(); 19 | } 20 | 21 | protected function getLabel($field) 22 | { 23 | return ucfirst(str_replace('_', ' ', $field)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Model/Config/Source/ApiMode.php: -------------------------------------------------------------------------------- 1 | self::SANDBOX, 15 | 'label' => __('Sandbox') 16 | ], 17 | [ 18 | 'value' => self::PRODUCTION, 19 | 'label' => __('Production') 20 | ] 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /etc/cron_groups.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 4 6 | 2 7 | 10 8 | 60 9 | 600 10 | 1 11 | 12 | 13 | -------------------------------------------------------------------------------- /view/frontend/web/template/cta/pdp/cta.html: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | -------------------------------------------------------------------------------- /Gateway/Validator/CheckoutResponseValidator.php: -------------------------------------------------------------------------------- 1 | createResult(false, [$response['message']], [$response['errorCode']]); 13 | } 14 | 15 | return $this->createResult(true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Gateway/Validator/MerchantConfigurationResponseValidator.php: -------------------------------------------------------------------------------- 1 | createResult(false, [], [$response['errorCode']]); 13 | } 14 | 15 | return $this->createResult(true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Model/Config/Source/PaymentFlow.php: -------------------------------------------------------------------------------- 1 | self::IMMEDIATE, 15 | 'label' => __('Immediate Payment Flow') 16 | ], 17 | [ 18 | 'value' => self::DEFERRED, 19 | 'label' => __('Deferred Payment Flow') 20 | ] 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /view/frontend/web/template/cta/cta.html: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | -------------------------------------------------------------------------------- /Gateway/ErrorMessageMapper/MerchantConfigurationErrorMessageMapper.php: -------------------------------------------------------------------------------- 1 | getOrder(); 10 | 11 | if (!$order) { 12 | return $result; 13 | } 14 | 15 | $isCbtCurrency = (bool) $order->getPayment()->getAdditionalInformation( 16 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY 17 | ); 18 | 19 | if ($isCbtCurrency === true) { 20 | unset($result['base_grandtotal']); 21 | } 22 | 23 | return $result; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /view/frontend/templates/express-checkout/button.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 5 | ?> 6 | isContainerEnable()): ?> 7 |
8 | 9 |
10 | 17 | 18 | -------------------------------------------------------------------------------- /Api/Data/RedirectPathInterface.php: -------------------------------------------------------------------------------- 1 | getValidatorPool()->get('quote_items'); 14 | } catch (\Throwable $e) { 15 | return true; 16 | } 17 | 18 | $result = $quoteItemsValidator->validate(['quote' => $quote]); 19 | return $result->isValid(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Model/Order/Payment/Auth/TokenValidator.php: -------------------------------------------------------------------------------- 1 | tokensResource = $tokensResource; 15 | } 16 | 17 | public function checkIsUsed(string $token): bool 18 | { 19 | if (!isset($this->results[$token])) { 20 | $this->results[$token] = (bool)$this->tokensResource->selectByToken($token); 21 | } 22 | 23 | return $this->results[$token]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /view/frontend/templates/cta/minicart/headless.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 5 | $jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/cta/minicart/headless.js'); 6 | ?> 7 | 15 | 16 | -------------------------------------------------------------------------------- /view/frontend/templates/cta/pdp/headless.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 5 | $jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/cta/pdp/headless.js'); 6 | ?> 7 | 13 | 14 | -------------------------------------------------------------------------------- /view/frontend/layout/catalog_product_view_type_grouped.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Gateway/Response/MerchantConfiguration/MpidConfigurationHandler.php: -------------------------------------------------------------------------------- 1 | config = $config; 13 | } 14 | 15 | public function handle(array $handlingSubject, array $response): void 16 | { 17 | $websiteId = (int)$handlingSubject['websiteId']; 18 | $mpid = $response['publicId'] ?? ''; 19 | $this->config->setPublicId($mpid, $websiteId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Model/Url/Lib/WidgetCheckoutLibUrlProvider.php: -------------------------------------------------------------------------------- 1 | config = $config; 15 | } 16 | 17 | protected function buildUrl(): string 18 | { 19 | return $this->urlBuilder->build( 20 | \Afterpay\Afterpay\Model\Url\UrlBuilder::TYPE_JS_LIB, 21 | 'square-marketplace.js' 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Gateway/Request/Checkout/GetCheckoutDataBuilder.php: -------------------------------------------------------------------------------- 1 | getPayment()->getAdditionalInformation( 14 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_TOKEN 15 | ); 16 | return [ 17 | 'storeId' => $payment->getOrder()->getStoreId(), 18 | 'afterpayToken' => $afterpayToken 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Gateway/Request/GetPaymentDataBuilder.php: -------------------------------------------------------------------------------- 1 | getPayment()->getAdditionalInformation( 14 | \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_ORDER_ID 15 | ); 16 | return [ 17 | 'storeId' => $payment->getOrder()->getStoreId(), 18 | 'orderId' => $afterpayOrderId, 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Gateway/Request/PaymentAction/ReversalDataBuilder.php: -------------------------------------------------------------------------------- 1 | getPayment()->getAdditionalInformation( 14 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_TOKEN 15 | ); 16 | 17 | return [ 18 | 'storeId' => $payment->getOrder()->getStoreId(), 19 | 'afterpayToken' => $afterpayToken 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Block/Cta/Cart.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | if ($this->config->getIsEnableCtaCartPage() && !$this->config->getIsEnableCartPageHeadless()) { 24 | return parent::_toHtml(); 25 | } 26 | 27 | return ''; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Model/RedirectPath.php: -------------------------------------------------------------------------------- 1 | confirmPath = $path; 13 | return $this; 14 | } 15 | 16 | public function getConfirmPath(): string 17 | { 18 | return $this->confirmPath; 19 | } 20 | 21 | public function setCancelPath(string $path): self 22 | { 23 | $this->cancelPath = $path; 24 | return $this; 25 | } 26 | 27 | public function getCancelPath(): string 28 | { 29 | return $this->cancelPath; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Block/Cta/Product.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | if ($this->config->getIsEnableCtaProductPage() && !$this->config->getIsEnableProductPageHeadless()) { 24 | return parent::_toHtml(); 25 | } 26 | 27 | return ''; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Model/Order/Payment/Auth/TokenSaver.php: -------------------------------------------------------------------------------- 1 | tokensResource = $tokensResource; 18 | $this->dateTime = $dateTime; 19 | } 20 | 21 | public function execute(int $orderId, string $token, ?string $expiryDate): bool 22 | { 23 | return (bool)$this->tokensResource->insertNewToken($orderId, $token, $this->dateTime->formatDate($expiryDate)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/Form/Field/Disable.php: -------------------------------------------------------------------------------- 1 | unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue(); 13 | return parent::render($element); 14 | } 15 | 16 | protected function _getElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element) 17 | { 18 | /** @phpstan-ignore-next-line */ 19 | $element->setDisabled('disabled'); 20 | return $element->getElementHtml(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /view/frontend/web/js/service/container/configurable-mixin.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'jquery', 3 | "Afterpay_Afterpay/js/model/container/container-model-holder" 4 | ], function ($, containerModelHolder) { 5 | 'use strict'; 6 | const containerModel = containerModelHolder.getModel("afterpay-pdp-container"); 7 | const configurableMixin = { 8 | _configureElement: function (element) { 9 | const res = this._super(element); 10 | if (this.simpleProduct) { 11 | containerModel.setCurrentProductId(this.simpleProduct); 12 | } 13 | return res; 14 | } 15 | }; 16 | return function (targetWidget) { 17 | $.widget('mage.configurable', targetWidget, configurableMixin); 18 | 19 | return $.mage.configurable; 20 | }; 21 | }); 22 | -------------------------------------------------------------------------------- /Model/Order/OrderItem.php: -------------------------------------------------------------------------------- 1 | refundedQty; 13 | } 14 | 15 | public function setAfterpayRefundedQty(float $qty): OrderItemInterface 16 | { 17 | $this->refundedQty = $qty; 18 | return $this; 19 | } 20 | 21 | public function getAfterpayVoidedQty(): float 22 | { 23 | return $this->voidedQty; 24 | } 25 | 26 | public function setAfterpayVoidedQty(float $qty): OrderItemInterface 27 | { 28 | $this->voidedQty = $qty; 29 | return $this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/limits.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'jquery' 3 | ], function ($) { 4 | return function (options, selector) { 5 | $(options.spinnerId).hide(); 6 | $(selector).click(function (e) { 7 | e.preventDefault(); 8 | $(selector).addClass('disabled'); 9 | $(options.spinnerId).show(); 10 | $.post( 11 | options.actionUrl, 12 | { 13 | websiteId: $('#website_switcher').val() 14 | } 15 | ).done(() => location.reload()) 16 | .always(function () { 17 | $(selector).removeClass('disabled'); 18 | $(options.spinnerId).hide(); 19 | }) 20 | e.stopPropagation(); 21 | return false; 22 | }); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /Block/ExpressCheckout/Cart.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | if ($this->config->getIsEnableExpressCheckoutCartPage() && !$this->config->getIsEnableCartPageHeadless()) { 24 | return parent::_toHtml(); 25 | } 26 | 27 | return ''; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Block/ExpressCheckout/Product.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | if ($this->config->getIsEnableExpressCheckoutProductPage() && !$this->config->getIsEnableProductPageHeadless()) { 24 | return parent::_toHtml(); 25 | } 26 | 27 | return ''; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Gateway/Validator/RefundResponseValidator.php: -------------------------------------------------------------------------------- 1 | createResult(false, [$response['message']], [$response['errorCode']]); 13 | } 14 | 15 | if (isset($response['refundId'])) { 16 | return $this->createResult(true); 17 | } 18 | 19 | return $this->createResult(false, [__('Unknown result has been returned')]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Model/Payment/AmountProcessor/Shipment.php: -------------------------------------------------------------------------------- 1 | getOrderItem()->getParentItem()) { 15 | $amount += $this->calculateItemPrice($payment, $item->getOrderItem(), (float)$item->getQty()); 16 | } 17 | } 18 | $amount += $this->getShippingAmount($payment->getOrder()); 19 | 20 | return $this->processDiscount($amount, $payment); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Model/Url/Lib/LibUrlProvider.php: -------------------------------------------------------------------------------- 1 | urlBuilder = $urlBuilder; 14 | } 15 | 16 | public function getAfterpayLib(): ?string 17 | { 18 | if (!$this->isLibGotten) { 19 | $this->isLibGotten = true; 20 | return $this->buildUrl(); 21 | } 22 | return null; 23 | } 24 | 25 | public function getIsLibGotten(): bool 26 | { 27 | return $this->isLibGotten; 28 | } 29 | 30 | abstract protected function buildUrl(): string; 31 | } 32 | -------------------------------------------------------------------------------- /Gateway/ErrorMessageMapper/CaptureErrorMessageMapper.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | } 17 | 18 | public function handle(array $handlingSubject, array $response): void 19 | { 20 | $websiteId = (int)$handlingSubject['websiteId']; 21 | $mpid = $response['publicId'] ?? ''; 22 | $flagValue = false; // TODO: replace it with a flag pull 23 | $this->config->setIsCreditMemoGrandTotalOnlyEnabled((int)$flagValue, $websiteId); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Model/Order/Payment/Auth/ExpiryDate.php: -------------------------------------------------------------------------------- 1 | timezone = $timezone; 15 | } 16 | 17 | public function format(?string $date = null): string 18 | { 19 | return $this->timezone->date($date)->format(static::FORMAT); 20 | } 21 | 22 | public function isExpired(string $expireDate, ?string $dateToCheck = null): bool 23 | { 24 | if ($dateToCheck == null) { 25 | $dateToCheck = $this->format(); 26 | } 27 | return strtotime($expireDate) < strtotime($dateToCheck); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /etc/adminhtml/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "afterpay-global/module-afterpay", 3 | "license": "Apache-2.0", 4 | "type": "magento2-module", 5 | "description": "Magento 2 Afterpay Payment Module", 6 | "version": "5.4.4", 7 | "require": { 8 | "php": ">=7.4.0", 9 | "magento/framework": "^103.0", 10 | "magento/module-checkout": "100.4.*", 11 | "magento/module-payment": "100.4.*", 12 | "magento/module-quote-graph-ql": "100.4.*", 13 | "magento/module-sales": "103.0.*", 14 | "magento/module-weee": "100.4.*", 15 | "afterpay-global/module-cash-app-pay" : "^5" 16 | }, 17 | "authors": [ 18 | { 19 | "name": "Afterpay", 20 | "homepage": "https://www.afterpay.com" 21 | } 22 | ], 23 | "autoload": { 24 | "files": [ 25 | "registration.php" 26 | ], 27 | "psr-4": { 28 | "Afterpay\\Afterpay\\": "" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /view/frontend/requirejs-config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | map: { 3 | '*': { 4 | "afterpayBaseContainer": "Afterpay_Afterpay/js/view/container/container", 5 | "afterpayCta": "Afterpay_Afterpay/js/view/container/cta/cta", 6 | "afterpayCtaPdp": "Afterpay_Afterpay/js/view/container/cta/pdp/cta", 7 | "afterpayExpressCheckoutButton": "Afterpay_Afterpay/js/view/container/express-checkout/button", 8 | "afterpayExpressCheckoutButtonPdp": "Afterpay_Afterpay/js/view/container/express-checkout/product/button" 9 | } 10 | }, 11 | config: { 12 | mixins: { 13 | "Magento_Catalog/js/price-box": { 14 | "Afterpay_Afterpay/js/service/container/pricebox-widget-mixin": true 15 | }, 16 | "Magento_ConfigurableProduct/js/configurable": { 17 | "Afterpay_Afterpay/js/service/container/configurable-mixin": true 18 | } 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /Model/CheckoutManagement/ExpressCheckoutValidator.php: -------------------------------------------------------------------------------- 1 | config = $config; 13 | } 14 | 15 | /** 16 | * @inheritDoc 17 | */ 18 | public function validate(\Magento\Quote\Model\Quote $quote): void 19 | { 20 | $grandTotal = $quote->getBaseGrandTotal(); 21 | if ($grandTotal < $this->config->getMinOrderTotal() || 22 | $grandTotal > $this->config->getMaxOrderTotal()) { 23 | throw new \Magento\Framework\Validation\ValidationException( 24 | __('Order amount exceed Afterpay order limit.') 25 | ); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /view/adminhtml/templates/system/config/button/update.phtml: -------------------------------------------------------------------------------- 1 | 5 |
6 | 9 |
10 |
11 | 12 |
13 |
14 |
15 | 25 | -------------------------------------------------------------------------------- /Block/Cta/ProductHeadless.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | /** @var \Afterpay\Afterpay\ViewModel\Container\Cta\Headless $viewModel */ 24 | $viewModel = $this->getViewModel(); 25 | if ($viewModel && $viewModel->isContainerEnable() 26 | && $this->config->getIsEnableCtaProductPage() && $this->config->getIsEnableProductPageHeadless()) { 27 | return parent::_toHtml(); 28 | } 29 | 30 | return ''; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /view/frontend/templates/cta/cart/headless.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 5 | $jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/cta/cart/headless.js'); 6 | ?> 7 | 21 | 22 | -------------------------------------------------------------------------------- /Model/Url/UrlBuilder.php: -------------------------------------------------------------------------------- 1 | urlFactory = $urlFactory; 16 | } 17 | 18 | public function build(string $type, string $path, array $pathArgs = [], ?int $storeId = null): string 19 | { 20 | return $this->urlFactory->create($type, $storeId, $pathArgs) . $this->replaceArgsInPath($path, $pathArgs); 21 | } 22 | 23 | private function replaceArgsInPath(string $path, array $args): string 24 | { 25 | foreach ($args as $argKey => $argVal) { 26 | $path = str_replace('{' . $argKey . '}', (string)$argVal, $path); 27 | } 28 | return $path; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Plugin/Catalog/Model/ResourceModel/Category/Tree.php: -------------------------------------------------------------------------------- 1 | categorySourceRegistry = $categorySourceRegistry; 12 | } 13 | 14 | public function beforeAddCollectionData( 15 | \Magento\Catalog\Model\ResourceModel\Category\Tree $subject, 16 | $collection = null, 17 | $sorted = false, 18 | $exclude = [], 19 | $toLoad = true, 20 | $onlyActive = false 21 | ): array { 22 | return [ 23 | $collection, 24 | $sorted, 25 | $exclude, 26 | $toLoad, 27 | $this->categorySourceRegistry->getShowAllCategories() ? false : $onlyActive 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Block/ExpressCheckout/ProductHeadless.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | /** @var \Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless $viewModel */ 24 | $viewModel = $this->getViewModel(); 25 | if ($viewModel && $viewModel->isContainerEnable() 26 | && $this->config->getIsEnableExpressCheckoutProductPage() && $this->config->getIsEnableProductPageHeadless()) { 27 | return parent::_toHtml(); 28 | } 29 | 30 | return ''; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Gateway/Response/MerchantConfiguration/ConsumerLendingConfigurationHandler.php: -------------------------------------------------------------------------------- 1 | config = $config; 13 | } 14 | 15 | public function handle(array $handlingSubject, array $response): void 16 | { 17 | $websiteId = (int)$handlingSubject['websiteId']; 18 | $consumerLending = $response['consumerLending']['enabled'] ?? false; 19 | $this->config->setConsumerLendingEnabled((int)$consumerLending, $websiteId); 20 | if ($consumerLending) { 21 | $minAmount = $response['consumerLending']['minimumAmount']['amount'] ?? null; 22 | $this->config->setConsumerLendingMinAmount($minAmount, $websiteId); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Plugin/Payment/Checks/CanUseForCountryPlugin.php: -------------------------------------------------------------------------------- 1 | getCode() !== \Afterpay\Afterpay\Gateway\Config\Config::CODE) { 14 | return $result; 15 | } 16 | 17 | $billingAddress = $quote->getBillingAddress(); 18 | if ($billingAddress->getCountry()) { 19 | return $paymentMethod->canUseForCountry($billingAddress->getCountry()); 20 | } 21 | 22 | $shippingAddress = $quote->getShippingAddress(); 23 | if ($shippingAddress->getCountry()) { 24 | return $paymentMethod->canUseForCountry($shippingAddress->getCountry()); 25 | } 26 | 27 | return $result; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Block/Cta/CartHeadless.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | /** @var \Afterpay\Afterpay\ViewModel\Container\Cta\Headless $viewModel */ 24 | $viewModel = $this->getViewModel(); 25 | if ($viewModel && $viewModel->isContainerEnable() && $this->isEnabledForCart()) { 26 | return parent::_toHtml(); 27 | } 28 | 29 | return ''; 30 | } 31 | public function isEnabledForCart(): bool 32 | { 33 | return $this->config->getIsEnableCtaCartPage() && $this->config->getIsEnableCartPageHeadless(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/Form/Field/Version.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | protected function _getElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element) 30 | { 31 | return $this->resource->getDataVersion(self::MODULE_NAME) ?: ""; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Block/Cta/MiniCartHeadless.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | /** @var \Afterpay\Afterpay\ViewModel\Container\Cta\Headless $viewModel */ 24 | $viewModel = $this->getViewModel(); 25 | if ($viewModel && $viewModel->isContainerEnable() && $this->isEnabledForMinicart()) { 26 | return parent::_toHtml(); 27 | } 28 | 29 | return ''; 30 | } 31 | 32 | public function isEnabledForMinicart(): bool 33 | { 34 | return $this->config->getIsEnableCtaMiniCart() && $this->config->getIsEnableMiniCartHeadless(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Model/Order/CreditMemo/StatusChanger.php: -------------------------------------------------------------------------------- 1 | ordersRetriever = $ordersRetriever; 17 | $this->creditMemoProcessor = $creditMemoProcessor; 18 | $this->logger = $logger; 19 | } 20 | 21 | public function execute(): void 22 | { 23 | $orders = $this->ordersRetriever->getAfterpayOrders(); 24 | foreach ($orders as $order) { 25 | try { 26 | $this->creditMemoProcessor->processOrder($order); 27 | } catch (\Magento\Framework\Exception\LocalizedException $e) { 28 | $this->logger->error($e->getMessage()); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /view/frontend/web/js/action/create-afterpay-checkout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @api 3 | */ 4 | define([ 5 | 'Magento_Checkout/js/model/quote', 6 | 'Magento_Checkout/js/model/url-builder', 7 | 'mage/storage', 8 | 'Magento_Checkout/js/model/error-processor', 9 | 'Magento_Checkout/js/model/full-screen-loader', 10 | ], function (quote, urlBuilder, storage, errorProcessor, fullScreenLoader) { 11 | 'use strict'; 12 | 13 | return function (messageContainer, redirectPath) { 14 | const payload = { 15 | cartId: quote.getQuoteId(), 16 | redirectPath: redirectPath 17 | }; 18 | 19 | const serviceUrl = urlBuilder.createUrl('/afterpay/checkout', {}); 20 | 21 | fullScreenLoader.startLoader(); 22 | 23 | return storage.post( 24 | serviceUrl, 25 | JSON.stringify(payload), 26 | true, 27 | 'application/json' 28 | ).fail(function (response) { 29 | errorProcessor.process(response, messageContainer); 30 | }).always(function () { 31 | fullScreenLoader.stopLoader(); 32 | }); 33 | }; 34 | }); 35 | -------------------------------------------------------------------------------- /Block/ExpressCheckout/CartHeadless.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | /** @var \Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless $viewModel */ 24 | $viewModel = $this->getViewModel(); 25 | if ($viewModel && $viewModel->isContainerEnable() && $this->isEnabledForCart()) { 26 | return parent::_toHtml(); 27 | } 28 | 29 | return ''; 30 | } 31 | public function isEnabledForCart(): bool 32 | { 33 | return $this->config->getIsEnableExpressCheckoutCartPage() && $this->config->getIsEnableCartPageHeadless(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Block/ExpressCheckout/MiniCartHeadless.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | } 20 | 21 | protected function _toHtml() 22 | { 23 | /** @var \Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless $viewModel */ 24 | $viewModel = $this->getViewModel(); 25 | if ($viewModel && $viewModel->isContainerEnable() && $this->isEnabledForMinicart()) { 26 | return parent::_toHtml(); 27 | } 28 | 29 | return ''; 30 | } 31 | 32 | public function isEnabledForMinicart(): bool 33 | { 34 | return $this->config->getIsEnableExpressCheckoutMiniCart() && $this->config->getIsEnableMiniCartHeadless(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Observer/SetQuoteIsPaidByAfterpay.php: -------------------------------------------------------------------------------- 1 | quotePaidStorage = $quotePaidStorage; 15 | $this->checkPaymentMethod = $checkPaymentMethod; 16 | } 17 | 18 | public function execute(\Magento\Framework\Event\Observer $observer) 19 | { 20 | /** @var \Magento\Sales\Model\Order\Payment $payment */ 21 | $payment = $observer->getEvent()->getData('payment'); 22 | 23 | if ($this->checkPaymentMethod->isAfterPayMethod($payment)) { 24 | $this->quotePaidStorage->setAfterpayPaymentForQuote((int)$payment->getOrder()->getQuoteId(), $payment); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Gateway/Response/Checkout/CheckoutResultHandler.php: -------------------------------------------------------------------------------- 1 | getPayment($handlingSubject); 10 | 11 | $payment->setAdditionalInformation( 12 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_TOKEN, 13 | $response['token'] 14 | ); 15 | $payment->setAdditionalInformation( 16 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_AUTH_TOKEN_EXPIRES, 17 | $response['expires'] 18 | ); 19 | $payment->setAdditionalInformation( 20 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_REDIRECT_CHECKOUT_URL, 21 | $response['redirectCheckoutUrl'] 22 | ); 23 | } 24 | 25 | protected function getPayment(array $handlingSubject): \Magento\Payment\Model\InfoInterface 26 | { 27 | $quote = $handlingSubject['quote']; 28 | return $quote->getPayment(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Gateway/Validator/CaptureResponseValidator.php: -------------------------------------------------------------------------------- 1 | createResult( 16 | false, 17 | [self::STATUS_DECLINED] 18 | ); 19 | } 20 | 21 | if (isset($response['status']) && $response['status'] == self::STATUS_APPROVED) { 22 | return $this->createResult(true); 23 | } 24 | 25 | if (isset($response['errorCode'])) { 26 | return $this->createResult(false, [$response['message']], [$response['errorCode']]); 27 | } 28 | 29 | return $this->createResult(false, ['Unknown status has been returned']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Plugin/Block/Adminhtml/Order/View.php: -------------------------------------------------------------------------------- 1 | checkPaymentMethod = $checkPaymentMethod; 12 | } 13 | 14 | /** 15 | * @param string $buttonId 16 | * @param \Magento\Sales\Block\Adminhtml\Order\View $result 17 | * @return \Magento\Sales\Block\Adminhtml\Order\View 18 | */ 19 | public function afterAddButton( 20 | \Magento\Sales\Block\Adminhtml\Order\View $orderView, 21 | $result, 22 | $buttonId 23 | ) { 24 | if ($buttonId !== 'order_creditmemo') { 25 | return $result; 26 | } 27 | $order = $orderView->getOrder(); 28 | $payment = $order->getPayment(); 29 | if ($payment == null) { 30 | return $result; 31 | } 32 | if ($this->checkPaymentMethod->isAfterPayMethod($payment)) { 33 | $orderView->removeButton($buttonId); 34 | } 35 | return $result; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /etc/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Gateway/Response/MerchantConfiguration/LimitConfigurationHandler.php: -------------------------------------------------------------------------------- 1 | config = $config; 13 | } 14 | 15 | public function handle(array $handlingSubject, array $response): void 16 | { 17 | $websiteId = (int)$handlingSubject['websiteId']; 18 | if (isset($response['maximumAmount']['amount'])) { 19 | $minimumAmount = $response['minimumAmount']['amount'] ?? "0"; 20 | $minimumAmount = (string)max((float)$minimumAmount, 1); 21 | $maximumAmount = $response['maximumAmount']['amount']; 22 | $this->config 23 | ->setMinOrderTotal($minimumAmount, $websiteId) 24 | ->setMaxOrderTotal($maximumAmount, $websiteId); 25 | } else { 26 | $this->config 27 | ->deleteMaxOrderTotal($websiteId) 28 | ->deleteMinOrderTotal($websiteId); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Model/FeatureFlag/Service/GetCreditMemoOnGrandTotalEnabled.php: -------------------------------------------------------------------------------- 1 | client = $client; 17 | } 18 | 19 | /** 20 | * Pulls the "credit-memo-on-grand-total-enabled" feature flag value from the internal API. 21 | * 22 | * @param string $mpid Merchant Public ID - Afterpay\Afterpay\Model\Config::getPublicId 23 | * @param string $countryCode Merchant Country ID - Afterpay\Afterpay\Model\Config::getMerchantCountry 24 | * 25 | * @return bool 26 | * @throws LocalizedException 27 | */ 28 | public function execute(string $mpid, string $countryCode): bool 29 | { 30 | $response = $this->client->execute(self::CREDIT_MEMP_ON_GRAND_TOTAL_ENABLED_FLAG, $mpid, $countryCode); 31 | 32 | return $response['value'] ?? false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Model/Checkout.php: -------------------------------------------------------------------------------- 1 | afterpayToken = $token; 14 | return $this; 15 | } 16 | 17 | public function getAfterpayToken(): string 18 | { 19 | return $this->afterpayToken; 20 | } 21 | 22 | public function setAfterpayAuthTokenExpires(string $authTokenExpires): self 23 | { 24 | $this->afterpayAuthTokenExpires = $authTokenExpires; 25 | return $this; 26 | } 27 | 28 | public function getAfterpayAuthTokenExpires(): string 29 | { 30 | return $this->afterpayAuthTokenExpires; 31 | } 32 | 33 | public function setAfterpayRedirectCheckoutUrl(string $redirectCheckoutUrl): self 34 | { 35 | $this->afterpayRedirectCheckoutUrl = $redirectCheckoutUrl; 36 | return $this; 37 | } 38 | 39 | public function getAfterpayRedirectCheckoutUrl(): string 40 | { 41 | return $this->afterpayRedirectCheckoutUrl; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Model/ResourceModel/NotAllowedProductsProvider.php: -------------------------------------------------------------------------------- 1 | config = $config; 15 | $this->resourceConnection = $resourceConnection; 16 | } 17 | 18 | public function provideIds(?int $storeId = null): array 19 | { 20 | $excludedCategoriesIds = $this->config->getExcludeCategories($storeId); 21 | if (empty($excludedCategoriesIds)) { 22 | return []; 23 | } 24 | 25 | $connection = $this->resourceConnection->getConnection(); 26 | $select = $connection->select()->from( 27 | ['cat' => $this->resourceConnection->getTableName('catalog_category_product')], 28 | 'cat.product_id' 29 | )->where($connection->prepareSqlCondition('cat.category_id', ['in' => $excludedCategoriesIds])); 30 | 31 | return $connection->fetchCol($select); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Test/Unit/Model/Order/Payment/QuotePaidStorageTest.php: -------------------------------------------------------------------------------- 1 | createMock(\Magento\Sales\Model\Order\Payment::class); 12 | 13 | $testAfterpayOrderPaymentMock->expects($this->any()) 14 | ->method('getId') 15 | ->willReturn($testPaymentId); 16 | 17 | $quotePaidStorage = new \Afterpay\Afterpay\Model\Order\Payment\QuotePaidStorage(); 18 | $quotePaidStorage->setAfterpayPaymentForQuote($testQuoteId, $testAfterpayOrderPaymentMock); 19 | static::assertSame( 20 | $testAfterpayOrderPaymentMock->getId(), 21 | $quotePaidStorage->getAfterpayPaymentIfQuoteIsPaid($testQuoteId)->getId() 22 | ); 23 | } 24 | 25 | public function testEmptyPayment() 26 | { 27 | $testUnexistedQuoteId = 99; 28 | 29 | $quotePaidStorage = new \Afterpay\Afterpay\Model\Order\Payment\QuotePaidStorage(); 30 | static::assertSame($quotePaidStorage->getAfterpayPaymentIfQuoteIsPaid($testUnexistedQuoteId), null); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /etc/graphql/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | Afterpay\Afterpay\Model\GraphQl\Payment\AfterpayDataProvider 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | Afterpay\Afterpay\Gateway\Command\ValidateCheckoutDataCommand 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /view/frontend/web/js/view/container/express-checkout/check-message.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", (event) => { 2 | 3 | let url = window.location.href; 4 | 5 | if (url.includes('success')) { 6 | const handleMessages = () => { 7 | let messages = document.querySelectorAll(".message.success"); 8 | 9 | messages?.forEach((message) => { 10 | let links = message.getElementsByTagName('a'); 11 | 12 | if(links.length > 0) { 13 | for (let link of links) { 14 | if (link.href.includes("checkout/cart")) { 15 | message.remove(); 16 | break; 17 | } 18 | } 19 | }else { 20 | message.classList.add("show-message"); 21 | } 22 | }); 23 | }; 24 | 25 | handleMessages(); 26 | 27 | const observer = new MutationObserver((mutations) => { 28 | mutations.forEach((mutation) => { 29 | if (mutation.addedNodes.length > 0) { 30 | handleMessages(); 31 | } 32 | }); 33 | }); 34 | 35 | observer.observe(document.body, { childList: true, subtree: true }); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /Gateway/Response/MerchantConfiguration/CBTAvailableCurrenciesConfigurationHandler.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | $this->serializer = $serializer; 20 | } 21 | 22 | public function handle(array $handlingSubject, array $response): void 23 | { 24 | $websiteId = (int)$handlingSubject['websiteId']; 25 | $cbtAvailableCurrencies = ''; 26 | 27 | if (isset($response['CBT']['enabled']) && 28 | isset($response['CBT']['limits']) && 29 | is_array($response['CBT']['limits']) 30 | ) { 31 | $cbtAvailableCurrencies = $this->serializer->serialize($response['CBT']['limits']); 32 | } 33 | 34 | $this->config->setCbtCurrencyLimits($cbtAvailableCurrencies, $websiteId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Model/Order/CreditMemo/OrderUpdater.php: -------------------------------------------------------------------------------- 1 | orderRepository = $orderRepository; 16 | } 17 | public function updateOrder( 18 | \Magento\Sales\Model\Order $order 19 | ): \Magento\Sales\Model\Order { 20 | $payment = $order->getPayment(); 21 | /** @var \Magento\Sales\Api\Data\OrderPaymentInterface $payment */ 22 | $additionalInformation = $payment->getAdditionalInformation(); 23 | $paymentState = $additionalInformation[ 24 | AdditionalInformationInterface::AFTERPAY_PAYMENT_STATE 25 | ]; 26 | if ($paymentState == PaymentStateInterface::CAPTURED) { 27 | $order->setState(\Magento\Sales\Model\Order::STATE_COMPLETE); 28 | $order->setStatus(\Magento\Sales\Model\Order::STATE_COMPLETE); 29 | } 30 | $this->orderRepository->save($order); 31 | return $order; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Test/Unit/Model/Url/UrlBuilderTest.php: -------------------------------------------------------------------------------- 1 | createMock(\Afterpay\Afterpay\Model\Url\UrlBuilder\UrlFactory::class); 16 | $urlFactoryMock->expects($this->once()) 17 | ->method('create') 18 | ->willReturn($testUrl); 19 | 20 | $urlBuilder = new \Afterpay\Afterpay\Model\Url\UrlBuilder($urlFactoryMock); 21 | $url = $urlBuilder->build($testType, $path, $args); 22 | 23 | static::assertSame($testUrl . $expectedPath, $url); 24 | } 25 | 26 | public function argumentsReplacementDataProvider(): array 27 | { 28 | return [ 29 | ['v2/payments/{orderId}/capture', ['orderId' => 12345], 'v2/payments/12345/capture'], 30 | ['v2/payments/auth', [], 'v2/payments/auth'], 31 | ['v2/payments/capture', ['unExistedArg' => 999], 'v2/payments/capture'] 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Observer/Payment/DataAssignObserver.php: -------------------------------------------------------------------------------- 1 | readDataArgument($observer); 16 | $additionalData = $data->getData(\Magento\Quote\Api\Data\PaymentInterface::KEY_ADDITIONAL_DATA); 17 | if (!is_array($additionalData)) { 18 | return; 19 | } 20 | 21 | $paymentInfo = $this->readPaymentModelArgument($observer); 22 | 23 | foreach ($this->additionalInformationList as $additionalInformationKey) { 24 | if (isset($additionalData[$additionalInformationKey])) { 25 | $paymentInfo->setAdditionalInformation( 26 | $additionalInformationKey, 27 | $additionalData[$additionalInformationKey] 28 | ); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Setup/Patch/Data/AdaptPayments.php: -------------------------------------------------------------------------------- 1 | salesSetup = $salesSetup; 15 | } 16 | 17 | public function getAliases(): array 18 | { 19 | return []; 20 | } 21 | 22 | public static function getDependencies(): array 23 | { 24 | return []; 25 | } 26 | 27 | public function apply(): self 28 | { 29 | $this->salesSetup->getConnection() 30 | ->update( 31 | $this->salesSetup->getTable('sales_order_payment'), 32 | [ 33 | 'method' => \Afterpay\Afterpay\Gateway\Config\Config::CODE, 34 | 'additional_information' => new \Zend_Db_Expr( 35 | 'replace(additional_information, "afterpay_payment_status", "afterpay_payment_state")' 36 | ) 37 | ], 38 | ['method = ?' => static::METHOD_CODE] 39 | ); 40 | 41 | return $this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Model/Shipment/Express/ShippingListProvider.php: -------------------------------------------------------------------------------- 1 | createShippingOption = $createShippingOption; 15 | $this->shipmentEstimation = $shipmentEstimation; 16 | } 17 | 18 | public function provide(\Magento\Quote\Model\Quote $quote): array 19 | { 20 | $shippingMethods = $this->shipmentEstimation->estimateByExtendedAddress( 21 | $quote->getId(), 22 | $quote->getShippingAddress() 23 | ); 24 | $shippingOptions = []; 25 | foreach ($shippingMethods as $shippingMethod) { 26 | if (!$shippingMethod->getAvailable()) { 27 | continue; 28 | } 29 | $shippingOption = $this->createShippingOption->create($quote, $shippingMethod); 30 | if ($shippingOption != null) { 31 | $shippingOptions[] = $shippingOption; 32 | } 33 | } 34 | return $shippingOptions; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Plugin/Block/Adminhtml/Order/Creditmemo/Create/Items.php: -------------------------------------------------------------------------------- 1 | layout = $layout; 15 | $this->checkPaymentMethod = $checkPaymentMethod; 16 | } 17 | 18 | /** 19 | * @return null 20 | */ 21 | public function beforeToHtml( 22 | \Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Items $creditmemoBlock 23 | ) { 24 | $creditmemo = $creditmemoBlock->getCreditmemo(); 25 | $payment = $creditmemo->getOrder()->getPayment(); 26 | if ($payment == null) { 27 | return null; 28 | } 29 | 30 | if ($this->checkPaymentMethod->isAfterPayMethod($payment)) { 31 | $this->layout->unsetChild( 32 | $creditmemoBlock->getNameInLayout(), 33 | !$creditmemo->canRefund() ? 'submit_button' : 'submit_offline' 34 | ); 35 | } 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Gateway/Validator/Method/CurrencyValidator.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | $this->checkoutSession = $checkoutSession; 17 | parent::__construct($resultFactory); 18 | } 19 | 20 | public function validate(array $validationSubject): \Magento\Payment\Gateway\Validator\ResultInterface 21 | { 22 | $quote = $this->checkoutSession->getQuote(); 23 | $currentCurrency = $quote->getStore()->getCurrentCurrencyCode(); 24 | $allowedCurrencies = $this->config->getAllowedCurrencies(); 25 | $cbtCurrencies = array_keys($this->config->getCbtCurrencyLimits()); 26 | 27 | if (in_array($currentCurrency, array_merge($allowedCurrencies, $cbtCurrencies))) { 28 | return $this->createResult(true); 29 | } 30 | 31 | return $this->createResult(false); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Model/Order/CreditMemo/CaptureProcessor.php: -------------------------------------------------------------------------------- 1 | authCaptureCommand = $authCaptureCommand; 18 | $this->paymentDataObjectFactory = $paymentDataObjectFactory; 19 | } 20 | 21 | /** 22 | * @throws \Magento\Framework\Exception\PaymentException 23 | * @throws \Magento\Payment\Gateway\Command\CommandException 24 | */ 25 | public function execute(float $amountToCapture, \Magento\Payment\Model\InfoInterface $payment): void 26 | { 27 | if ($amountToCapture > 0) { 28 | $this->authCaptureCommand->execute([ 29 | 'amount' => $amountToCapture, 30 | 'payment' => $this->paymentDataObjectFactory->create($payment) 31 | ]); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Api/CheckoutManagementInterface.php: -------------------------------------------------------------------------------- 1 | config = $config; 14 | } 15 | 16 | public function check(string $currencyCode, ?float $amount = null): bool 17 | { 18 | $cbtCurrencies = $this->config->getCbtCurrencyLimits(); 19 | 20 | if ($amount === null) { 21 | return key_exists($currencyCode, $cbtCurrencies); 22 | } 23 | 24 | return key_exists($currencyCode, $cbtCurrencies) && $cbtCurrencies[$currencyCode] > $amount; 25 | } 26 | 27 | public function checkByQuote(\Magento\Quote\Model\Quote $quote): bool 28 | { 29 | if ($this->canUseCurrentCurrency !== null) { 30 | return $this->canUseCurrentCurrency; 31 | } 32 | 33 | $currentCurrencyCode = $quote->getQuoteCurrencyCode() ?? $quote->getStore()->getCurrentCurrencyCode(); 34 | $amount = (float) $quote->getGrandTotal(); 35 | $this->canUseCurrentCurrency = $this->check($currentCurrencyCode, $amount); 36 | 37 | return $this->canUseCurrentCurrency; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Model/ResourceModel/Token.php: -------------------------------------------------------------------------------- 1 | _init('afterpay_tokens_log', TokenInterface::LOG_ID_FIELD); 16 | $this->_useIsObjectNew = true; 17 | } 18 | 19 | public function selectByToken(string $token): string 20 | { 21 | $connection = $this->getConnection(); 22 | $select = $connection->select() 23 | ->from($this->getMainTable()) 24 | ->where(TokenInterface::TOKEN_FIELD . ' = ?', $token); 25 | 26 | $result = $connection->fetchOne($select); 27 | 28 | return is_string($result) ? $result : ''; 29 | } 30 | 31 | public function insertNewToken(int $orderId, string $token, ?string $expiryDate): int 32 | { 33 | return $this->getConnection()->insert( 34 | $this->getMainTable(), 35 | [ 36 | TokenInterface::ORDER_ID_FIELD => $orderId, 37 | TokenInterface::TOKEN_FIELD => $token, 38 | TokenInterface::EXPIRATION_DATE_FIELD => $expiryDate, 39 | ] 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /view/frontend/web/js/view/container/cta/cta.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'afterpayBaseContainer', 3 | 'ko', 4 | 'Magento_Catalog/js/price-utils', 5 | 'Afterpay_Afterpay/js/service/container/cta/modal-options-updater' 6 | ], function (Component, ko, priceUtils, modalOptionsUpdater) { 7 | 'use strict'; 8 | 9 | return Component.extend({ 10 | defaults: { 11 | dataIsEligible: "true", 12 | dataCbtEnabledString: "false", 13 | pageType: "product" 14 | }, 15 | initialize: function () { 16 | const res = this._super(); 17 | this.dataShowLowerLimit = this._getStringBool(this.dataShowLowerLimit); 18 | this.dataCbtEnabledString = this._getStringBool(this.dataCbtEnabled); 19 | return res; 20 | }, 21 | initObservable: function () { 22 | const res = this._super(); 23 | this.dataIsEligible = ko.computed(() => this._getStringBool(this.isProductAllowed())); 24 | this.dataAmount = ko.computed(() => this.isProductAllowed() ? priceUtils.formatPrice(this.currentPrice()) : ""); 25 | return res; 26 | }, 27 | onRendered: function () { 28 | if (this.id) { 29 | modalOptionsUpdater(this.id, { 30 | locale: this.dataLocale, 31 | cbtEnabled: this.dataCbtEnabled 32 | }); 33 | } 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/Button/LimitUpdate.php: -------------------------------------------------------------------------------- 1 | unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue(); 15 | return parent::render($element); 16 | } 17 | 18 | /** @inheritDoc */ 19 | protected function _prepareLayout() 20 | { 21 | parent::_prepareLayout(); 22 | 23 | if (!$this->getTemplate()) { 24 | $this->setTemplate(static::TEMPLATE); 25 | } 26 | 27 | return $this; 28 | } 29 | 30 | /** @inheritDoc */ 31 | public function _getElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element) 32 | { 33 | $originalData = $element->getData('original_data'); 34 | $this->addData( 35 | [ 36 | 'button_label' => __($originalData['button_label']), 37 | 'button_url' => $this->getUrl($originalData['button_url']), 38 | 'html_id' => $element->getHtmlId(), 39 | ] 40 | ); 41 | return $this->_toHtml(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Gateway/Validator/Method/NotAllowedProductsValidator.php: -------------------------------------------------------------------------------- 1 | notAllowedProductsProvider = $notAllowedProductsProvider; 15 | } 16 | 17 | public function validate(array $validationSubject): \Magento\Payment\Gateway\Validator\ResultInterface 18 | { 19 | /** @var \Magento\Quote\Model\Quote $quote */ 20 | $quote = $validationSubject['quote']; 21 | 22 | $disallowedProductsIds = $this->notAllowedProductsProvider->provideIds((int)$quote->getStoreId()); 23 | $disallowedProductsIdsAsKeys = array_flip($disallowedProductsIds); 24 | 25 | foreach ($quote->getItems() ?? [] as $item) { 26 | if (isset($disallowedProductsIdsAsKeys[(int)$item->getProductId()])) { 27 | return $this->createResult(false); 28 | } 29 | } 30 | return $this->createResult(true); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Model/Order/CreditMemo/PaymentUpdater.php: -------------------------------------------------------------------------------- 1 | getPaymentDataCommand = $getPaymentDataCommand; 17 | $this->paymentDataObjectFactory = $paymentDataObjectFactory; 18 | $this->orderPaymentRepository = $orderPaymentRepository; 19 | } 20 | 21 | public function updatePayment( 22 | \Magento\Payment\Model\InfoInterface $payment 23 | ): \Magento\Sales\Api\Data\OrderPaymentInterface { 24 | $this->getPaymentDataCommand->execute([ 25 | 'payment' => $this->paymentDataObjectFactory->create($payment) 26 | ]); 27 | /** @var \Magento\Sales\Api\Data\OrderPaymentInterface $payment */ 28 | $this->orderPaymentRepository->save($payment); 29 | return $payment; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Plugin/Block/Adminhtml/CustomerBalance/Order/Creditmemo/Controls.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 15 | $this->checkPaymentMethod = $checkPaymentMethod; 16 | } 17 | 18 | /** 19 | * @param \Magento\CustomerBalance\Block\Adminhtml\Sales\Order\Creditmemo\Controls $subject 20 | * @param callable $proceed 21 | * 22 | * @return false 23 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 24 | */ 25 | public function aroundCanRefundToCustomerBalance($subject, callable $proceed) 26 | { 27 | $creditmemo = $this->registry->registry('current_creditmemo'); 28 | if (!$creditmemo) { 29 | return $proceed(); 30 | } 31 | $payment = $creditmemo->getOrder()->getPayment(); 32 | if (!$payment) { 33 | return $proceed(); 34 | } 35 | if ($this->checkPaymentMethod->isAfterPayMethod($payment)) { 36 | return false; 37 | } 38 | return $proceed(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /etc/db_schema.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /view/frontend/templates/express-checkout/minicart-headless.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 7 | $jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/minicart/headless.js'); 8 | $externalJsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/minicart/express-checkout-minicart-widget.js'); 9 | ?> 10 | 11 | 16 | 17 | 18 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /Observer/AuthCaptureBeforeShipment.php: -------------------------------------------------------------------------------- 1 | shipmentCaptureProcessor = $shipmentCaptureProcessor; 15 | $this->checkPaymentMethod = $checkPaymentMethod; 16 | } 17 | 18 | /** 19 | * @throws \Magento\Framework\Exception\PaymentException 20 | * @throws \Magento\Payment\Gateway\Command\CommandException 21 | */ 22 | public function execute(\Magento\Framework\Event\Observer $observer): void 23 | { 24 | /** @var \Magento\Sales\Model\Order\Shipment $shipment */ 25 | $shipment = $observer->getEvent()->getData('shipment'); 26 | 27 | /** @var \Magento\Sales\Model\Order\Payment $paymentInfo */ 28 | $paymentInfo = $shipment->getOrder()->getPayment(); 29 | 30 | if (!$this->checkPaymentMethod->isAfterPayMethod($paymentInfo)) { 31 | return; 32 | } 33 | 34 | $this->shipmentCaptureProcessor->execute($shipment); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Api/Data/CheckoutInterface.php: -------------------------------------------------------------------------------- 1 | config = $config; 13 | } 14 | 15 | public function handle(array $handlingSubject, array $response): void 16 | { 17 | $websiteId = (int)$handlingSubject['websiteId']; 18 | $merchantCountry = $this->config->getMerchantCountry( 19 | \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITE, 20 | $websiteId 21 | ); 22 | $specificCountries = []; 23 | if ($merchantCountry != null) { 24 | $specificCountries[] = $merchantCountry; 25 | } 26 | $specificCountries = array_intersect($this->config->getAllowedCountries($websiteId), $specificCountries); 27 | if (isset($response['CBT']['enabled']) && 28 | isset($response['CBT']['countries']) && 29 | is_array($response['CBT']['countries']) && 30 | count($specificCountries) > 0 31 | ) { 32 | $specificCountries = array_unique(array_merge($specificCountries, $response['CBT']['countries'])); 33 | } 34 | $this->config->setSpecificCountries(implode(",", $specificCountries), $websiteId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Plugin/Quote/CheckoutManagement.php: -------------------------------------------------------------------------------- 1 | quoteRepository = $quoteRepository; 25 | $this->checkoutSession = $checkoutSession; 26 | $this->paymentMethodChecker = $paymentMethodChecker; 27 | } 28 | 29 | public function beforePlaceOrder(CartManagementInterface $subject, $cartId, ?PaymentInterface $paymentMethod = null) 30 | { 31 | $quote = $this->quoteRepository->getActive($cartId); 32 | $payment = $quote->getPayment(); 33 | if ($this->paymentMethodChecker->isAfterPayMethod($payment) && !$this->checkoutSession->getAfterpayRedirect()) { 34 | throw new LocalizedException(__('You cannot use the chosen payment method.')); 35 | } 36 | 37 | return [$cartId, $paymentMethod]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Gateway/Request/PaymentAction/CaptureDataBuilder.php: -------------------------------------------------------------------------------- 1 | getPayment(); 11 | /** @var \Magento\Sales\Api\Data\OrderInterface $order */ 12 | $order = $payment->getOrder(); 13 | 14 | $isCBTCurrency = (bool) $payment->getAdditionalInformation( 15 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY 16 | ); 17 | $cbtCurrency = $payment->getAdditionalInformation( 18 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_CBT_CURRENCY 19 | ); 20 | $currencyCode = ($isCBTCurrency && $cbtCurrency) ? $cbtCurrency : $order->getBaseCurrencyCode(); 21 | 22 | $token = $payment->getAdditionalInformation(\Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_TOKEN); 23 | $data = [ 24 | 'storeId' => $order->getStoreId(), 25 | 'token' => $token 26 | ]; 27 | 28 | if ($payment->getAdditionalInformation('afterpay_express')) { 29 | $data['amount'] = [ 30 | 'amount' => \Magento\Payment\Gateway\Helper\SubjectReader::readAmount($buildSubject), 31 | 'currency' => $currencyCode 32 | ]; 33 | } 34 | 35 | return $data; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Setup/Patch/Data/AfterpayPendingExceptionReviewOrderStatus.php: -------------------------------------------------------------------------------- 1 | statusFactory = $statusFactory; 19 | $this->statusResource = $statusResource; 20 | } 21 | 22 | public function getAliases(): array 23 | { 24 | return []; 25 | } 26 | 27 | public static function getDependencies(): array 28 | { 29 | return []; 30 | } 31 | 32 | public function apply(): self 33 | { 34 | $status = $this->statusFactory->create(); 35 | $status->setData([ 36 | 'status' => \Afterpay\Afterpay\Model\Payment\PaymentErrorProcessor::ORDER_STATUS_CODE, 37 | 'label' => 'Pending Exception Review (Afterpay)', 38 | ]); 39 | 40 | try { 41 | $this->statusResource->save($status); 42 | } catch (AlreadyExistsException $exception) { 43 | return $this; 44 | } 45 | 46 | $status->assignState('processing', false, false); 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Gateway/Request/PaymentAction/AuthCaptureDataBuilder.php: -------------------------------------------------------------------------------- 1 | getPayment(); 17 | 18 | $isCBTCurrency = (bool) $payment->getAdditionalInformation( 19 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY 20 | ); 21 | $cbtCurrency = $payment->getAdditionalInformation( 22 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_CBT_CURRENCY 23 | ); 24 | $currencyCode = ($isCBTCurrency && $cbtCurrency) ? $cbtCurrency : $paymentDO->getOrder()->getCurrencyCode(); 25 | 26 | $afterpayOrderId = $payment->getAdditionalInformation( 27 | \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_ORDER_ID 28 | ); 29 | 30 | return [ 31 | 'storeId' => $paymentDO->getOrder()->getStoreId(), 32 | 'orderId' => $afterpayOrderId, 33 | 'amount' => [ 34 | 'amount' => $this->formatPrice(SubjectReader::readAmount($buildSubject)), 35 | 'currency' => $currencyCode 36 | ] 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Plugin/QuoteGraphQl/Cart/PlaceOrderPlugin.php: -------------------------------------------------------------------------------- 1 | placeOrderProcessor = $placeOrderProcessor; 22 | $this->validateCheckoutDataCommand = $validateCheckoutDataCommand; 23 | } 24 | 25 | public function aroundExecute( 26 | PlaceOrderModel $subject, 27 | callable $proceed, 28 | Quote $cart, 29 | string $maskedCartId, 30 | int $userId 31 | ): int { 32 | $payment = $cart->getPayment(); 33 | if ($payment->getMethod() === Config::CODE) { 34 | $afterpayOrderToken = $payment->getAdditionalInformation(CheckoutInterface::AFTERPAY_TOKEN); 35 | 36 | return $this->placeOrderProcessor->execute($cart, $this->validateCheckoutDataCommand, $afterpayOrderToken); 37 | } 38 | 39 | return $proceed($cart, $maskedCartId, $userId); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ViewModel/Container/Cta/Lib.php: -------------------------------------------------------------------------------- 1 | storeManager = $storeManager; 22 | parent::__construct($config, $libUrlProvider, $containerConfigPath); 23 | } 24 | 25 | public function getMinTotalValue(): ?string 26 | { 27 | $currencyCode = $this->storeManager->getStore()->getCurrentCurrencyCode(); 28 | $cbtLimits = $this->config->getCbtCurrencyLimits(); 29 | if (isset($cbtLimits[$currencyCode])) { 30 | return $cbtLimits[$currencyCode]['minimumAmount']['amount'] ?? '0'; 31 | } 32 | 33 | return $this->config->getMinOrderTotal(); 34 | } 35 | 36 | public function getMaxTotalValue(): ?string 37 | { 38 | $currencyCode = $this->storeManager->getStore()->getCurrentCurrencyCode(); 39 | $cbtLimits = $this->config->getCbtCurrencyLimits(); 40 | if (isset($cbtLimits[$currencyCode])) { 41 | return $cbtLimits[$currencyCode]['maximumAmount']['amount']; 42 | } 43 | 44 | return $this->config->getMaxOrderTotal(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ViewModel/Container/Lib.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | $this->libUrlProvider = $libUrlProvider; 20 | $this->containerConfigPath = $containerConfigPath; 21 | } 22 | 23 | public function isContainerEnable(): bool 24 | { 25 | $isContainerConfigPathEnabled = true; 26 | if ($this->containerConfigPath !== null) { 27 | $isContainerConfigPathEnabled = (bool)$this->config->getByConfigPath($this->containerConfigPath); 28 | } 29 | return $isContainerConfigPathEnabled && 30 | $this->config->getIsPaymentActive() && 31 | $this->config->getMinOrderTotal() !== null && 32 | $this->config->getMaxOrderTotal() !== null && 33 | in_array($this->config->getMerchantCountry(), $this->config->getSpecificCountries()); 34 | } 35 | 36 | public function getIsLibLoadedAlready(): bool 37 | { 38 | return $this->libUrlProvider->getIsLibGotten(); 39 | } 40 | 41 | public function getAfterpayLib(): ?string 42 | { 43 | return $this->libUrlProvider->getAfterpayLib(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Gateway/Request/PaymentAction/RefundAndVoidDataBuilder.php: -------------------------------------------------------------------------------- 1 | getPayment()->getAdditionalInformation( 16 | \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_ORDER_ID 17 | ); 18 | 19 | $isCBTCurrency = (bool) $paymentDO->getPayment()->getAdditionalInformation( 20 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY 21 | ); 22 | $CBTCurrency = $paymentDO->getPayment()->getAdditionalInformation( 23 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_CBT_CURRENCY 24 | ); 25 | $currencyCode = ($isCBTCurrency && $CBTCurrency) ? $CBTCurrency : $paymentDO->getOrder()->getCurrencyCode(); 26 | 27 | $data = [ 28 | 'storeId' => $paymentDO->getOrder()->getStoreId(), 29 | 'orderId' => $afterpayOrderId 30 | ]; 31 | try { 32 | return array_merge($data, [ 33 | 'amount' => [ 34 | 'amount' => $this->formatPrice(SubjectReader::readAmount($buildSubject)), 35 | 'currency' => $currencyCode 36 | ] 37 | ]); 38 | } catch (\InvalidArgumentException $e) { 39 | return $data; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Plugin/Sales/Model/Service/CreditmemoService/AdjustmentAmountValidation.php: -------------------------------------------------------------------------------- 1 | getOrder(); 25 | if (($creditmemo->getBaseAdjustmentPositive() != 0 || $creditmemo->getBaseAdjustmentNegative() != 0) 26 | && $order->getPayment()->getMethod() === Config::CODE 27 | && !in_array( 28 | $order->getPayment()->getAdditionalInformation(AdditionalInformationInterface::AFTERPAY_PAYMENT_STATE), 29 | self::ALLOWED_PAYMENT_STATES 30 | )) { 31 | throw new LocalizedException(__( 32 | 'You cannot use adjustments for a payment with a status' 33 | . ' that does not equal "CAPTURED" or "PARTIALLY_CAPTURED" for the current payment method.' 34 | )); 35 | } 36 | 37 | return [$creditmemo, $offlineRequested]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Model/Url/UrlBuilder/UrlFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 17 | $this->storeManager = $storeManager; 18 | $this->environments = $environments; 19 | } 20 | 21 | public function create(string $type, ?int $storeId = null, array $pathArgs = []): string 22 | { 23 | $apiMode = $this->config->getApiMode($pathArgs['websiteId'] ?? null); 24 | $item = $this->environments[$apiMode][$type] ?? false; 25 | 26 | if (!$item) { 27 | throw new \InvalidArgumentException('Afterpay environment url config is not found'); 28 | } 29 | if (is_string($item)) { 30 | return $item; 31 | } 32 | 33 | if (isset($pathArgs['websiteId'])) { 34 | $currencyCode = $this->config->getMerchantCurrency( 35 | \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, 36 | $pathArgs['websiteId'] 37 | ); 38 | } 39 | if (!isset($currencyCode)) { 40 | /** @var \Magento\Store\Model\Store $store */ 41 | $store = $this->storeManager->getStore($storeId); 42 | $currencyCode = $store->getCurrentCurrencyCode(); 43 | } 44 | 45 | return $item[$currencyCode] ?? $item['default']; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Model/CheckoutConfigProvider.php: -------------------------------------------------------------------------------- 1 | localeResolver = $localeResolver; 21 | $this->checkoutSession = $checkoutSession; 22 | $this->checkCBTCurrencyAvailability = $checkCBTCurrencyAvailability; 23 | $this->config = $config; 24 | } 25 | 26 | public function getConfig(): array 27 | { 28 | $quote = $this->checkoutSession->getQuote(); 29 | 30 | return [ 31 | 'payment' => [ 32 | 'afterpay' => [ 33 | 'locale' => $this->localeResolver->getLocale(), 34 | 'isCBTCurrency' => $this->checkCBTCurrencyAvailability->checkByQuote($quote), 35 | 'consumerLendingEnabled' => $this->config->getConsumerLendingEnabled(), 36 | 'consumerLendingMinimumAmount' => $this->config->getConsumerLendingMinAmount(), 37 | 'mpid' => $this->config->getPublicId(), 38 | ] 39 | ] 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /view/frontend/templates/express-checkout/cart-headless.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 7 | $jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/cart/headless.js'); 8 | $externalJsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/cart/express-checkout-cart-widget.js'); 9 | ?> 10 | 11 | 22 | 23 | 24 | 25 | 26 | 35 | -------------------------------------------------------------------------------- /Gateway/Command/CommandPoolProxy.php: -------------------------------------------------------------------------------- 1 | commandPoolFactory = $commandPoolFactory; 19 | $this->config = $config; 20 | $this->commands = $commands; 21 | } 22 | 23 | protected function getCommandPool(): \Magento\Payment\Gateway\Command\CommandPoolInterface 24 | { 25 | if ($this->commandPool === null) { 26 | $paymentFlow = $this->config->getPaymentFlow(); 27 | 28 | $this->commands['capture'] = $paymentFlow == \Afterpay\Afterpay\Model\Config\Source\PaymentFlow::DEFERRED ? 29 | $this->commands['auth_deferred'] : 30 | $this->commands['capture_immediate']; 31 | 32 | unset($this->commands['capture_immediate']); 33 | unset($this->commands['auth_deferred']); 34 | 35 | $this->commandPool = $this->commandPoolFactory->create(['commands' => $this->commands]); 36 | } 37 | return $this->commandPool; 38 | } 39 | 40 | public function get($commandCode): \Magento\Payment\Gateway\CommandInterface 41 | { 42 | return $this->getCommandPool()->get($commandCode); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /view/frontend/templates/express-checkout/headless.phtml: -------------------------------------------------------------------------------- 1 | getViewModel(); 7 | $jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/product/headless.js'); 8 | $externalJsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/product/express-checkout-widget.js'); 9 | ?> 10 | 11 | 22 | 23 | 24 | 25 | 26 | 35 | -------------------------------------------------------------------------------- /Model/GraphQl/Resolver/AfterpayConfigMiniCart.php: -------------------------------------------------------------------------------- 1 | config->getIsEnableMiniCartHeadless($websiteId); 36 | $result['is_enabled_ec_minicart_headless'] = $this->config->getIsEnableMiniCartHeadless($websiteId); 37 | $result['placement_wrapper'] = $this->config->getMiniCartPlacementContainerSelector($websiteId); 38 | $result['placement_after_selector'] = $this->config->getMiniCartPlacementAfterSelector($websiteId); 39 | $result['price_selector'] = $this->config->getMiniCartPlacementPriceSelector($websiteId); 40 | 41 | return $result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /view/frontend/web/css/afterpay-checkout.less: -------------------------------------------------------------------------------- 1 | .afterpay.payment-method { 2 | .payment-method-note { 3 | &.afterpay-checkout-note { 4 | font-size: 1.2rem; 5 | color: #a0a0a0; 6 | text-align: center; 7 | } 8 | } 9 | .payment-method-content { 10 | overflow: hidden; 11 | padding-right: 22px; 12 | } 13 | .payment-icon { 14 | height: 40px; 15 | width: auto; 16 | } 17 | .actions-toolbar-wraper { 18 | margin-top: 1rem; 19 | text-align: left; 20 | background-color: #f6f7f9; 21 | display: inline-block; 22 | padding: 15px; 23 | .terms-conditions { 24 | width: 65%; 25 | float: left; 26 | } 27 | .actions-toolbar { 28 | float: right; 29 | .action.primary.checkout { 30 | line-height: 2.2rem; 31 | padding: 14px 17px; 32 | font-size: 1.8rem; 33 | } 34 | } 35 | } 36 | .afterpay-green { 37 | background: #00d632; 38 | border: #00d632; 39 | } 40 | } 41 | 42 | @media only screen and (max-width : 1150px) { 43 | .afterpay.payment-method { 44 | .actions-toolbar-wraper { 45 | .terms-conditions { 46 | width: 57%; 47 | } 48 | } 49 | } 50 | } 51 | 52 | @media only screen and (max-width : 993px) { 53 | .afterpay.payment-method { 54 | .actions-toolbar-wraper { 55 | .terms-conditions { 56 | width: 100%; 57 | } 58 | .actions-toolbar { 59 | width: 100%; 60 | margin-top: 1.5rem; 61 | .primary, .action.primary.checkout { 62 | width: 100%; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Gateway/Request/ExpressCheckoutDataBuilder.php: -------------------------------------------------------------------------------- 1 | getQuoteCurrencyCode(); 12 | $isCBTCurrencyAvailable = $this->checkCBTCurrencyAvailability->checkByQuote($quote); 13 | $amount = $isCBTCurrencyAvailable ? $quote->getGrandTotal() : $quote->getBaseGrandTotal(); 14 | $currencyCode = $isCBTCurrencyAvailable ? $currentCurrencyCode : $quote->getBaseCurrencyCode(); 15 | $popupOriginUrl = $buildSubject['popup_origin_url']; 16 | 17 | $lastSelectedShippingRate = null; 18 | if ($quote->getShippingAddress() && $quote->getShippingAddress()->getShippingMethod()) { 19 | $lastSelectedShippingRate = $quote->getShippingAddress()->getShippingMethod(); 20 | } 21 | 22 | $data = [ 23 | 'mode' => 'express', 24 | 'storeId' => $quote->getStoreId(), 25 | 'amount' => [ 26 | 'amount' => $this->formatPrice($amount), 27 | 'currency' => $currencyCode 28 | ], 29 | 'merchant' => [ 30 | 'popupOriginUrl' => $popupOriginUrl 31 | ], 32 | 'items' => $this->getItems($quote), 33 | 'merchantReference' => $quote->getReservedOrderId(), 34 | 'shippingOptionIdentifier' => $lastSelectedShippingRate 35 | ]; 36 | 37 | if ($discounts = $this->getDiscounts($quote)) { 38 | $data['discounts'] = $discounts; 39 | } 40 | 41 | return $data; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Model/Log/Method/Logger.php: -------------------------------------------------------------------------------- 1 | forceDebug = $forceDebug; 14 | return $this; 15 | } 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | public function debug(array $data, ?array $maskKeys = null, $forceDebug = null) 21 | { 22 | if ($forceDebug === null) { 23 | $forceDebug = $this->forceDebug ? true : null; 24 | } 25 | parent::debug($data, $maskKeys, $forceDebug); 26 | } 27 | 28 | protected function filterDebugData( 29 | array $debugData, 30 | array $debugReplacePrivateDataKeys, 31 | bool $maskAll = false 32 | ): array { 33 | $debugReplacePrivateDataKeys = array_map('strtolower', $debugReplacePrivateDataKeys); 34 | 35 | foreach (array_keys($debugData) as $key) { 36 | $isKeyToReplace = in_array(strtolower((string)$key), $debugReplacePrivateDataKeys); 37 | $isMasked = !is_array($debugData[$key]) && ($isKeyToReplace || $maskAll); 38 | if ($isMasked) { 39 | $debugData[$key] = self::DEBUG_KEYS_MASK; 40 | } elseif ($isKeyToReplace && is_array($debugData[$key])) { 41 | $debugData[$key] = $this->filterDebugData( 42 | $debugData[$key], 43 | $debugReplacePrivateDataKeys, 44 | true 45 | ); 46 | } elseif (is_array($debugData[$key])) { 47 | $debugData[$key] = $this->filterDebugData($debugData[$key], $debugReplacePrivateDataKeys); 48 | } 49 | } 50 | return $debugData; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Cron/MerchantConfigurationUpdater.php: -------------------------------------------------------------------------------- 1 | merchantConfigurationCommand = $merchantConfigurationCommand; 23 | $this->storeManager = $storeManager; 24 | $this->config = $config; 25 | $this->typeList = $typeList; 26 | $this->logger = $logger; 27 | } 28 | 29 | public function execute(): void 30 | { 31 | $websites = $this->storeManager->getWebsites(true); 32 | foreach ($websites as $website) { 33 | $websiteId = (int)$website->getId(); 34 | if ($this->config->getIsPaymentActive($websiteId)) { 35 | try { 36 | $this->merchantConfigurationCommand->execute([ 37 | 'websiteId' => $websiteId 38 | ]); 39 | $this->typeList->cleanType(\Magento\PageCache\Model\Cache\Type::TYPE_IDENTIFIER); 40 | } catch (\Exception $e) { 41 | $this->logger->critical($e->getMessage()); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /view/frontend/web/js/view/container/cta/pdp/cta.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'afterpayBaseContainer', 3 | 'ko', 4 | 'Magento_Catalog/js/price-utils', 5 | 'Afterpay_Afterpay/js/service/container/cta/modal-options-updater' 6 | ], function (Component, ko, priceUtils, modalOptionsUpdater) { 7 | 'use strict'; 8 | 9 | return Component.extend({ 10 | defaults: { 11 | dataIsEligible: "true", 12 | dataCbtEnabledString: "false", 13 | pageType: "product", 14 | dataAmountSelector: ".product-info-main .price-final_price .price-wrapper .price", 15 | dataAmountSelectorBundle: "#bundleSummary .price-as-configured .price" 16 | }, 17 | initialize: function () { 18 | const res = this._super(); 19 | if (!!document.querySelector(this.dataAmountSelectorBundle)) { 20 | this.dataAmountSelector = this.dataAmountSelectorBundle; 21 | } 22 | this.dataShowLowerLimit = this._getStringBool(this.dataShowLowerLimit); 23 | this.dataCbtEnabledString = this._getStringBool(this.dataCbtEnabled); 24 | return res; 25 | }, 26 | initObservable: function () { 27 | const res = this._super(); 28 | this.dataIsEligible = ko.computed(() => this._getStringBool(this.isProductAllowed())); 29 | this.dataAmount = ko.computed(() => this.isProductAllowed() ? priceUtils.formatPrice(this.currentPrice()) : ""); 30 | return res; 31 | }, 32 | _getIsVisible: function () { 33 | return true; 34 | }, 35 | onRendered: function () { 36 | if (this.id) { 37 | modalOptionsUpdater(this.id, { 38 | locale: this.dataLocale, 39 | cbtEnabled: this.dataCbtEnabled 40 | }); 41 | } 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /Setup/Patch/Data/UpdateCbtInfoPatch.php: -------------------------------------------------------------------------------- 1 | merchantConfigurationCommand = $merchantConfigurationCommand; 25 | $this->storeManager = $storeManager; 26 | $this->typeList = $typeList; 27 | $this->logger = $logger; 28 | } 29 | 30 | public function getAliases(): array 31 | { 32 | return []; 33 | } 34 | 35 | public static function getDependencies(): array 36 | { 37 | return []; 38 | } 39 | 40 | public function apply() 41 | { 42 | $websites = $this->storeManager->getWebsites(true); 43 | foreach ($websites as $website) { 44 | $websiteId = (int)$website->getId(); 45 | try { 46 | $this->merchantConfigurationCommand->execute([ 47 | 'websiteId' => $websiteId 48 | ]); 49 | $this->typeList->cleanType(\Magento\PageCache\Model\Cache\Type::TYPE_IDENTIFIER); 50 | } catch (\Exception $e) { 51 | $this->logger->critical($e); 52 | } 53 | } 54 | 55 | return $this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /view/frontend/web/js/view/container/container.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'uiComponent', 3 | 'ko', 4 | 'Afterpay_Afterpay/js/model/container/container-model-holder', 5 | ], function (Component, ko, containerModelHolder) { 6 | 'use strict'; 7 | 8 | return Component.extend({ 9 | defaults: { 10 | currentProductsModelId: "none", 11 | modelContainerId: "none", 12 | currentPrice: 0, 13 | isProductAllowed: "true", 14 | isVisible: false, 15 | notAllowedProducts: [] 16 | }, 17 | initialize: function () { 18 | const res = this._super(); 19 | this.dataShowLowerLimit = this._getStringBool(this.dataShowLowerLimit); 20 | return res; 21 | }, 22 | initObservable: function () { 23 | const res = this._super(); 24 | this.containerModel = containerModelHolder.getModel(this.modelContainerId); 25 | this.isProductAllowed = ko.computed(this._getIsAllProductsAllowed.bind(this)); 26 | this.currentPrice = ko.computed(() => this.containerModel.getPrice()); 27 | this.isVisible = ko.computed(this._getIsVisible.bind(this)); 28 | return res; 29 | }, 30 | _getStringBool: function (value) { 31 | return value ? "true" : "false"; 32 | }, 33 | _getIsVisible: function () { 34 | return this.currentPrice() !== 0; 35 | }, 36 | _getIsAllProductsAllowed: function () { 37 | return this._getIsAllProductsInArrayAllowed(this.containerModel.getCurrentProductsIds()); 38 | }, 39 | _getIsAllProductsInArrayAllowed: function (array) { 40 | return array.reduce( 41 | (isAllowed, productId) => isAllowed && !this.notAllowedProducts.includes(productId.toString()), 42 | true 43 | ); 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /Setup/Patch/Data/UpdateMPIDInfoPatch.php: -------------------------------------------------------------------------------- 1 | merchantConfigurationCommand = $merchantConfigurationCommand; 25 | $this->storeManager = $storeManager; 26 | $this->typeList = $typeList; 27 | $this->logger = $logger; 28 | } 29 | 30 | public function getAliases(): array 31 | { 32 | return []; 33 | } 34 | 35 | public static function getDependencies(): array 36 | { 37 | return []; 38 | } 39 | 40 | public function apply() 41 | { 42 | $websites = $this->storeManager->getWebsites(true); 43 | foreach ($websites as $website) { 44 | $websiteId = (int)$website->getId(); 45 | try { 46 | $this->merchantConfigurationCommand->execute([ 47 | 'websiteId' => $websiteId 48 | ]); 49 | $this->typeList->cleanType(\Magento\PageCache\Model\Cache\Type::TYPE_IDENTIFIER); 50 | } catch (\Exception $e) { 51 | $this->logger->critical($e); 52 | } 53 | } 54 | 55 | return $this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /view/frontend/web/js/service/container/cart/abstract-data-retriever.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'uiComponent', 3 | 'ko', 4 | 'Afterpay_Afterpay/js/model/container/container-model-holder', 5 | 'Magento_Customer/js/customer-data' 6 | ], function (Component, ko, containerModelHolder, customerData) { 7 | return Component.extend({ 8 | defaults: { 9 | modelContainerId: "none", 10 | isVisible: false, 11 | dataAmount: "0", 12 | cartModel: customerData.get('cart') 13 | }, 14 | initialize: function () { 15 | const res = this._super(); 16 | this._updateContainerModel(this.modelWithPrice()); 17 | this._cartUpdate(this.cartModel()); 18 | return res; 19 | }, 20 | _updateContainerModel: function (modelWithPrice) { 21 | const containerModel = containerModelHolder.getModel(this.modelContainerId); 22 | if (modelWithPrice[this.priceKey] && parseFloat(modelWithPrice[this.priceKey]) > 0) { 23 | containerModel.setPrice(modelWithPrice[this.priceKey]); 24 | } else { 25 | containerModel.setPrice(0); 26 | } 27 | }, 28 | _cartUpdate: function (newCart) { 29 | const containerModel = containerModelHolder.getModel(this.modelContainerId); 30 | if (newCart && Array.isArray(newCart.items)) { 31 | containerModel.setCurrentProductsIds(newCart.items.map((item) => item.product_id)); 32 | containerModel.setIsVirtual(newCart.items.reduce((isVirtual, item) => isVirtual && item.is_virtual, true)); 33 | } 34 | }, 35 | initObservable: function () { 36 | const res = this._super(); 37 | this.modelWithPrice.subscribe(this._updateContainerModel, this) 38 | this.cartModel.subscribe(this._cartUpdate, this) 39 | return res; 40 | } 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /view/frontend/web/js/service/container/pricebox-widget-mixin.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'jquery', 3 | "Afterpay_Afterpay/js/model/container/container-model-holder" 4 | ], function ($, containerModelHolder) { 5 | 'use strict'; 6 | const containerModel = containerModelHolder.getModel("afterpay-pdp-container"); 7 | const priceBoxWidget = { 8 | _checkIsFinalPriceDefined: function () { 9 | return !!( 10 | this.cache.displayPrices && 11 | this.cache.displayPrices.finalPrice && 12 | this.cache.displayPrices.finalPrice.formatted 13 | ); 14 | }, 15 | updatePrice: function (newPrices) { 16 | const res = this._super(newPrices); 17 | let amountSelector = ".product-info-main .price-final_price .price-wrapper .price"; 18 | const amountSelectorBundle = "#bundleSummary .price-as-configured .price"; 19 | let price = 0; 20 | if (this._checkIsFinalPriceDefined()) { 21 | price = this.cache.displayPrices.finalPrice.amount; 22 | } else { 23 | if (!!document.querySelector(amountSelectorBundle)) { 24 | amountSelector = amountSelectorBundle; 25 | } 26 | let priceData = $(amountSelector).first().text(); 27 | price = parseFloat(priceData.replace(/[^0-9.]/g, '')); 28 | } 29 | 30 | if (this.element.closest('.product-info-main').length > 0 || 31 | this.element.closest('.bundle-options-container').length > 0) { 32 | containerModel.setCurrentProductId(this.element.data('productId')); 33 | containerModel.setPrice(price); 34 | } 35 | 36 | return res; 37 | } 38 | }; 39 | return function (targetWidget) { 40 | $.widget('mage.priceBox', targetWidget, priceBoxWidget); 41 | 42 | return $.mage.priceBox; 43 | }; 44 | }); 45 | -------------------------------------------------------------------------------- /ViewModel/Container/Cta/Cta.php: -------------------------------------------------------------------------------- 1 | localeResolver = $localeResolver; 20 | } 21 | 22 | public function updateJsLayout( 23 | string $jsLayoutJson, 24 | bool $remove = false, 25 | string $containerNodeName = 'afterpay.cta', 26 | array $config = [] 27 | ): string { 28 | if (!$remove && $this->isContainerEnable()) { 29 | $store = $this->storeManager->getStore(); 30 | $config['dataCurrency'] = $store->getCurrentCurrencyCode(); 31 | $config['dataLocale'] = $this->localeResolver->getLocale(); 32 | $config['dataShowLowerLimit'] = $this->config->getMinOrderTotal() >= 1; 33 | $config['dataCbtEnabled'] = count($this->config->getSpecificCountries()) > 1; 34 | $config['dataMPID'] = $this->config->getPublicId(); 35 | $config['dataPlatform'] = 'Magento'; 36 | $config['dataAmountSelector'] = $this->config->getPdpPlacementPriceSelector(); 37 | $config['dataAmountSelectorBundle'] = $this->config->getPdpPlacementPriceSelectorBundle(); 38 | } 39 | return parent::updateJsLayout($jsLayoutJson, $remove, $containerNodeName, $config); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/Fieldset/AllowedByCountry.php: -------------------------------------------------------------------------------- 1 | afterpay = $afterpay; 23 | $this->config = $config; 24 | $this->allowedCountriesConfigPath = $allowedCountriesConfigPath; 25 | } 26 | 27 | public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element): string 28 | { 29 | $allowedMerchantCountries = explode(',', $this->afterpay->getConfigData($this->allowedCountriesConfigPath)); 30 | if (in_array($this->getMerchantCountry(), $allowedMerchantCountries)) { 31 | return parent::render($element); 32 | } 33 | return ''; 34 | } 35 | 36 | private function getMerchantCountry(): ?string 37 | { 38 | /** @var \Magento\Config\Block\System\Config\Form $fieldSetForm */ 39 | $fieldSetForm = $this->getForm(); 40 | $scope = $fieldSetForm->getScope(); 41 | $scopeCode = $fieldSetForm->getScopeCode(); 42 | 43 | return $this->config->getMerchantCountry($scope, (int)$scopeCode); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /view/frontend/web/css/afterpay-express-checkout.less: -------------------------------------------------------------------------------- 1 | .afterpay-express-button, button.afterpay-express-button:hover { 2 | background-image: none; 3 | background: #000; 4 | border: 1px solid #000; 5 | color: #ffffff; 6 | cursor: pointer; 7 | display: inline-block; 8 | float: none; 9 | width: 267px; 10 | max-width: 100%; 11 | margin-top: 10px; 12 | 13 | } 14 | .afterpay-express-checkout-minicart-wraper { 15 | margin: 0 10px 15px; 16 | position: relative; 17 | z-index: 9; 18 | .afterpay-express-button, button.afterpay-express-button:hover { 19 | width: 100%; 20 | cursor: pointer; 21 | text-align: center; 22 | } 23 | } 24 | 25 | .headless-afterpay-ec .afterpay-express-button, 26 | .headless-afterpay-ec button.afterpay-express-button:hover, 27 | .headless-afterpay-ec .afterpay-express-button-minicart, 28 | .headless-afterpay-ec button.afterpay-express-button-minicart:hover { 29 | background-image: none; 30 | background: #000; 31 | border: 1px solid #000; 32 | color: #ffffff; 33 | cursor: pointer; 34 | display: inline-block; 35 | float: none; 36 | width: 267px; 37 | max-width: 100%; 38 | margin-top: 10px; 39 | } 40 | 41 | .headless-afterpay-ec .afterpay-express-button-minicart, 42 | .headless-afterpay-ec button.afterpay-express-button-minicart:hover { 43 | margin: 0 10px; 44 | width: 100%; 45 | max-width: 328px; 46 | padding: 2px 15px; 47 | } 48 | 49 | .headless-afterpay-ec .afterpay-express-button-cart, 50 | .headless-afterpay-ec button.afterpay-express-button-cart:hover { 51 | background: #000; 52 | border: none; 53 | } 54 | 55 | .headless-afterpay-ec.hidden { 56 | display: none; 57 | } 58 | 59 | .checkout-onepage-success .success.message { 60 | display: none; 61 | } 62 | 63 | .checkout-onepage-success .success.message.show-message { 64 | display: block; 65 | } 66 | 67 | .hyva_checkout-index-index #payment-method-option-afterpay { 68 | display: none; 69 | } 70 | -------------------------------------------------------------------------------- /Plugin/Checkout/Block/Cart/Sidebar.php: -------------------------------------------------------------------------------- 1 | ctaContainerViewModel = $ctaContainerViewModel; 19 | $this->config = $config; 20 | $this->expressCheckoutViewModel = $expressCheckoutViewModel; 21 | } 22 | 23 | /** 24 | * @param string $result 25 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 26 | */ 27 | public function afterGetJsLayout(\Magento\Checkout\Block\Cart\Sidebar $sidebar, $result): string 28 | { 29 | if (is_string($result) && 30 | $this->config->getIsPaymentActive() && 31 | $this->config->getMinOrderTotal() !== null && 32 | $this->config->getMaxOrderTotal() !== null 33 | ) { 34 | $result = $this->ctaContainerViewModel->updateJsLayout( 35 | $result, 36 | !($this->config->getIsEnableCtaMiniCart() 37 | && $this->ctaContainerViewModel->isContainerEnable() 38 | && !$this->config->getIsEnableMiniCartHeadless()) 39 | ); 40 | $result = $this->expressCheckoutViewModel->updateJsLayout( 41 | $result, 42 | !($this->config->getIsEnableExpressCheckoutMiniCart() && 43 | $this->expressCheckoutViewModel->isContainerEnable() 44 | && !$this->config->getIsEnableMiniCartHeadless()) 45 | ); 46 | } 47 | 48 | return $result; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ViewModel/Container/Cta/Headless.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 34 | $this->localeResolver = $localeResolver; 35 | $this->checkoutSession = $checkoutSession; 36 | } 37 | 38 | public function getProductSku(): string 39 | { 40 | return $this->registry->registry('current_product')->getSku(); 41 | } 42 | 43 | public function getStoreId(): string 44 | { 45 | return (string)$this->storeManager->getStore()->getId(); 46 | } 47 | 48 | public function getCurrency(): string 49 | { 50 | return $this->storeManager->getStore()->getCurrentCurrencyCode(); 51 | } 52 | 53 | public function getLocale(): string 54 | { 55 | return $this->localeResolver->getLocale(); 56 | } 57 | 58 | public function getCartId(): string 59 | { 60 | return (string)$this->checkoutSession->getQuoteId(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Controller/Adminhtml/MerchantConfiguration/Update.php: -------------------------------------------------------------------------------- 1 | resultJsonFactory = $resultJsonFactory; 19 | $this->messageManager = $messageManager; 20 | $this->request = $request; 21 | $this->merchantConfigurationCommand = $merchantConfigurationCommand; 22 | } 23 | 24 | public function execute() 25 | { 26 | $websiteId = $this->request->getParam('websiteId'); 27 | try { 28 | $this->merchantConfigurationCommand->execute( 29 | [ 30 | "websiteId" => (int)$websiteId 31 | ] 32 | ); 33 | $this->messageManager->addSuccessMessage( 34 | (string)__('Afterpay merchant configuration fetching is success.') 35 | ); 36 | } catch (\Magento\Payment\Gateway\Command\CommandException $e) { 37 | $this->messageManager->addWarningMessage($e->getMessage()); 38 | } catch (\Exception $e) { 39 | $this->messageManager->addErrorMessage( 40 | (string)__('Afterpay merchant configuration fetching is failed. See logs.') 41 | ); 42 | } 43 | return $this->resultJsonFactory->create()->setData([ 44 | 'done' => true 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Observer/AuthCaptureBeforeCompleteOrder.php: -------------------------------------------------------------------------------- 1 | authCaptureCommand = $authCaptureCommand; 26 | $this->paymentDataObjectFactory = $paymentDataObjectFactory; 27 | } 28 | 29 | /** 30 | * Observer that captures the remaining amount when the order status is complete. 31 | * 32 | * @throws PaymentException 33 | * @throws CommandException 34 | */ 35 | public function execute(Observer $observer): void 36 | { 37 | /** @var Order $order */ 38 | $order = $observer->getEvent()->getOrder(); 39 | $payment = $order->getPayment(); 40 | 41 | if (!$payment->getMethod() == Config::CODE || $order->getStatus() !== Order::STATE_COMPLETE) { 42 | return; 43 | } 44 | 45 | $openToCapture = $payment->getAdditionalInformation( 46 | AdditionalInformationInterface::AFTERPAY_OPEN_TO_CAPTURE_AMOUNT 47 | ); 48 | 49 | if ($openToCapture > 0) { 50 | $this->authCaptureCommand->execute([ 51 | 'amount' => $openToCapture, 52 | 'payment' => $this->paymentDataObjectFactory->create($payment) 53 | ]); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Gateway/Command/RefundAndVoidCommand.php: -------------------------------------------------------------------------------- 1 | refundCommand = $refundCommand; 19 | $this->voidCommand = $voidCommand; 20 | $this->creditMemoAmountProcessor = $creditMemoAmountProcessor; 21 | } 22 | 23 | public function execute(array $commandSubject): ?\Magento\Payment\Gateway\Command\ResultInterface 24 | { 25 | $paymentDO = \Magento\Payment\Gateway\Helper\SubjectReader::readPayment($commandSubject); 26 | 27 | /** @var \Magento\Sales\Model\Order\Payment $payment */ 28 | $payment = $paymentDO->getPayment(); 29 | 30 | [$amountToRefund, $amountToVoid] = $this->creditMemoAmountProcessor->process($payment); 31 | 32 | $refundCommandSubject = array_merge( 33 | $commandSubject, 34 | ['amount' => $amountToRefund] 35 | ); 36 | $voidCommandSubject = array_merge( 37 | $commandSubject, 38 | ['amount' => $amountToVoid] 39 | ); 40 | 41 | if (\Magento\Payment\Gateway\Helper\SubjectReader::readAmount($refundCommandSubject) > 0) { 42 | $this->refundCommand->execute($refundCommandSubject); 43 | } 44 | if (\Magento\Payment\Gateway\Helper\SubjectReader::readAmount($voidCommandSubject) > 0) { 45 | $this->voidCommand->execute($voidCommandSubject); 46 | } 47 | 48 | return null; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ViewModel/Container/ExpressCheckout/Headless.php: -------------------------------------------------------------------------------- 1 | checkoutSession = $checkoutSession; 32 | $this->registry = $registry; 33 | } 34 | 35 | public function getProductSku(): string 36 | { 37 | return $this->registry->registry('current_product')->getSku(); 38 | } 39 | 40 | public function getStoreId(): string 41 | { 42 | return (string)$this->storeManager->getStore()->getId(); 43 | } 44 | 45 | public function getCartId(): string 46 | { 47 | return (string)$this->checkoutSession->getQuoteId(); 48 | } 49 | 50 | public function getImageurl(): string 51 | { 52 | $urlPrefix = $this->config->getApiMode() === ApiMode::SANDBOX ? 'static.sandbox' : 'static'; 53 | $localePart = str_replace('_', '-', $this->localeResolver->getLocale()); 54 | 55 | return "https://$urlPrefix.afterpay.com/$localePart/integration/button/checkout-with-afterpay/white-on-black.svg"; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /view/frontend/web/js/view/payment/info/terms-and-conditions.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'uiComponent', 3 | 'Magento_Checkout/js/model/totals', 4 | 'mage/translate' 5 | ], function ( 6 | Component, 7 | totals, 8 | $t 9 | ) { 10 | 'use strict'; 11 | 12 | return Component.extend({ 13 | getTermsText: function () { 14 | let afterpayTermsText = ''; 15 | switch(totals.totals().quote_currency_code){ 16 | case 'USD': 17 | afterpayTermsText = $t('You will be redirected to the $Afterpay website to fill out your payment information. You will be redirected back to our site to complete your order.'); 18 | break 19 | case 'CAD': 20 | afterpayTermsText = $t('You will be redirected to the Afterpay website to fill out your payment information. You will be redirected back to our site to complete your order.'); 21 | break; 22 | default: 23 | afterpayTermsText = $t('You will be redirected to the Afterpay website when you proceed to checkout.'); 24 | } 25 | 26 | return afterpayTermsText; 27 | }, 28 | getTermsLink: function () { 29 | let afterpayCheckoutTermsLink = ''; 30 | switch(totals.totals().quote_currency_code){ 31 | case 'USD': 32 | afterpayCheckoutTermsLink = "https://www.afterpay.com/en-US/installment-agreement"; 33 | break; 34 | case 'CAD': 35 | afterpayCheckoutTermsLink = "https://www.afterpay.com/en-CA/instalment-agreement"; 36 | break; 37 | case 'NZD': 38 | afterpayCheckoutTermsLink="https://www.afterpay.com/en-NZ/terms-of-service"; 39 | break ; 40 | case 'AUD': 41 | afterpayCheckoutTermsLink="https://www.afterpay.com/en-AU/terms-of-service"; 42 | break ; 43 | default: 44 | afterpayCheckoutTermsLink = "https://www.afterpay.com/terms/"; 45 | } 46 | return afterpayCheckoutTermsLink; 47 | }, 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /Model/Order/CreditMemo/CreditMemoInitiator.php: -------------------------------------------------------------------------------- 1 | creditmemoFactory = $creditmemoFactory; 13 | } 14 | 15 | public function init(\Magento\Sales\Model\Order $order): \Magento\Sales\Model\Order\Creditmemo 16 | { 17 | $qtysToRefund = []; 18 | /** @var \Magento\Sales\Model\Order\Item $orderItem */ 19 | foreach ($order->getItemsCollection() as $orderItem) { 20 | if ($orderItem->getProductType() == \Magento\Bundle\Model\Product\Type::TYPE_CODE) { 21 | /** @var \Magento\Sales\Model\Order\Item $childrenItem */ 22 | foreach ($orderItem->getChildrenItems() as $childrenItem) { 23 | if (!$childrenItem->getIsVirtual()) { 24 | $qtyShipped = $childrenItem->getQtyShipped(); 25 | $qtyOrdered = $childrenItem->getQtyOrdered(); 26 | $qtyRefunded = $childrenItem->getQtyRefunded(); 27 | $childItemLeftToShip = $qtyOrdered - ($qtyShipped + $qtyRefunded); 28 | if ($childItemLeftToShip > 0) { 29 | $qtysToRefund[$childrenItem->getId()] = $childItemLeftToShip; 30 | } 31 | } 32 | } 33 | } 34 | 35 | if (!$orderItem->getParentItem() && !$orderItem->getIsVirtual()) { 36 | $qtyShipped = $orderItem->getQtyShipped(); 37 | $qtyOrdered = $orderItem->getQtyOrdered(); 38 | $qtyRefunded = $orderItem->getQtyRefunded(); 39 | $itemLeftToShip = $qtyOrdered - ($qtyShipped + $qtyRefunded); 40 | if ($itemLeftToShip > 0) { 41 | $qtysToRefund[$orderItem->getId()] = $itemLeftToShip; 42 | } 43 | } 44 | } 45 | return $this->creditmemoFactory->createByOrder($order, ['qtys' => $qtysToRefund]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Gateway/Http/TransferFactory/UserAgentProvider.php: -------------------------------------------------------------------------------- 1 | moduleList = $moduleList; 21 | $this->productMetadata = $productMetadata; 22 | $this->util = $util; 23 | $this->config = $config; 24 | $this->store = $store; 25 | } 26 | 27 | public function provide(?int $websiteId = null): string 28 | { 29 | $afterpayModule = $this->moduleList->getOne('Afterpay_Afterpay'); 30 | $moduleVersion = $afterpayModule['setup_version'] ?? null; 31 | $magentoProductName = $this->productMetadata->getName(); 32 | $magentoProductEdition = $this->productMetadata->getEdition(); 33 | $magentoVersion = $this->productMetadata->getVersion(); 34 | $phpVersion = $this->util->getTrimmedPhpVersion(); 35 | $afterpayMerchantId = $this->config->getMerchantId($websiteId); 36 | $publicId = $this->config->getPublicId($websiteId); 37 | $afterpayMPId=$publicId??"null"; 38 | $websiteDomain = $this->store->getBaseUrl(); 39 | $CashAppPayAvailable=(int)$this->config->getCashAppPayAvailable($websiteId); 40 | $CashAppPayEnabled=(int)$this->config->getCashAppPayEnabled($websiteId); 41 | 42 | return "AfterpayMagento2Plugin $moduleVersion ($magentoProductName $magentoProductEdition $magentoVersion) " . 43 | "PHPVersion: PHP/$phpVersion MerchantID: $afterpayMerchantId; MPID/$afterpayMPId; CAPAvailable/$CashAppPayAvailable; CAPEnabled/$CashAppPayEnabled; URL: $websiteDomain"; 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Gateway/Response/DiscountHandler.php: -------------------------------------------------------------------------------- 1 | getPayment(); 20 | 21 | $totalDiscount = $this->getOrderDiscountAmount($payment->getOrder()); 22 | $paymentState = $payment->getAdditionalInformation(AdditionalInformationInterface::AFTERPAY_PAYMENT_STATE); 23 | if ($paymentState == PaymentStateInterface::CAPTURED) { 24 | $rolloverDiscount = '0.00'; 25 | $capturedDiscount = $totalDiscount; 26 | } else { 27 | $rolloverDiscount = $totalDiscount; 28 | $capturedDiscount = '0.00'; 29 | } 30 | $payment->setAdditionalInformation( 31 | AdditionalInformationInterface::AFTERPAY_ROLLOVER_DISCOUNT, 32 | $rolloverDiscount 33 | ); 34 | $payment->setAdditionalInformation( 35 | AdditionalInformationInterface::AFTERPAY_CAPTURED_DISCOUNT, 36 | $capturedDiscount 37 | ); 38 | } 39 | 40 | protected function getOrderDiscountAmount(\Magento\Sales\Model\Order $order): float 41 | { 42 | $isCBTCurrency = (bool) $order->getPayment()->getAdditionalInformation( 43 | \Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY 44 | ); 45 | 46 | if ($isCBTCurrency) { 47 | return (float)($order->getGiftCardsAmount() + $order->getCustomerBalanceAmount() + $order->getRewardCurrencyAmount()); 48 | } else { 49 | return (float)($order->getBaseGiftCardsAmount() + $order->getBaseCustomerBalanceAmount() + $order->getBaseRewardCurrencyAmount()); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Gateway/Response/MerchantConfiguration/ChannelsConfigurationHandler.php: -------------------------------------------------------------------------------- 1 | config = $config; 18 | $this->logger = $logger; 19 | } 20 | 21 | public function handle(array $handlingSubject, array $response): void 22 | { 23 | $websiteId = (int)$handlingSubject['websiteId']; 24 | 25 | if (isset($response['channels']) && is_array($response['channels'])) { 26 | $getCashAppConfig = array_search("CASH_APP", array_column($response['channels'], 'name')); 27 | $isCashAppEnabled = 0; 28 | 29 | if ( isset($response['channels'][$getCashAppConfig]) && 30 | isset($response['channels'][$getCashAppConfig]['name']) && 31 | strtoupper($response['channels'][$getCashAppConfig]['name'])== "CASH_APP") { 32 | $cashappData=$response['channels'][$getCashAppConfig]; 33 | 34 | if (isset($cashappData['enabled']) && $cashappData['enabled']==true && 35 | isset($cashappData['integrationCompleted']) && $cashappData['integrationCompleted']==true && 36 | isset($cashappData['enabledForOrders']) && $cashappData['enabledForOrders']==true) { 37 | $isCashAppEnabled = (int)$cashappData['enabled']; 38 | } 39 | 40 | }else{ 41 | if( $this->config->getCashAppPayActive($websiteId)==true) { 42 | //Disable the Cash App Pay if it's not available for the Merchant 43 | $this->config->setCashAppPayActive($isCashAppEnabled, $websiteId); 44 | $this->logger->notice("Disable the Cash App Pay as it's not available for the Merchant"); 45 | } 46 | } 47 | $this->config->setCashAppPayAvailable($isCashAppEnabled, $websiteId); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Test/Unit/Service/Payment/Auth/ExpiryDateTest.php: -------------------------------------------------------------------------------- 1 | timezone = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) 17 | ->disableOriginalConstructor() 18 | ->getMock(); 19 | $this->expiryDate = new ExpiryDate($this->timezone); 20 | } 21 | 22 | /** 23 | * @dataProvider datesProvider 24 | */ 25 | public function testIsExpired(string $expireDate, ?string $dateToCheck, bool $result) 26 | { 27 | if (!$dateToCheck) { 28 | $this->timezone->expects($this->once())->method("date")->willReturn(new \DateTime()); 29 | } 30 | $this->assertEquals($this->expiryDate->isExpired($expireDate, $dateToCheck), $result); 31 | } 32 | 33 | public function datesProvider(): array 34 | { 35 | return [ 36 | ["2021-08-10 09:31 CDT", "2021-08-10 09:31 CDT", false], 37 | ["2021-08-10 09:31 CDT", "2021-08-10 09:32 CDT", true], 38 | ["2021-08-10 09:31 CDT", "2024-11-11 09:32 CDT", true], 39 | ["2021-08-10 09:31 CDT", "2024-01-11 09:32 CDT", true], 40 | ["2021-07-05 09:31 CDT", "2020-01-11 09:32 CDT", false], 41 | [(new \DateTime())->format('Y-m-d H:i T'), null, false], 42 | [(new \DateTime())->modify('-1 minute')->format(ExpiryDate::FORMAT), null, true], 43 | [(new \DateTime())->modify('-1 days')->format(ExpiryDate::FORMAT), null, true], 44 | [(new \DateTime())->modify('-1 week')->format(ExpiryDate::FORMAT), null, true], 45 | [(new \DateTime())->modify('-1 year +2 days -3 minutes')->format(ExpiryDate::FORMAT), null, true], 46 | [(new \DateTime())->modify('+1 year +2 days -3 minutes')->format(ExpiryDate::FORMAT), null, false], 47 | [(new \DateTime())->modify('+1 day')->format(ExpiryDate::FORMAT), null, false], 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Model/Payment/AmountProcessor/VirtualProducts.php: -------------------------------------------------------------------------------- 1 | getAdditionalInformation( 10 | \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_CAPTURED_DISCOUNT 11 | ) ?? 0; 12 | $totalDiscountAmount = $payment->getAdditionalInformation( 13 | \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_ROLLOVER_DISCOUNT 14 | ) ?? 0; 15 | 16 | $isCBTCurrency = $payment->getAdditionalInformation(\Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY); 17 | $orderTotal = $isCBTCurrency ? $payment->getOrder()->getGrandTotal() : $payment->getOrder()->getBaseGrandTotal(); 18 | $totalWithoutVirtualProducts = $orderTotal - $amount; 19 | $returnAmount = $amount; 20 | 21 | if ($amount > $totalDiscountAmount) { 22 | $returnAmount -= $totalDiscountAmount; 23 | $capturedDiscount += $totalDiscountAmount; 24 | $totalDiscountAmount = '0.00'; 25 | } else { 26 | if ($totalWithoutVirtualProducts < $totalDiscountAmount) { 27 | $returnAmount = $orderTotal; 28 | } 29 | 30 | $openToCapture = $payment->getAdditionalInformation( 31 | \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_OPEN_TO_CAPTURE_AMOUNT 32 | ); 33 | if ($openToCapture && $openToCapture == $returnAmount) { 34 | $capturedDiscount += $totalDiscountAmount; 35 | $totalDiscountAmount = '0.00'; 36 | } 37 | } 38 | 39 | $payment->setAdditionalInformation( 40 | \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_ROLLOVER_DISCOUNT, 41 | (string)$totalDiscountAmount 42 | ); 43 | $payment->setAdditionalInformation( 44 | \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_CAPTURED_DISCOUNT, 45 | $capturedDiscount 46 | ); 47 | 48 | return $returnAmount; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Controller/Express/CreateCheckout.php: -------------------------------------------------------------------------------- 1 | checkoutManagement = $checkoutManagement; 25 | $this->checkoutSession = $checkoutSession; 26 | $this->url = $url; 27 | $this->jsonResultFactory = $jsonResultFactory; 28 | $this->messageManager = $messageManager; 29 | $this->logger = $logger; 30 | } 31 | 32 | public function execute(): \Magento\Framework\Controller\ResultInterface 33 | { 34 | $result = $this->jsonResultFactory->create(); 35 | try { 36 | $checkout = $this->checkoutManagement->createExpress( 37 | (string)$this->checkoutSession->getQuoteId(), 38 | $this->url->getUrl('checkout/cart') 39 | ); 40 | $result->setData([ 41 | CheckoutInterface::AFTERPAY_TOKEN => $checkout->getAfterpayToken() 42 | ]); 43 | } catch (\Magento\Framework\Exception\LocalizedException $e) { 44 | $this->messageManager->addErrorMessage($e->getMessage()); 45 | } catch (\Throwable $e) { 46 | $this->logger->error($e->getMessage()); 47 | $this->messageManager->addErrorMessage((string)__('Afterpay payment is declined. Please select an alternative payment method.')); 48 | } 49 | return $result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /etc/adminhtml/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Afterpay\Afterpay\Gateway\Config\Config 7 | 8 | 9 | 10 | 11 | Afterpay\Afterpay\Model\Method\MethodFacade 12 | 13 | 14 | 15 | 16 | allowed_merchant_countries 17 | 18 | 19 | 20 | 21 | expresscheckout/allowed_merchant_countries 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Block/Adminhtml/System/Config/Form/Field/CBTAvailableCurrencies.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 21 | $this->logger = $logger; 22 | parent::__construct($context, $data); 23 | } 24 | 25 | protected function _renderValue(\Magento\Framework\Data\Form\Element\AbstractElement $element) 26 | { 27 | try { 28 | if (!empty($element->getValue())) { 29 | $CbtAvailableCurrencies = $this->serializer->unserialize($element->getValue()); 30 | $newValue = ''; 31 | if (!$CbtAvailableCurrencies) { 32 | return parent::_renderValue($element); 33 | } 34 | 35 | foreach ($CbtAvailableCurrencies as $currencyCode => $currency) { 36 | $min = $currency['minimumAmount']['amount'] ?? '0'; 37 | $newValue .= $currencyCode . '(min:' . $min . ',max:' . $currency['maximumAmount']['amount'] . ') '; 38 | } 39 | $element->setValue($newValue); 40 | } 41 | } catch (\Exception $e) { 42 | $this->logger->critical($e); 43 | } 44 | 45 | return parent::_renderValue($element); 46 | } 47 | 48 | public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) 49 | { 50 | /** @phpstan-ignore-next-line */ 51 | $element->unsScope()->unsCanUseWebsiteValue()->unsCanUseDefaultValue(); 52 | return parent::render($element); 53 | } 54 | 55 | protected function _getElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element) 56 | { 57 | /** @phpstan-ignore-next-line */ 58 | $element->setDisabled('disabled'); 59 | return $element->getElementHtml(); 60 | } 61 | } 62 | --------------------------------------------------------------------------------