diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index 8804fc33e6b31..184a71e2c1263 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -22,6 +22,7 @@ + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCartMinimumAmountTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCartMinimumAmountTest.xml new file mode 100644 index 0000000000000..b6ba5529f1a42 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCartMinimumAmountTest.xml @@ -0,0 +1,68 @@ + + + + + + + + + <description value="When the minimum order amount is set it must consider the tax value"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-28285"/> + <group value="checkout"/> + <group value="tax"/> + </annotations> + <before> + <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <magentoCLI command="config:set sales/minimum_order/active 1" stepKey="enableMinimumOrderAmount"/> + <magentoCLI command="config:set sales/minimum_order/amount 100" stepKey="setMinimumOrderAmount100"/> + <createData entity="taxRate_US_NY_8_1" stepKey="createTaxRateUSNY"/> + <createData entity="DefaultTaxRuleWithCustomTaxRate" stepKey="createTaxRuleUSNY"> + <requiredEntity createDataKey="createTaxRateUSNY" /> + </createData> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">93.00</field> + </createData> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createTaxRuleUSNY" stepKey="deleteTaxRuleUSNY"/> + <deleteData createDataKey="createTaxRateUSNY" stepKey="deleteTaxRateUSNY"/> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + </after> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> + <argument name="product" value="$simpleProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="addSimpleProductToTheCart"> + <argument name="productQty" value="1"/> + </actionGroup> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="clickMiniCart"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.proceedToCheckoutDisabled}}" stepKey="goToCheckoutDisabled"/> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertMinimumAmountOrderMessage"> + <argument name="message" value="Minimum order amount is $100.00"/> + <argument name="messageType" value="notice"/> + </actionGroup> + <actionGroup ref="CheckoutFillEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxFields"> + <argument name="address" value="US_Address_NY_Default_Shipping"/> + </actionGroup> + <click selector="{{CheckoutCartSummarySection.shippingMethodElementId('freeshipping', 'freeshipping')}}" stepKey="selectShippingMethod"/> + <scrollTo selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="scrollToProceedToCheckout" /> + <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <waitForElementVisible selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js b/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js index 836c7a8c58471..24e11ffc0e1d3 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js @@ -6,8 +6,9 @@ define([ 'jquery', 'Magento_Customer/js/model/authentication-popup', - 'Magento_Customer/js/customer-data' -], function ($, authenticationPopup, customerData) { + 'Magento_Customer/js/customer-data', + 'Magento_Checkout/js/model/quote' +], function ($, authenticationPopup, customerData, quote) { 'use strict'; return function (config, element) { @@ -26,5 +27,16 @@ define([ location.href = config.checkoutUrl; }); + quote.totals.subscribe(function (totals) { + if (totals['is_minimum_order_amount']) { + $(element).prop('disabled', false); + $(element).removeClass('disabled'); + + return; + } + + $(element).prop('disabled', true); + $(element).addClass('disabled'); + }); }; }); diff --git a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php index 8f7e6504cb7d8..24c050d906150 100644 --- a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php +++ b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php @@ -117,6 +117,10 @@ public function get($cartId): QuoteTotalsInterface $quoteTotals->setItemsQty($quote->getItemsQty()); $quoteTotals->setBaseCurrencyCode($quote->getBaseCurrencyCode()); $quoteTotals->setQuoteCurrencyCode($quote->getQuoteCurrencyCode()); + $isMinimumOrderAmount = $quote->validateMinimumAmount(); + $extensionAttributes = $quoteTotals->getExtensionAttributes(); + $extensionAttributes->setIsMinimumOrderAmount($isMinimumOrderAmount); + $quoteTotals->setExtensionAttributes($extensionAttributes); return $quoteTotals; } } diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index 48cd4b2eb2fad..e1916f2f75dd9 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -2298,6 +2298,8 @@ public function validateMinimumAmount($multishipping = false) $storeId ); + $this->collectTotals()->save(); + $addresses = $this->getAllAddresses(); if (!$multishipping) { diff --git a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php index bd2a2b7a82712..c52f1fb07418c 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php @@ -14,6 +14,7 @@ use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\CouponManagementInterface; use Magento\Quote\Api\Data\TotalSegmentInterface; +use Magento\Quote\Api\Data\TotalsExtensionInterface; use Magento\Quote\Api\Data\TotalsInterface as QuoteTotalsInterface; use Magento\Quote\Api\Data\TotalsInterfaceFactory; use Magento\Quote\Model\Cart\CartTotalRepository; @@ -108,7 +109,8 @@ protected function setUp(): void 'getBillingAddress', 'getAllVisibleItems', 'getItemsQty', - 'collectTotals' + 'collectTotals', + 'validateMinimumAmount' ] ) ->disableOriginalConstructor() @@ -138,6 +140,10 @@ protected function setUp(): void TotalsConverter::class ); + $this->totalsConverterMock = $this->createMock( + TotalsConverter::class + ); + $this->model = new CartTotalRepository( $this->totalsFactoryMock, $this->quoteRepositoryMock, @@ -247,6 +253,29 @@ public function testGetCartTotal($isVirtual, $getAddressType): void ->with(self::STUB_CURRENCY_CODE) ->willReturnSelf(); + $totalExtensionInterfaceMock = $this->getMockBuilder(TotalsExtensionInterface::class) + ->onlyMethods(['setIsMinimumOrderAmount']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $totalsMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($totalExtensionInterfaceMock); + + $totalExtensionInterfaceMock->expects($this->once()) + ->method('setIsMinimumOrderAmount') + ->with(true) + ->willReturnSelf(); + + $totalsMock->expects($this->once()) + ->method('setExtensionAttributes') + ->with($totalExtensionInterfaceMock) + ->willReturnSelf(); + + $this->quoteMock->expects($this->once()) + ->method('validateMinimumAmount') + ->willReturn(true); + $this->assertEquals($totalsMock, $this->model->get(self::STUB_CART_ID)); } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php index 422a6cbcb7bbe..8dfca53e7e8f9 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php @@ -40,6 +40,7 @@ use Magento\Quote\Model\Quote\AddressFactory; use Magento\Quote\Model\Quote\Item; use Magento\Quote\Model\Quote\Item\Processor; +use Magento\Quote\Model\Quote\TotalsCollector; use Magento\Quote\Model\Quote\Payment; use Magento\Quote\Model\Quote\PaymentFactory; use Magento\Quote\Model\ResourceModel\Quote\Address\Collection; @@ -190,6 +191,11 @@ class QuoteTest extends TestCase */ private $orderIncrementIdChecker; + /** + * @var TotalsCollector|MockObject + */ + private $totalsCollector; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -311,6 +317,12 @@ protected function setUp(): void CustomerInterfaceFactory::class, ['create'] ); + + $this->totalsCollector = $this->getMockBuilder(TotalsCollector::class) + ->onlyMethods(['collect']) + ->disableOriginalConstructor() + ->getMock(); + $this->orderIncrementIdChecker = $this->createMock(OrderIncrementIdChecker::class); $this->quote = (new ObjectManager($this)) ->getObject( @@ -337,6 +349,7 @@ protected function setUp(): void 'customerDataFactory' => $this->customerDataFactoryMock, 'itemProcessor' => $this->itemProcessor, 'orderIncrementIdChecker' => $this->orderIncrementIdChecker, + 'totalsCollector' => $this->totalsCollector, 'data' => [ 'reserved_order_id' => 1000001, ], @@ -1083,6 +1096,19 @@ public function testValidateMinimumAmount() ->method('setQuoteFilter') ->willReturn([$this->quoteAddressMock]); + $totalsMock = $this->getMockBuilder(DataObject::class) + ->onlyMethods(['getData']) + ->disableOriginalConstructor() + ->getMock(); + + $totalsMock->expects($this->once()) + ->method('getData') + ->willReturn([]); + + $this->totalsCollector->expects($this->once()) + ->method('collect') + ->willReturn($totalsMock); + $this->assertTrue($this->quote->validateMinimumAmount()); } @@ -1110,6 +1136,19 @@ public function testValidateMinimumAmountNegative() ->method('setQuoteFilter') ->willReturn([$this->quoteAddressMock]); + $totalsMock = $this->getMockBuilder(DataObject::class) + ->onlyMethods(['getData']) + ->disableOriginalConstructor() + ->getMock(); + + $totalsMock->expects($this->once()) + ->method('getData') + ->willReturn([]); + + $this->totalsCollector->expects($this->once()) + ->method('collect') + ->willReturn($totalsMock); + $this->assertFalse($this->quote->validateMinimumAmount()); } diff --git a/app/code/Magento/Quote/etc/extension_attributes.xml b/app/code/Magento/Quote/etc/extension_attributes.xml index 6fff29e32d2ab..9d9169b7d2966 100644 --- a/app/code/Magento/Quote/etc/extension_attributes.xml +++ b/app/code/Magento/Quote/etc/extension_attributes.xml @@ -12,5 +12,6 @@ <extension_attributes for="Magento\Quote\Api\Data\TotalsInterface"> <attribute code="coupon_label" type="string" /> + <attribute code="is_minimum_order_amount" type="int" /> </extension_attributes> </config> diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php index 1b219d0e11141..c22235442fab3 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php @@ -240,7 +240,7 @@ private function getData(Quote $quote, Address $shippingAddress) : array Totals::KEY_BASE_CURRENCY_CODE => $quote->getBaseCurrencyCode(), Totals::KEY_QUOTE_CURRENCY_CODE => $quote->getQuoteCurrencyCode(), Totals::KEY_ITEMS_QTY => $quote->getItemsQty(), - Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)], + Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)] ]; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php index 943e34d280bf2..ce0a09197b138 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php @@ -86,7 +86,7 @@ public function testGetTotals() Totals::KEY_BASE_CURRENCY_CODE => $quote->getBaseCurrencyCode(), Totals::KEY_QUOTE_CURRENCY_CODE => $quote->getQuoteCurrencyCode(), Totals::KEY_ITEMS_QTY => $quote->getItemsQty(), - Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)], + Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)] ]; $requestData = ['cartId' => $cartId];