diff --git a/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php b/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php index 5f8b7eedc0fd..4755a8c0ae47 100644 --- a/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php +++ b/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php @@ -9,6 +9,10 @@ use Exception; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Config; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; @@ -20,6 +24,11 @@ use Magento\InventoryMultiDimensionalIndexerApi\Model\IndexNameResolverInterface; use Magento\InventoryIndexer\Indexer\SelectBuilderInterface; +/** + * Get configurable product for given stock select builder + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SelectBuilder implements SelectBuilderInterface { /** @@ -46,25 +55,33 @@ class SelectBuilder implements SelectBuilderInterface */ private $defaultStockProvider; + /** + * @var Config + */ + private $eavConfig; + /** * @param ResourceConnection $resourceConnection * @param IndexNameBuilder $indexNameBuilder * @param IndexNameResolverInterface $indexNameResolver * @param MetadataPool $metadataPool * @param DefaultStockProviderInterface $defaultStockProvider + * @param Config $eavConfig */ public function __construct( ResourceConnection $resourceConnection, IndexNameBuilder $indexNameBuilder, IndexNameResolverInterface $indexNameResolver, MetadataPool $metadataPool, - DefaultStockProviderInterface $defaultStockProvider + DefaultStockProviderInterface $defaultStockProvider, + Config $eavConfig ) { $this->resourceConnection = $resourceConnection; $this->indexNameBuilder = $indexNameBuilder; $this->indexNameResolver = $indexNameResolver; $this->metadataPool = $metadataPool; $this->defaultStockProvider = $defaultStockProvider; + $this->eavConfig = $eavConfig; } /** @@ -88,6 +105,7 @@ public function execute(int $stockId): Select $metadata = $this->metadataPool->getMetadata(ProductInterface::class); $linkField = $metadata->getLinkField(); + $statusAttributeId = $this->getAttribute(ProductInterface::STATUS)->getId(); $select = $connection->select() ->from( @@ -114,10 +132,27 @@ public function execute(int $stockId): Select 'inventory_stock_item.product_id = parent_product_entity.entity_id' . ' AND inventory_stock_item.stock_id = ' . $this->defaultStockProvider->getId(), [] + )->joinInner( + ['product_status' => $this->resourceConnection->getTableName('catalog_product_entity_int')], + "product_entity.$linkField = product_status.$linkField" + . " AND product_status.attribute_id = $statusAttributeId" + . ' AND product_status.value = ' . ProductStatus::STATUS_ENABLED, + [] ) ->group(['parent_product_entity.sku']) ->order('parent_product_entity.sku ASC'); return $select; } + + /** + * Retrieve catalog_product attribute instance by attribute code + * + * @param string $attributeCode + * @return Attribute + */ + private function getAttribute($attributeCode): Attribute + { + return $this->eavConfig->getAttribute(Product::ENTITY, $attributeCode); + } } diff --git a/InventoryConfigurableProductIndexer/Test/Mftf/Test/ConfigurableProductNotVisibleAfterDisablingChildProductsTest.xml b/InventoryConfigurableProductIndexer/Test/Mftf/Test/ConfigurableProductNotVisibleAfterDisablingChildProductsTest.xml new file mode 100644 index 000000000000..ee45d5db6cd8 --- /dev/null +++ b/InventoryConfigurableProductIndexer/Test/Mftf/Test/ConfigurableProductNotVisibleAfterDisablingChildProductsTest.xml @@ -0,0 +1,140 @@ + + + + + + + + + <description value="Verify, configurable product is not displayed on category page after disabling first child product and set out of stock to second"/> + <testCaseId value="MC-38896"/> + <useCaseId value="MC-38590"/> + <severity value="AVERAGE"/> + <group value="msi"/> + </annotations> + <before> + <!--Create test data.--> + <!-- Create the category to put the product in --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createFirstConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createSecondConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createFirstConfigChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getFirstConfigAttributeOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createSecondConfigChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getSecondConfigAttributeOption"/> + </createData> + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getFirstConfigAttributeOption"/> + <requiredEntity createDataKey="getSecondConfigAttributeOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createFirstConfigProductAddChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createFirstConfigChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createSecondConfigProductAddChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createSecondConfigChildProduct"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <createData entity="_minimalSource" stepKey="createSource"/> + <createData entity="BasicMsiStockWithMainWebsite1" stepKey="stock"/> + <createData entity="SourceStockLinked1" stepKey="linkStockAndSource"> + <requiredEntity createDataKey="stock"/> + <requiredEntity createDataKey="createSource"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <!--Assign additional source to configurable product.--> + <amOnPage url="{{AdminProductEditPage.url($createFirstConfigChildProduct.id$)}}" stepKey="openProductEditPage"/> + <actionGroup ref="UnassignSourceFromProductActionGroup" stepKey="unassignDefaultSourceFromProduct"> + <argument name="sourceCode" value="{{_defaultSource.name}}"/> + </actionGroup> + <actionGroup ref="AdminAssignSourceToProductAndSetSourceQuantityActionGroup" stepKey="assignCreatedSourceToFirstChildProduct"> + <argument name="sourceCode" value="$createSource.source[source_code]$"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveFirstChildProduct"/> + <!--Assign additional source to configurable product second.--> + <amOnPage url="{{AdminProductEditPage.url($createSecondConfigChildProduct.id$)}}" stepKey="openSecondProductEditPage"/> + <actionGroup ref="UnassignSourceFromProductActionGroup" stepKey="unassignDefaultSourceFromSecondProduct"> + <argument name="sourceCode" value="{{_defaultSource.name}}"/> + </actionGroup> + <actionGroup ref="AdminAssignSourceToProductAndSetSourceQuantityActionGroup" stepKey="assignCreatedSourceToSecondChildProduct"> + <argument name="sourceCode" value="$createSource.source[source_code]$"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondChildProduct"/> + <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createFirstConfigChildProduct" stepKey="deleteFirstConfigChildProduct"/> + <deleteData createDataKey="createSecondConfigChildProduct" stepKey="deleteSecondConfigChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <!--Assign Default Stock to Main Website.--> + <actionGroup ref="AssignWebsiteToStockActionGroup" stepKey="assignMainWebsiteToDefaultStock"> + <argument name="stockName" value="{{_defaultStock.name}}"/> + <argument name="websiteName" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <deleteData createDataKey="stock" stepKey="deleteStock"/> + <!--Disable source.--> + <actionGroup ref="DisableAllSourcesActionGroup" stepKey="disableSources"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminArea"/> + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + <!--Verify product is visible on storefront.--> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPageOnFrontend"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="checkProductOnCategoryPage"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + <!--Open first child product in Admin. Make it disabled (Enable Product = No)--> + <amOnPage url="{{AdminProductEditPage.url($$createFirstConfigChildProduct.id$$)}}" stepKey="openProductEditPageForDisablingProduct"/> + <actionGroup ref="AdminSetProductDisabledActionGroup" stepKey="disableProduct"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> + <!--Open second child product and set product stock status to out of stock--> + <amOnPage url="{{AdminProductEditPage.url($$createSecondConfigChildProduct.id$$)}}" stepKey="openProductEditPageForDisablingSource"/> + <actionGroup ref="AdminChangeSourceStockStatusActionGroup" stepKey="setProductStatusToOutOfStock"> + <argument name="sourceCode" value="$createSource.source[source_code]$"/> + <argument name="sourceStatus" value="{{SourceStatusOutOfStock.value}}"/> + </actionGroup> + <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveProduct"/> + <!--Verify product is not visible on storefront.--> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductOnCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + </test> +</tests> diff --git a/InventoryConfigurableProductIndexer/Test/Unit/Indexer/SelectBuilderTest.php b/InventoryConfigurableProductIndexer/Test/Unit/Indexer/SelectBuilderTest.php index 2d5251dbf6e4..d45c1f351449 100644 --- a/InventoryConfigurableProductIndexer/Test/Unit/Indexer/SelectBuilderTest.php +++ b/InventoryConfigurableProductIndexer/Test/Unit/Indexer/SelectBuilderTest.php @@ -7,6 +7,8 @@ namespace Magento\InventoryConfigurableProductIndexer\Test\Unit\Indexer; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Config; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; @@ -20,6 +22,9 @@ use Magento\InventoryMultiDimensionalIndexerApi\Model\IndexNameResolverInterface; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SelectBuilderTest extends TestCase { public function testExecuteOrdersBySkuAscending(): void @@ -58,6 +63,11 @@ public function testExecuteOrdersBySkuAscending(): void $defaultStockProvider = $this->createMock(DefaultStockProviderInterface::class); $defaultStockProvider->method('getId')->willReturn(1); + $statusAttribute = $this->createMock(Attribute::class); + $statusAttribute->method('getId')->willReturn(97); + $eavConfig = $this->createMock(Config::class); + $eavConfig->method('getAttribute')->willReturn($statusAttribute); + $selectBuilder = (new ObjectManager($this))->getObject( SelectBuilder::class, [ @@ -66,6 +76,7 @@ public function testExecuteOrdersBySkuAscending(): void 'indexNameResolver' => $indexNameResolver, 'metadataPool' => $metadataPool, 'defaultStockProvider' => $defaultStockProvider, + 'eavConfig' => $eavConfig, ] ); diff --git a/InventoryConfigurableProductIndexer/composer.json b/InventoryConfigurableProductIndexer/composer.json index 0efa94a6aa02..2fb23b761b1e 100644 --- a/InventoryConfigurableProductIndexer/composer.json +++ b/InventoryConfigurableProductIndexer/composer.json @@ -11,6 +11,7 @@ "php": "~8.2.0||~8.3.0||~8.4.0", "magento/framework": "*", "magento/module-catalog": "*", + "magento/module-eav": "*", "magento/module-inventory-api": "1.2.*", "magento/module-inventory-catalog-api": "1.3.*", "magento/module-inventory-indexer": "2.2.*",