diff --git a/InventoryAdminUi/Test/Mftf/Test/CatalogPriceRuleConfigurableProductAdditionalStockTest.xml b/InventoryAdminUi/Test/Mftf/Test/CatalogPriceRuleConfigurableProductAdditionalStockTest.xml index 5579a169c3a9..850cc7bea066 100644 --- a/InventoryAdminUi/Test/Mftf/Test/CatalogPriceRuleConfigurableProductAdditionalStockTest.xml +++ b/InventoryAdminUi/Test/Mftf/Test/CatalogPriceRuleConfigurableProductAdditionalStockTest.xml @@ -21,63 +21,70 @@ - - + + - - - + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + - + - - - + - + - + - + diff --git a/InventoryAdminUi/Test/Mftf/Test/StorefrontGroupedProductCustomStockWithOneOptionSoldOutTest.xml b/InventoryAdminUi/Test/Mftf/Test/StorefrontGroupedProductCustomStockWithOneOptionSoldOutTest.xml new file mode 100644 index 000000000000..1f8e9fe62690 --- /dev/null +++ b/InventoryAdminUi/Test/Mftf/Test/StorefrontGroupedProductCustomStockWithOneOptionSoldOutTest.xml @@ -0,0 +1,116 @@ + + + + + + + + + <description value="Verify totally sold out grouped product option on custom stock is not visible on storefront."/> + <testCaseId value="https://studio.cucumber.io/projects/69435/test-plan/folders/912250/scenarios/4747577"/> + <severity value="CRITICAL"/> + <group value="msi"/> + <group value="multi_mode"/> + </annotations> + + <before> + <!--Create test data.--> + <createData entity="FullSource1" stepKey="source"/> + <createData entity="BasicMsiStockWithMainWebsite1" stepKey="stock"/> + <createData entity="SourceStockLinked1" stepKey="linkSourceStock"> + <requiredEntity createDataKey="stock"/> + <requiredEntity createDataKey="source"/> + </createData> + <createData entity="ApiGroupedProduct" stepKey="groupedProduct"/> + <createData entity="ApiProductWithDescription" stepKey="product1"/> + <createData entity="ApiProductWithDescription" stepKey="product2"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="groupedProduct"/> + <requiredEntity createDataKey="product1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="groupedProduct"/> + <requiredEntity createDataKey="product2"/> + </updateData> + <createData entity="MsiCustomer1" stepKey="customer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Assign sources to product1.--> + <actionGroup ref="NavigateToCreatedProductEditPageActionGroup" stepKey="navigateToSimpleProduct1"> + <argument name="product" value="$product1$"/> + </actionGroup> + <actionGroup ref="UnassignSourceFromProductActionGroup" stepKey="unassignDefaultSourceFromProduct1"> + <argument name="sourceCode" value="{{_defaultSource.name}}"/> + </actionGroup> + <actionGroup ref="AdminAssignSourceToProductAndSetSourceQuantityActionGroup" stepKey="assignSourceToProduct1"> + <argument name="sourceCode" value="$source.source[source_code]$"/> + <argument name="sourceQty" value="{{minimalProductQty.value}}"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct1"/> + <!--Assign sources to product2.--> + <actionGroup ref="NavigateToCreatedProductEditPageActionGroup" stepKey="navigateToSimpleProduct2"> + <argument name="product" value="$product2$"/> + </actionGroup> + <actionGroup ref="UnassignSourceFromProductActionGroup" stepKey="unassignDefaultSourceFromProduct2"> + <argument name="sourceCode" value="{{_defaultSource.name}}"/> + </actionGroup> + <actionGroup ref="AdminAssignSourceToProductAndSetSourceQuantityActionGroup" stepKey="assignSourceToProduct2"> + <argument name="sourceCode" value="$source.source[source_code]$"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct2"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <deleteData createDataKey="groupedProduct" stepKey="deleteGroupedProduct"/> + <deleteData createDataKey="product1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="product2" stepKey="deleteSimpleProduct2"/> + <!--Assign Default Stock to Default Website.--> + <actionGroup ref="AssignWebsiteToStockActionGroup" stepKey="assignMainWebsiteToDefaultStock"> + <argument name="stockName" value="{{_defaultStock.name}}"/> + <argument name="websiteName" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <!--Disable created sources.--> + <actionGroup ref="DisableAllSourcesActionGroup" stepKey="disableSource"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + <deleteData createDataKey="stock" stepKey="deleteStock"/> + </after> + + <!--Login To storefront as Customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <!--Add grouped product to cart.--> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="navigateToGroupedProductPDP"> + <argument name="product" value="$groupedProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontAddGroupedProductWithTwoLinksToCartActionGroup" stepKey="addGroupedProductToCart"> + <argument name="product" value="$groupedProduct$"/> + <argument name="linkedProduct1Name" value="$product1.name$"/> + <argument name="linkedProduct2Name" value="$product2.name$"/> + </actionGroup> + <!--Place order.--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutPage"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="orderNumber"/> + <!--Create Shipment--> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="{$orderNumber}"/> + </actionGroup> + <actionGroup ref="AdminShipThePendingOrderActionGroup" stepKey="shipOrder"/> + <!--Verify grouped product PDP.--> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="navigateToGroupedProductPDPAfterOrderPlacement"> + <argument name="product" value="$groupedProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLinkIsNotVisibleOnGroupedProductPageActionGroup" stepKey="verifyFirstOptionIsNotPresentedOnPage"> + <argument name="productName" value="$product1.name$"/> + </actionGroup> + <actionGroup ref="AssertLinkPresenceOnGroupedProductPage" stepKey="verifySecondOptionIsStillPresentedOnPage"> + <argument name="productName" value="$product2.name$"/> + </actionGroup> + </test> +</tests> diff --git a/InventoryGroupedProduct/Plugin/GroupedProduct/Model/Product/Type/Grouped/AdaptIsSalableOptionPlugin.php b/InventoryGroupedProduct/Plugin/GroupedProduct/Model/Product/Type/Grouped/AdaptIsSalableOptionPlugin.php new file mode 100644 index 000000000000..23f9c6ecb6ad --- /dev/null +++ b/InventoryGroupedProduct/Plugin/GroupedProduct/Model/Product/Type/Grouped/AdaptIsSalableOptionPlugin.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\InventoryGroupedProduct\Plugin\GroupedProduct\Model\Product\Type\Grouped; + +use Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\InventorySalesApi\Api\AreProductsSalableInterface; +use Magento\InventorySalesApi\Api\Data\SalesChannelInterface; +use Magento\InventorySalesApi\Api\StockResolverInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Filter only salable grouped options plugin. + */ +class AdaptIsSalableOptionPlugin +{ + /** + * @var AreProductsSalableInterface + */ + private $areProductsSalable; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var StockResolverInterface + */ + private $stockResolver; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @param AreProductsSalableInterface $areProductsSalable + * @param StockResolverInterface $stockResolver + * @param StoreManagerInterface $storeManager + * @param StockConfigurationInterface $stockConfiguration + */ + public function __construct( + AreProductsSalableInterface $areProductsSalable, + StockResolverInterface $stockResolver, + StoreManagerInterface $storeManager, + StockConfigurationInterface $stockConfiguration + ) { + $this->areProductsSalable = $areProductsSalable; + $this->storeManager = $storeManager; + $this->stockResolver = $stockResolver; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Filter salable grouped options. + * + * @param Grouped $subject + * @param Collection $result + * @return Collection + * @throws NoSuchEntityException in case there is no stock connected to given website. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetAssociatedProductCollection(Grouped $subject, Collection $result): Collection + { + if ($this->stockConfiguration->isShowOutOfStock()) { + return $result; + } + $website = $this->storeManager->getStore($result->getStoreId())->getWebsite(); + $stock = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $website->getCode()); + $skus = array_column($result->getData(), 'sku'); + $areProductsSalable = $this->areProductsSalable->execute($skus, $stock->getStockId()); + $sksToInclude = []; + foreach ($areProductsSalable as $salableResult) { + if ($salableResult->isSalable()) { + $sksToInclude[] = $salableResult->getSku(); + } + } + $result->addAttributeToFilter('sku', ['in' => $sksToInclude]); + + return $result; + } +} diff --git a/InventoryGroupedProduct/composer.json b/InventoryGroupedProduct/composer.json index fc112d52f6eb..c76426e03304 100644 --- a/InventoryGroupedProduct/composer.json +++ b/InventoryGroupedProduct/composer.json @@ -4,7 +4,11 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", - "magento/module-grouped-product": "*" + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-grouped-product": "*", + "magento/module-inventory-sales-api": "*", + "magento/module-store": "*" }, "suggest": { "magento/module-inventory-configuration-api": "*" diff --git a/InventoryGroupedProduct/etc/frontend/di.xml b/InventoryGroupedProduct/etc/frontend/di.xml new file mode 100644 index 000000000000..2ca4c5b05e17 --- /dev/null +++ b/InventoryGroupedProduct/etc/frontend/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\GroupedProduct\Model\Product\Type\Grouped"> + <plugin name="adapt_is_salable_option" type="Magento\InventoryGroupedProduct\Plugin\GroupedProduct\Model\Product\Type\Grouped\AdaptIsSalableOptionPlugin"/> + </type> +</config> diff --git a/InventoryGroupedProductAdminUi/Test/Mftf/ActionGroup/StorefrontAssertLinkIsNotVisibleOnGroupedProductPageActionGroup.xml b/InventoryGroupedProductAdminUi/Test/Mftf/ActionGroup/StorefrontAssertLinkIsNotVisibleOnGroupedProductPageActionGroup.xml new file mode 100644 index 000000000000..8c39a149eb9d --- /dev/null +++ b/InventoryGroupedProductAdminUi/Test/Mftf/ActionGroup/StorefrontAssertLinkIsNotVisibleOnGroupedProductPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertLinkIsNotVisibleOnGroupedProductPageActionGroup"> + <annotations> + <description>Validates that the provided Product Name is not presented on a Storefront Grouped Product page.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + </arguments> + + <dontSee selector="{{StorefrontProductInfoMainSection.groupedProductsTable}}" userInput="{{productName}}" stepKey="seeFirstStagedGroupedProduct"/> + </actionGroup> +</actionGroups>