From cd78e25d9ec90f996574cc9651ee3eea851d410a Mon Sep 17 00:00:00 2001 From: Misha Slabko Date: Mon, 25 Oct 2021 17:19:21 -0500 Subject: [PATCH 01/18] MDEE-40:[Commerce Export] Implement stock_item_status feed - cover with tests: bacis cases --- .../Model/Provider/StockStatus.php | 8 +- .../Integration/ExportStockStatusTest.php | 154 +++++++++ .../Test/_files/products_with_sources.php | 326 ++++++++++++++++++ .../_files/products_with_sources_rollback.php | 68 ++++ 4 files changed, 554 insertions(+), 2 deletions(-) create mode 100644 InventoryDataExporter/Test/Integration/ExportStockStatusTest.php create mode 100644 InventoryDataExporter/Test/_files/products_with_sources.php create mode 100644 InventoryDataExporter/Test/_files/products_with_sources_rollback.php diff --git a/InventoryDataExporter/Model/Provider/StockStatus.php b/InventoryDataExporter/Model/Provider/StockStatus.php index c8f698f0..3496f620 100644 --- a/InventoryDataExporter/Model/Provider/StockStatus.php +++ b/InventoryDataExporter/Model/Provider/StockStatus.php @@ -9,6 +9,7 @@ namespace Magento\InventoryDataExporter\Model\Provider; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\TableNotFoundException; use Magento\InventoryDataExporter\Model\Query\InventoryStockQuery; use Psr\Log\LoggerInterface; @@ -83,9 +84,12 @@ public function get(array $values): array while ($row = $cursor->fetch()) { $output[] = $this->fillWithDefaultValues($row); } - + } catch (TableNotFoundException $e) { + $this->logger->warning( + 'StockStatus export warning. Inventory index should be run first. Error: ' . $e->getMessage(). ' ' + ); } catch (\Throwable $e) { - $this->logger->error("StockStatus export error: " . $e->getMessage(), ['exception' => $e]); + $this->logger->error('StockStatus export error: ' . $e->getMessage(), ['exception' => $e]); throw $e; } diff --git a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php new file mode 100644 index 00000000..c794c6dc --- /dev/null +++ b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php @@ -0,0 +1,154 @@ +processor = Bootstrap::getObjectManager()->create(Processor::class); + } + + /** + * @magentoDataFixture Magento_InventoryDataExporter::Test/_files/products_with_sources.php + */ + public function testExportStockStatuses() + { + $actualStockStatus = $this->processor->process( + 'stock_statuses', + [ + ['sku' => 'product_in_EU_stock_with_2_sources'], + ['sku' => 'product_in_Global_stock_with_3_sources'], + ['sku' => 'product_with_default_stock_only'], + ['sku' => 'product_with_disabled_manage_stock'], + ['sku' => 'product_with_enabled_backorders'], + ] + ); + + $actualStockStatusFormatted = []; + foreach ($actualStockStatus as $stockStatus) { + $actualStockStatusFormatted[$stockStatus['stockId']][$stockStatus['sku']] = $stockStatus; + } + foreach ($this->getExpectedStockStatus() as $stockId => $stockStatuses) { + foreach ($stockStatuses as $sku => $stockStatus) { + if (!isset($actualStockStatusFormatted[$stockId][$sku])) { + self::fail("Cannot find stock status for stock $stockId & sku $sku"); + } + $actualStockStatus = $actualStockStatusFormatted[$stockId][$sku]; + // ignore fields for now + unset($actualStockStatus['id'], $actualStockStatus['lowStock'], $actualStockStatus['updatedAt']); + self::assertEquals( + $stockStatus, + $actualStockStatus, + "Wrong stock status for stock $stockId & sku $sku" + ); + } + } + } + + /** + * @return \array[][] + */ + private function getExpectedStockStatus(): array + { + return [ + // default stock + '1' => [ + 'product_with_default_stock_only' => [ + 'stockId' => '1', + 'sku' => 'product_with_default_stock_only', + 'qty' => 8.5, + 'qtyForSale' => 8.5, + 'infiniteStock' => false, + 'isSalable' => true, + ], + 'product_with_disabled_manage_stock' => [ + 'stockId' => '1', + 'sku' => 'product_with_disabled_manage_stock', + 'qty' => 0, + 'qtyForSale' => 0, + 'infiniteStock' => true, + 'isSalable' => true, + ], + 'product_with_enabled_backorders' => [ + 'stockId' => '1', + 'sku' => 'product_with_enabled_backorders', + 'qty' => 5, + 'qtyForSale' => 5, + 'infiniteStock' => true, + 'isSalable' => true, + ], + ], + // EU Stock + '10' => [ + 'product_in_EU_stock_with_2_sources' => [ + 'stockId' => '10', + 'sku' => 'product_in_EU_stock_with_2_sources', + 'qty' => 9.5, + 'qtyForSale' => 9.5, + 'infiniteStock' => false, + 'isSalable' => true, + ], + 'product_in_Global_stock_with_3_sources' => [ + 'stockId' => '10', + 'sku' => 'product_in_Global_stock_with_3_sources', + 'qty' => 3, // eu1 + eu2 + 'qtyForSale' => 3, + 'infiniteStock' => false, + 'isSalable' => true, + ], + ], + // US Stock + '20' => [ + 'product_in_Global_stock_with_3_sources' => [ + 'stockId' => '20', + 'sku' => 'product_in_Global_stock_with_3_sources', + 'qty' => 4, // us-1 source assigned to both stocks: US & Global + 'qtyForSale' => 4, + 'infiniteStock' => false, + 'isSalable' => true, + ], + ], + // Global Stock + '30' => [ + 'product_in_Global_stock_with_3_sources' => [ + 'stockId' => '30', + 'sku' => 'product_in_Global_stock_with_3_sources', + 'qty' => 7, + 'qtyForSale' => 7, + 'infiniteStock' => false, + 'isSalable' => true, + ], + 'product_in_EU_stock_with_2_sources' => [ + 'stockId' => '30', + 'sku' => 'product_in_EU_stock_with_2_sources', + 'qty' => 9.5, // eu1 + eu2 + 'qtyForSale' => 9.5, + 'infiniteStock' => false, + 'isSalable' => true, + ], + ], + ]; + } +} diff --git a/InventoryDataExporter/Test/_files/products_with_sources.php b/InventoryDataExporter/Test/_files/products_with_sources.php new file mode 100644 index 00000000..18ca3efc --- /dev/null +++ b/InventoryDataExporter/Test/_files/products_with_sources.php @@ -0,0 +1,326 @@ +get(StockInterfaceFactory::class); + /** @var DataObjectHelper $dataObjectHelper */ + $dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); + /** @var StockRepositoryInterface $stockRepository */ + $stockRepository = Bootstrap::getObjectManager()->get(StockRepositoryInterface::class); + + $stocksData = [ + [ + // define only required and needed for tests fields + StockInterface::STOCK_ID => TEST_EU_STOCK_ID, + StockInterface::NAME => 'EU_stock', + ], + [ + StockInterface::STOCK_ID => TEST_US_STOCK_ID, + StockInterface::NAME => 'US_stock', + ], + [ + StockInterface::STOCK_ID => TEST_GLOBAL_STOCK_ID, + StockInterface::NAME => 'Global_stock', + ] + ]; + foreach ($stocksData as $stockData) { + /** @var StockInterface $stock */ + $stock = $stockFactory->create(); + $dataObjectHelper->populateWithArray($stock, $stockData, StockInterface::class); + $stockRepository->save($stock); + } +} + +/** + * Create Sources + */ +function createSources(): void +{ + /** @var SourceInterfaceFactory $sourceFactory */ + $sourceFactory = Bootstrap::getObjectManager()->get(SourceInterfaceFactory::class); + /** @var DataObjectHelper $dataObjectHelper */ + $dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); + /** @var SourceRepositoryInterface $sourceRepository */ + $sourceRepository = Bootstrap::getObjectManager()->get(SourceRepositoryInterface::class); + + $sourcesData = [ + [ + // define only required and needed for tests fields + SourceInterface::SOURCE_CODE => 'eu-1', + SourceInterface::NAME => 'EU-source-1', + SourceInterface::ENABLED => true, + SourceInterface::POSTCODE => 'postcode', + SourceInterface::COUNTRY_ID => 'FR', + ], + [ + SourceInterface::SOURCE_CODE => 'eu-2', + SourceInterface::NAME => 'EU-source-2', + SourceInterface::ENABLED => true, + SourceInterface::POSTCODE => 'postcode', + SourceInterface::COUNTRY_ID => 'FR', + ], + [ + SourceInterface::SOURCE_CODE => 'us-1', + SourceInterface::NAME => 'US-source-1', + SourceInterface::ENABLED => true, + SourceInterface::POSTCODE => 'postcode', + SourceInterface::COUNTRY_ID => 'US', + ], + ]; + foreach ($sourcesData as $sourceData) { + /** @var SourceInterface $source */ + $source = $sourceFactory->create(); + $dataObjectHelper->populateWithArray($source, $sourceData, SourceInterface::class); + $sourceRepository->save($source); + } +} + +/** + * Link Source to Stocks + */ +function assignSourceToStock(): void +{ + /** @var DataObjectHelper $dataObjectHelper */ + $dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); + /** @var StockSourceLinksSaveInterface $stockSourceLinksSave */ + $stockSourceLinksSave = Bootstrap::getObjectManager()->get(StockSourceLinksSaveInterface::class); + /** @var StockSourceLinkInterfaceFactory $stockSourceLinkFactory */ + $stockSourceLinkFactory = Bootstrap::getObjectManager()->get(StockSourceLinkInterfaceFactory::class); + + $linksData = [ + [ + StockSourceLinkInterface::STOCK_ID => TEST_EU_STOCK_ID, + StockSourceLinkInterface::SOURCE_CODE => 'eu-1', + StockSourceLinkInterface::PRIORITY => 1, + ], + [ + StockSourceLinkInterface::STOCK_ID => TEST_EU_STOCK_ID, + StockSourceLinkInterface::SOURCE_CODE => 'eu-2', + StockSourceLinkInterface::PRIORITY => 2, + ], + [ + StockSourceLinkInterface::STOCK_ID => TEST_US_STOCK_ID, + StockSourceLinkInterface::SOURCE_CODE => 'us-1', + StockSourceLinkInterface::PRIORITY => 1, + ], + [ + StockSourceLinkInterface::STOCK_ID => TEST_GLOBAL_STOCK_ID, + StockSourceLinkInterface::SOURCE_CODE => 'eu-1', + StockSourceLinkInterface::PRIORITY => 1, + ], + [ + StockSourceLinkInterface::STOCK_ID => TEST_GLOBAL_STOCK_ID, + StockSourceLinkInterface::SOURCE_CODE => 'eu-2', + StockSourceLinkInterface::PRIORITY => 1, + ], + [ + StockSourceLinkInterface::STOCK_ID => TEST_GLOBAL_STOCK_ID, + StockSourceLinkInterface::SOURCE_CODE => 'us-1', + StockSourceLinkInterface::PRIORITY => 2, + ], + ]; + + $links = []; + foreach ($linksData as $linkData) { + /** @var StockSourceLinkInterface $link */ + $link = $stockSourceLinkFactory->create(); + $dataObjectHelper->populateWithArray($link, $linkData, StockSourceLinkInterface::class); + $links[] = $link; + } + $stockSourceLinksSave->execute($links); +} + +function createProducts() +{ + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductInterfaceFactory $productFactory */ + $productFactory = $objectManager->get(ProductInterfaceFactory::class); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + $productRepository->cleanCache(); + + $stockData = [ + 'product_with_default_stock_only' => [ + 'qty' => 8.5, + 'is_in_stock' => true, + 'manage_stock' => true, + 'is_qty_decimal' => true + ], + 'product_with_disabled_manage_stock' => [ + 'use_config_manage_stock' => false, + 'manage_stock' => false, + ], + 'product_with_enabled_backorders' => [ + 'qty' => 5, + 'is_in_stock' => true, + 'manage_stock' => true, + 'min_qty' => -3, + 'backorders' => true + ], + 'product_in_EU_stock_with_2_sources' => [ + 'qty' => 0, + 'is_in_stock' => true, + 'is_qty_decimal' => true, + 'manage_stock' => true + ], + 'product_in_Global_stock_with_3_sources' => [ + 'qty' => 0, + 'is_in_stock' => true, + 'manage_stock' => true + ], + ]; + + foreach ($stockData as $sku => $productStockData) { + $product = $productFactory->create(); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple Product ' . $sku) + ->setSku($sku) + ->setPrice(10) + ->setStockData($productStockData) + ->setStatus(Status::STATUS_ENABLED); + $productRepository->save($product); + } +} + +function assignProductsToSources(): void +{ + /** @var DataObjectHelper $dataObjectHelper */ + $dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); + /** @var SourceItemInterfaceFactory $sourceItemFactory */ + $sourceItemFactory = Bootstrap::getObjectManager()->get(SourceItemInterfaceFactory::class); + /** @var SourceItemsSaveInterface $sourceItemsSave */ + $sourceItemsSave = Bootstrap::getObjectManager()->get(SourceItemsSaveInterface::class); + + $sourcesItemsData = [ + [ + SourceItemInterface::SOURCE_CODE => 'eu-1', + SourceItemInterface::SKU => 'product_in_EU_stock_with_2_sources', + SourceItemInterface::QUANTITY => 5.5, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ], + [ + SourceItemInterface::SOURCE_CODE => 'eu-2', + SourceItemInterface::SKU => 'product_in_EU_stock_with_2_sources', + SourceItemInterface::QUANTITY => 4, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ], + [ + SourceItemInterface::SOURCE_CODE => 'eu-1', + SourceItemInterface::SKU => 'product_in_Global_stock_with_3_sources', + SourceItemInterface::QUANTITY => 1, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ], + [ + SourceItemInterface::SOURCE_CODE => 'eu-2', + SourceItemInterface::SKU => 'product_in_Global_stock_with_3_sources', + SourceItemInterface::QUANTITY => 2, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ], + [ + SourceItemInterface::SOURCE_CODE => 'us-1', + SourceItemInterface::SKU => 'product_in_Global_stock_with_3_sources', + SourceItemInterface::QUANTITY => 4, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ], + // TODO +// [ +// SourceItemInterface::SOURCE_CODE => 'eu-2', +// SourceItemInterface::SKU => 'SKU-3', +// SourceItemInterface::QUANTITY => 6, +// SourceItemInterface::STATUS => SourceItemInterface::STATUS_OUT_OF_STOCK, +// ], + ]; + + $sourceItems = []; + foreach ($sourcesItemsData as $sourceItemData) { + /** @var SourceItemInterface $source */ + $sourceItem = $sourceItemFactory->create(); + $dataObjectHelper->populateWithArray($sourceItem, $sourceItemData, SourceItemInterface::class); + $sourceItems[] = $sourceItem; + } + $sourceItemsSave->execute($sourceItems); +} \ No newline at end of file diff --git a/InventoryDataExporter/Test/_files/products_with_sources_rollback.php b/InventoryDataExporter/Test/_files/products_with_sources_rollback.php new file mode 100644 index 00000000..b2f90af5 --- /dev/null +++ b/InventoryDataExporter/Test/_files/products_with_sources_rollback.php @@ -0,0 +1,68 @@ +create(ProductRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + + +$currentArea = $registry->registry('isSecureArea'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +foreach ($skusToDelete as $productSku) { + $productRepository->deleteById($productSku); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', $currentArea); + + +// Delete Sources + +/** @var ResourceConnection $connection */ +$connection = Bootstrap::getObjectManager()->get(ResourceConnection::class); +$connection->getConnection()->delete( + $connection->getTableName('inventory_source'), + [ + SourceInterface::SOURCE_CODE . ' IN (?)' => ['eu-1', 'eu-2', 'us-1'], + ] +); + +// Delete Stocks + +/** @var StockRepositoryInterface $stockRepository */ +$stockRepository = Bootstrap::getObjectManager()->get(StockRepositoryInterface::class); + +foreach ([10, 20, 30] as $stockId) { + try { + $stockRepository->deleteById($stockId); + } catch (NoSuchEntityException $e) { + //Stock already removed + } +} From 9db27616c07b43490d2a9153603e81ae61b01c6c Mon Sep 17 00:00:00 2001 From: Misha Slabko Date: Tue, 26 Oct 2021 16:19:35 -0500 Subject: [PATCH 02/18] MDEE-40:[Commerce Export] Implement stock_item_status feed - fix backorders logic - cover partial reindex --- .../Model/Provider/InfiniteStock.php | 6 +- .../Model/Query/InventoryStockQuery.php | 38 +++- InventoryDataExporter/README.md | 8 +- .../Integration/ExportStockStatusTest.php | 19 +- .../Integration/PartialReindexCheckTest.php | 202 ++++++++++++++++++ .../Test/_files/products_with_sources.php | 63 +++--- InventoryDataExporter/etc/db_schema.xml | 3 +- InventoryDataExporter/etc/di.xml | 13 +- 8 files changed, 306 insertions(+), 46 deletions(-) create mode 100644 InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php diff --git a/InventoryDataExporter/Model/Provider/InfiniteStock.php b/InventoryDataExporter/Model/Provider/InfiniteStock.php index fe5e93b9..9ebf3ea9 100644 --- a/InventoryDataExporter/Model/Provider/InfiniteStock.php +++ b/InventoryDataExporter/Model/Provider/InfiniteStock.php @@ -67,8 +67,10 @@ private function getIsInfiniteStock(array $row, bool $configManageStock, bool $c if (false === (bool)$row['useConfigManageStock'] && isset($row['manageStock'])) { $isInfinite = !(bool)$row['manageStock']; } - if (false === $isInfinite && false === (bool)$row['useConfigBackorders'] && isset($row['backorders'])) { - $isInfinite = (bool)$row['backorders']; + // With Backorders enabled, and Out-of-Stock Threshold = 0 allows for infinite backorders + if (false === $isInfinite && false === (bool)$row['useConfigBackorders'] + && false === (bool)$row['useConfigMinQty'] && isset($row['backorders'], $row['minQty'])) { + $isInfinite = (bool)$row['backorders'] && (float)$row['minQty'] === 0.0; } return $isInfinite; } diff --git a/InventoryDataExporter/Model/Query/InventoryStockQuery.php b/InventoryDataExporter/Model/Query/InventoryStockQuery.php index ac4ede55..21dda11e 100644 --- a/InventoryDataExporter/Model/Query/InventoryStockQuery.php +++ b/InventoryDataExporter/Model/Query/InventoryStockQuery.php @@ -66,17 +66,43 @@ public function getQuery(array $skus): Select } $select = $connection->select() ->from(['isi' => $this->getTable(sprintf('inventory_stock_%s', $stockId))], []) - ->where('isi.sku IN (?)', $skus) + ->joinLeft( + [ + 'product' => $this->resourceConnection->getTableName('catalog_product_entity'), + ], + 'product.sku = isi.sku', + [] + )->joinLeft( + [ + 'stock_item' => $this->resourceConnection->getTableName('cataloginventory_stock_item'), + ], + 'stock_item.product_id = product.entity_id', + [] + )->where('isi.sku IN (?)', $skus) ->columns( [ 'qty' => "isi.quantity", 'isSalable' => "isi.is_salable", 'sku' => "isi.sku", 'stockId' => new Expression($stockId), - 'manageStock' => new Expression(1), - 'useConfigManageStock' => new Expression(1), - 'backorders' => new Expression(0), - 'useConfigBackorders' => new Expression(1), + 'manageStock' => $connection->getCheckSql( + 'stock_item.manage_stock IS NULL', 1, 'stock_item.manage_stock' + ), + 'useConfigManageStock' => $connection->getCheckSql( + 'stock_item.use_config_manage_stock IS NULL', 1, 'stock_item.use_config_manage_stock' + ), + 'backorders' => $connection->getCheckSql( + 'stock_item.backorders IS NULL', 0, 'stock_item.backorders' + ), + 'useConfigBackorders' => $connection->getCheckSql( + 'stock_item.use_config_backorders IS NULL', 1, 'stock_item.use_config_backorders' + ), + 'useConfigMinQty' => $connection->getCheckSql( + 'stock_item.use_config_min_qty IS NULL', 1, 'stock_item.use_config_min_qty' + ), + 'minQty' => $connection->getCheckSql( + 'stock_item.min_qty IS NULL', 0, 'stock_item.min_qty' + ), ] ); @@ -115,6 +141,8 @@ public function getQueryForDefaultStock(array $skus): Select 'useConfigManageStock' => 'stock_item.use_config_manage_stock', 'backorders' => 'stock_item.backorders', 'useConfigBackorders' => 'stock_item.use_config_backorders', + 'useConfigMinQty' => 'stock_item.use_config_min_qty', + 'minQty' => 'stock_item.min_qty', ]); return $select; } diff --git a/InventoryDataExporter/README.md b/InventoryDataExporter/README.md index 00ded704..a4a8fb66 100644 --- a/InventoryDataExporter/README.md +++ b/InventoryDataExporter/README.md @@ -1,3 +1,9 @@ ## Release notes -*Magento_InventoryDataExporter* module \ No newline at end of file +*Magento_InventoryDataExporter* module + +https://docs.magento.com/user-guide/catalog/inventory-backorders.html?itm_source=devdocs&itm_medium=quick_search&itm_campaign=federated_search&itm_term=backorer + + +Zero +With Backorders enabled, entering 0 allows for infinite backorders. \ No newline at end of file diff --git a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php index c794c6dc..4fdb60c5 100644 --- a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php +++ b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php @@ -43,6 +43,7 @@ public function testExportStockStatuses() ['sku' => 'product_with_default_stock_only'], ['sku' => 'product_with_disabled_manage_stock'], ['sku' => 'product_with_enabled_backorders'], + ['sku' => 'product_in_US_stock_with_disabled_source'], ] ); @@ -105,7 +106,7 @@ private function getExpectedStockStatus(): array 'product_in_EU_stock_with_2_sources' => [ 'stockId' => '10', 'sku' => 'product_in_EU_stock_with_2_sources', - 'qty' => 9.5, + 'qty' => 9.5, // 5.5 (eu-1) + 4 (eu-2) 'qtyForSale' => 9.5, 'infiniteStock' => false, 'isSalable' => true, @@ -129,22 +130,30 @@ private function getExpectedStockStatus(): array 'infiniteStock' => false, 'isSalable' => true, ], + 'product_in_US_stock_with_disabled_source' => [ + 'stockId' => '20', + 'sku' => 'product_in_US_stock_with_disabled_source', + 'qty' => 0, + 'qtyForSale' => 0, + 'infiniteStock' => false, + 'isSalable' => false, + ], ], // Global Stock '30' => [ 'product_in_Global_stock_with_3_sources' => [ 'stockId' => '30', 'sku' => 'product_in_Global_stock_with_3_sources', - 'qty' => 7, - 'qtyForSale' => 7, + 'qty' => 5, // 1 (eu-1) + 4 (us-1) + 'qtyForSale' => 5, 'infiniteStock' => false, 'isSalable' => true, ], 'product_in_EU_stock_with_2_sources' => [ 'stockId' => '30', 'sku' => 'product_in_EU_stock_with_2_sources', - 'qty' => 9.5, // eu1 + eu2 - 'qtyForSale' => 9.5, + 'qty' => 5.5, // eu-1 only + 'qtyForSale' => 5.5, 'infiniteStock' => false, 'isSalable' => true, ], diff --git a/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php b/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php new file mode 100644 index 00000000..3e7c75da --- /dev/null +++ b/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php @@ -0,0 +1,202 @@ +resource = Bootstrap::getObjectManager()->create(ResourceConnection::class); + $this->connection = $this->resource->getConnection(); + $this->indexer = Bootstrap::getObjectManager()->create(Indexer::class); + $this->jsonSerializer = Bootstrap::getObjectManager()->create(Json::class); + $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + $this->storeManager = Bootstrap::getObjectManager()->create(StoreManagerInterface::class); + $this->stockStatusFeed = Bootstrap::getObjectManager()->get(FeedPool::class)->getFeed('stock_statuses'); + $this->attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class); + $this->arrayUtils = $objectManager->create(ArrayUtils::class); + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->sourceItemsFactory = Bootstrap::getObjectManager()->get(SourceItemInterfaceFactory::class); + $this->sourceItemsSave = Bootstrap::getObjectManager()->get(SourceItemsSaveInterface::class); + } + + /** + * @magentoDataFixture Magento_InventoryDataExporter::Test/_files/products_with_sources.php + */ + public function testSourceItemQtyUpdated() + { + $sourceItem = $this->sourceItemsFactory->create(['data' => [ + SourceItemInterface::SOURCE_CODE => 'eu-2', + SourceItemInterface::SKU => 'product_in_EU_stock_with_2_sources', + SourceItemInterface::QUANTITY => 2, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ]]); + $this->sourceItemsSave->execute([$sourceItem]); + + $sku = 'product_in_EU_stock_with_2_sources'; + $this->runIndexer([$sku]); + $feedData = $this->getFeedData([$sku]); + + self::assertEquals( + [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'stock_id' => 10, + 'qty' => 7.5 // 5.5 (eu-1) + 2 (changed for eu-2) + ], + [ + 'sku' => $feedData[10][$sku]['sku'], + 'stock_id' => $feedData[10][$sku]['stockId'], + 'qty' => $feedData[10][$sku]['qty'], + ] + ); + // for Global Stock value remains the same + self::assertEquals( + [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'stock_id' => 30, + 'qty' => 5.5 // 5.5 (eu-1) + ], + [ + 'sku' => $feedData[30][$sku]['sku'], + 'stock_id' => $feedData[30][$sku]['stockId'], + 'qty' => $feedData[30][$sku]['qty'], + ] + ); + + } + + /** + * @param array $skus + * @return array[stock][sku] + */ + private function getFeedData(array $skus): array + { + $output = []; + foreach ($this->stockStatusFeed->getFeedSince('1')['feed'] as $item) { + if (in_array($item['sku'], $skus, true)) { + $output[$item['stockId']][$item['sku']] = $item; + } + } + return $output; + } + + /** + * @param string[] $skus + * @return void + * + * @throws RuntimeException + */ + private function runIndexer(array $skus): void + { + try { + $this->indexer->load(self::STOCK_STATUS_FEED_INDEXER); + $this->indexer->reindexList($skus); + } catch (Throwable $e) { + throw new RuntimeException('Could not reindex stock status export index: ' . $e->getMessage()); + } + } +} diff --git a/InventoryDataExporter/Test/_files/products_with_sources.php b/InventoryDataExporter/Test/_files/products_with_sources.php index 18ca3efc..e3382c0b 100644 --- a/InventoryDataExporter/Test/_files/products_with_sources.php +++ b/InventoryDataExporter/Test/_files/products_with_sources.php @@ -32,11 +32,6 @@ use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory; use Magento\InventoryApi\Api\SourceItemsSaveInterface; - -const TEST_EU_STOCK_ID = 10; -const TEST_US_STOCK_ID = 20; -const TEST_GLOBAL_STOCK_ID = 30; - /** * Generate the following structure: * @@ -47,6 +42,7 @@ * * US_stock (id: 20) * - us-1 + * - us-2 (OUT OF STOCK) * * Global_stock (id: 30) * - eu-1 @@ -61,15 +57,19 @@ * manage_stock = false * * product_with_enabled_backorders + * min_stock = 0 (with enabled backorders and out-of-Stock Threshold = 0 we assume infinitive stock * * product_in_EU_stock_with_2_sources * - eu-1 - 5.5qty - * - eu-2 - 3qty + * - eu-2 - 4qty * * product_in_Global_stock_with_3_sources * - eu-1 - 1qty * - eu-2 - 2qty - * - us-1 - 3qty + * - us-1 - 4qty + * + * product_in_US_stock_with_disabled_source + * - us-2 - 5qty */ @@ -94,15 +94,15 @@ function createStocks(): void $stocksData = [ [ // define only required and needed for tests fields - StockInterface::STOCK_ID => TEST_EU_STOCK_ID, + StockInterface::STOCK_ID => 10, StockInterface::NAME => 'EU_stock', ], [ - StockInterface::STOCK_ID => TEST_US_STOCK_ID, + StockInterface::STOCK_ID => 20, StockInterface::NAME => 'US_stock', ], [ - StockInterface::STOCK_ID => TEST_GLOBAL_STOCK_ID, + StockInterface::STOCK_ID => 30, StockInterface::NAME => 'Global_stock', ] ]; @@ -149,6 +149,13 @@ function createSources(): void SourceInterface::POSTCODE => 'postcode', SourceInterface::COUNTRY_ID => 'US', ], + [ + SourceInterface::SOURCE_CODE => 'us-2', + SourceInterface::NAME => 'US-source-2', + SourceInterface::ENABLED => true, + SourceInterface::POSTCODE => 'postcode', + SourceInterface::COUNTRY_ID => 'US', + ], ]; foreach ($sourcesData as $sourceData) { /** @var SourceInterface $source */ @@ -172,32 +179,32 @@ function assignSourceToStock(): void $linksData = [ [ - StockSourceLinkInterface::STOCK_ID => TEST_EU_STOCK_ID, + StockSourceLinkInterface::STOCK_ID => 10, StockSourceLinkInterface::SOURCE_CODE => 'eu-1', StockSourceLinkInterface::PRIORITY => 1, ], [ - StockSourceLinkInterface::STOCK_ID => TEST_EU_STOCK_ID, + StockSourceLinkInterface::STOCK_ID => 10, StockSourceLinkInterface::SOURCE_CODE => 'eu-2', StockSourceLinkInterface::PRIORITY => 2, ], [ - StockSourceLinkInterface::STOCK_ID => TEST_US_STOCK_ID, + StockSourceLinkInterface::STOCK_ID => 20, StockSourceLinkInterface::SOURCE_CODE => 'us-1', StockSourceLinkInterface::PRIORITY => 1, ], [ - StockSourceLinkInterface::STOCK_ID => TEST_GLOBAL_STOCK_ID, - StockSourceLinkInterface::SOURCE_CODE => 'eu-1', + StockSourceLinkInterface::STOCK_ID => 20, + StockSourceLinkInterface::SOURCE_CODE => 'us-2', StockSourceLinkInterface::PRIORITY => 1, ], [ - StockSourceLinkInterface::STOCK_ID => TEST_GLOBAL_STOCK_ID, - StockSourceLinkInterface::SOURCE_CODE => 'eu-2', + StockSourceLinkInterface::STOCK_ID => 30, + StockSourceLinkInterface::SOURCE_CODE => 'eu-1', StockSourceLinkInterface::PRIORITY => 1, ], [ - StockSourceLinkInterface::STOCK_ID => TEST_GLOBAL_STOCK_ID, + StockSourceLinkInterface::STOCK_ID => 30, StockSourceLinkInterface::SOURCE_CODE => 'us-1', StockSourceLinkInterface::PRIORITY => 2, ], @@ -237,7 +244,7 @@ function createProducts() 'qty' => 5, 'is_in_stock' => true, 'manage_stock' => true, - 'min_qty' => -3, + 'min_qty' => 0, 'backorders' => true ], 'product_in_EU_stock_with_2_sources' => [ @@ -251,6 +258,11 @@ function createProducts() 'is_in_stock' => true, 'manage_stock' => true ], + 'product_in_US_stock_with_disabled_source' => [ + 'qty' => 5, + 'is_in_stock' => true, + 'manage_stock' => true + ], ]; foreach ($stockData as $sku => $productStockData) { @@ -306,13 +318,12 @@ function assignProductsToSources(): void SourceItemInterface::QUANTITY => 4, SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, ], - // TODO -// [ -// SourceItemInterface::SOURCE_CODE => 'eu-2', -// SourceItemInterface::SKU => 'SKU-3', -// SourceItemInterface::QUANTITY => 6, -// SourceItemInterface::STATUS => SourceItemInterface::STATUS_OUT_OF_STOCK, -// ], + [ + SourceItemInterface::SOURCE_CODE => 'us-2', + SourceItemInterface::SKU => 'product_in_US_stock_with_disabled_source', + SourceItemInterface::QUANTITY => 5, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_OUT_OF_STOCK, + ], ]; $sourceItems = []; diff --git a/InventoryDataExporter/etc/db_schema.xml b/InventoryDataExporter/etc/db_schema.xml index 118ecc1b..63c96b9e 100644 --- a/InventoryDataExporter/etc/db_schema.xml +++ b/InventoryDataExporter/etc/db_schema.xml @@ -10,9 +10,8 @@ - + - stockStatus + + Magento\InventoryDataExporter\Model\Indexer\StockStatusFeed + - + - infiniteStock + Magento\InventoryDataExporter\Model\Indexer\StockStatusFeedIndexMetadata - + + stock_statuses From 2134cb847997a323ac74b80cd865135868fbea90 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Tue, 26 Oct 2021 16:23:35 -0500 Subject: [PATCH 03/18] MDEE-57: Handle is_deleted updates, MDEE-55: Prepare get updatedAt query --- .../Model/Query/StockStatusDeleteQuery.php | 22 +++++++++---------- .../Plugin/BulkSourceUnassign.php | 5 ++++- .../Plugin/MarkItemsAsDeleted.php | 5 ++++- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php index 1a505cdc..6c6f566e 100644 --- a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php +++ b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php @@ -9,6 +9,7 @@ use Magento\DataExporter\Model\Indexer\FeedIndexMetadata; use Magento\Framework\App\ResourceConnection; +use Magento\InventoryDataExporter\Model\Provider\StockStatusIdBuilder; /** * Stock Status mark as deleted query builder @@ -66,21 +67,18 @@ public function getStocksAssignedToSkus(array $skus): array /** * Mark stock statuses as deleted * - * @param array $stocksToDelete + * @param array $idsToDelete */ - public function markStockStatusesAsDeleted(array $stocksToDelete): void + public function markStockStatusesAsDeleted(array $idsToDelete): void { $connection = $this->resourceConnection->getConnection(); $feedTableName = $this->resourceConnection->getTableName($this->metadata->getFeedTableName()); - foreach ($stocksToDelete as $stockId => $skus) { - $connection->update( - $feedTableName, - ['is_deleted' => new \Zend_Db_Expr('1')], - [ - 'sku IN (?)' => $skus, - 'stock_id = ?' => $stockId - ] - ); - } + $connection->update( + $feedTableName, + ['is_deleted' => new \Zend_Db_Expr('1')], + [ + 'id IN (?)' => $idsToDelete + ] + ); } } diff --git a/InventoryDataExporter/Plugin/BulkSourceUnassign.php b/InventoryDataExporter/Plugin/BulkSourceUnassign.php index bb6fbd22..c501c959 100644 --- a/InventoryDataExporter/Plugin/BulkSourceUnassign.php +++ b/InventoryDataExporter/Plugin/BulkSourceUnassign.php @@ -5,6 +5,7 @@ */ namespace Magento\InventoryDataExporter\Plugin; +use Magento\InventoryDataExporter\Model\Provider\StockStatusIdBuilder; use Magento\InventoryDataExporter\Model\Query\StockStatusDeleteQuery; /** @@ -60,7 +61,9 @@ private function getStocksToDelete(array $affectedSkus, array $deletedSources, $ foreach ($affectedSkus as $deletedItemSku) { foreach ($fetchedSourceItems[$deletedItemSku] as $fetchedItemStockId => $fetchedItemSources) { if ($this->getContainsAllKeys($fetchedItemSources, $deletedSources)) { - $stocksToDelete[(string)$fetchedItemStockId][] = $deletedItemSku; + $stocksToDelete[] = StockStatusIdBuilder::build( + ['stockId' => (string)$fetchedItemStockId, 'sku' => $deletedItemSku] + ); } } } diff --git a/InventoryDataExporter/Plugin/MarkItemsAsDeleted.php b/InventoryDataExporter/Plugin/MarkItemsAsDeleted.php index 493793a1..0e883038 100644 --- a/InventoryDataExporter/Plugin/MarkItemsAsDeleted.php +++ b/InventoryDataExporter/Plugin/MarkItemsAsDeleted.php @@ -7,6 +7,7 @@ use Magento\Inventory\Model\ResourceModel\SourceItem\DeleteMultiple; use Magento\InventoryApi\Api\Data\SourceItemInterface; +use Magento\InventoryDataExporter\Model\Provider\StockStatusIdBuilder; use Magento\InventoryDataExporter\Model\Query\StockStatusDeleteQuery; /** @@ -65,7 +66,9 @@ private function getStocksToDelete(array $deletedSourceItems, $fetchedSourceItem foreach ($deletedSourceItems as $deletedItemSku => $deletedItemSources) { foreach ($fetchedSourceItems[$deletedItemSku] as $fetchedItemStockId => $fetchedItemSources) { if ($this->getContainsAllKeys($fetchedItemSources, $deletedItemSources)) { - $stocksToDelete[(string)$fetchedItemStockId][] = $deletedItemSku; + $stocksToDelete[] = StockStatusIdBuilder::build( + ['stockId' => (string)$fetchedItemStockId, 'sku' => $deletedItemSku] + ); } } } From 7db33b48c47eb2c75abb2a3bae9d6c11983e53d4 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Tue, 26 Oct 2021 16:24:37 -0500 Subject: [PATCH 04/18] MDEE-57: Handle is_deleted updates, MDEE-55: Prepare get updatedAt query --- InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php | 1 - 1 file changed, 1 deletion(-) diff --git a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php index 6c6f566e..06b79551 100644 --- a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php +++ b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php @@ -9,7 +9,6 @@ use Magento\DataExporter\Model\Indexer\FeedIndexMetadata; use Magento\Framework\App\ResourceConnection; -use Magento\InventoryDataExporter\Model\Provider\StockStatusIdBuilder; /** * Stock Status mark as deleted query builder From b4e71d73e0762316868e0dc1a0473b51c008b3d1 Mon Sep 17 00:00:00 2001 From: Misha Slabko Date: Tue, 26 Oct 2021 16:26:31 -0500 Subject: [PATCH 05/18] MDEE-40:[Commerce Export] Implement stock_item_status feed --- .../Integration/PartialReindexCheckTest.php | 65 +------------------ 1 file changed, 2 insertions(+), 63 deletions(-) diff --git a/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php b/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php index 3e7c75da..4053e7b1 100644 --- a/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php +++ b/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php @@ -11,18 +11,10 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\DataExporter\Model\FeedInterface; use Magento\DataExporter\Model\FeedPool; -use Magento\Eav\Model\AttributeRepository; -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\DB\Adapter\AdapterInterface; -use Magento\Framework\Registry; -use Magento\Framework\Serialize\Serializer\Json; -use Magento\Framework\Stdlib\ArrayUtils; use Magento\Indexer\Model\Indexer; use Magento\InventoryApi\Api\Data\SourceItemInterface; use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory; use Magento\InventoryApi\Api\SourceItemsSaveInterface; -use Magento\ProductVariantDataExporter\Model\Provider\ProductVariants\ConfigurableId; -use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -39,60 +31,15 @@ class PartialReindexCheckTest extends TestCase */ private const STOCK_STATUS_FEED_INDEXER = 'inventory_data_exporter_stock_status'; - /** - * @var ResourceConnection - */ - protected $resource; - - /** - * @var AdapterInterface - */ - protected $connection; - /** * @var Indexer */ - protected $indexer; - - /** - * @var Json - */ - protected $jsonSerializer; - - /** - * @var ProductRepositoryInterface - */ - protected $productRepository; - - /** - * @var StoreManagerInterface - */ - protected $storeManager; + private $indexer; /** * @var FeedInterface */ - protected $stockStatusFeed; - - /** - * @var AttributeRepository - */ - protected $attributeRepository; - - /** - * @var ArrayUtils - */ - protected $arrayUtils; - - /** - * @var Registry - */ - protected $registry; - - /** - * @var ConfigurableId|mixed - */ - protected $idResolver; + private $stockStatusFeed; /** * @var SourceItemsSaveInterface @@ -109,17 +56,9 @@ class PartialReindexCheckTest extends TestCase */ protected function setUp(): void { - $objectManager = Bootstrap::getObjectManager(); - $this->resource = Bootstrap::getObjectManager()->create(ResourceConnection::class); - $this->connection = $this->resource->getConnection(); $this->indexer = Bootstrap::getObjectManager()->create(Indexer::class); - $this->jsonSerializer = Bootstrap::getObjectManager()->create(Json::class); $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); - $this->storeManager = Bootstrap::getObjectManager()->create(StoreManagerInterface::class); $this->stockStatusFeed = Bootstrap::getObjectManager()->get(FeedPool::class)->getFeed('stock_statuses'); - $this->attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class); - $this->arrayUtils = $objectManager->create(ArrayUtils::class); - $this->registry = Bootstrap::getObjectManager()->get(Registry::class); $this->sourceItemsFactory = Bootstrap::getObjectManager()->get(SourceItemInterfaceFactory::class); $this->sourceItemsSave = Bootstrap::getObjectManager()->get(SourceItemsSaveInterface::class); } From 7a6404932bf0893a24a374646b99a66400c060ce Mon Sep 17 00:00:00 2001 From: Misha Slabko Date: Tue, 26 Oct 2021 17:27:47 -0500 Subject: [PATCH 06/18] MDEE-40:[Commerce Export] Implement stock_item_status feed - added scheduled reindex test --- .../StockStatusScheduledReindexTest.php | 93 +++++++++++++++++++ .../Test/_files/products_with_sources.php | 32 +++---- .../Test/_files/simple_product.php | 39 ++++++++ .../Test/_files/simple_product_rollback.php | 38 ++++++++ 4 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 InventoryDataExporter/Test/Integration/StockStatusScheduledReindexTest.php create mode 100644 InventoryDataExporter/Test/_files/simple_product.php create mode 100644 InventoryDataExporter/Test/_files/simple_product_rollback.php diff --git a/InventoryDataExporter/Test/Integration/StockStatusScheduledReindexTest.php b/InventoryDataExporter/Test/Integration/StockStatusScheduledReindexTest.php new file mode 100644 index 00000000..28bdca71 --- /dev/null +++ b/InventoryDataExporter/Test/Integration/StockStatusScheduledReindexTest.php @@ -0,0 +1,93 @@ +sourceItemsFactory = Bootstrap::getObjectManager()->get(SourceItemInterfaceFactory::class); + $this->sourceItemsSave = Bootstrap::getObjectManager()->get(SourceItemsSaveInterface::class); + $indexer = Bootstrap::getObjectManager()->create(Indexer::class); + $this->indexer = $indexer->load(self::STOCK_STATUS_FEED_INDEXER); + $this->indexer->setScheduled(true); + } + + protected function tearDown(): void + { + parent::tearDown(); + $changelog = $this->indexer->getView()->getChangelog(); + $currentVersion = $changelog->getVersion(); + $changelog->clear($currentVersion + 1); + $this->indexer->setScheduled(false); + } + + /** + * Verify that change in the source_item will add updated SKU into stock status changelog table + * + * @magentoDataFixture Magento_InventoryDataExporter::Test/_files/simple_product.php + */ + public function testScheduledUpdate() + { + $sku = 'product_without_assigned_source'; + + $currentVersion = $this->indexer->getView()->getChangelog()->getVersion(); + + // check no product added to changelog yet to prevent false-positive result + self::assertEmpty($this->indexer->getView()->getChangelog()->getList(0, $currentVersion + 1)); + + $sourceItem = $this->sourceItemsFactory->create(['data' => [ + SourceItemInterface::SOURCE_CODE => 'default', + SourceItemInterface::SKU => $sku, + SourceItemInterface::QUANTITY => 1, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ]]); + $this->sourceItemsSave->execute([$sourceItem]); + + $currentVersion = $this->indexer->getView()->getChangelog()->getVersion(); + + // verify SKU is present in changelog + self::assertEquals([$sku], $this->indexer->getView()->getChangelog()->getList(0, $currentVersion + 1)); + } +} diff --git a/InventoryDataExporter/Test/_files/products_with_sources.php b/InventoryDataExporter/Test/_files/products_with_sources.php index e3382c0b..b31a38a2 100644 --- a/InventoryDataExporter/Test/_files/products_with_sources.php +++ b/InventoryDataExporter/Test/_files/products_with_sources.php @@ -73,16 +73,10 @@ */ -createStocks(); -createSources(); -assignSourceToStock(); -createProducts(); -assignProductsToSources(); - /** * Create stocks */ -function createStocks(): void +$createStocks = static function (): void { /** @var StockInterfaceFactory $stockFactory */ $stockFactory = Bootstrap::getObjectManager()->get(StockInterfaceFactory::class); @@ -112,12 +106,12 @@ function createStocks(): void $dataObjectHelper->populateWithArray($stock, $stockData, StockInterface::class); $stockRepository->save($stock); } -} +}; /** * Create Sources */ -function createSources(): void +$createSources = static function (): void { /** @var SourceInterfaceFactory $sourceFactory */ $sourceFactory = Bootstrap::getObjectManager()->get(SourceInterfaceFactory::class); @@ -163,12 +157,12 @@ function createSources(): void $dataObjectHelper->populateWithArray($source, $sourceData, SourceInterface::class); $sourceRepository->save($source); } -} +}; /** * Link Source to Stocks */ -function assignSourceToStock(): void +$assignSourceToStock = static function (): void { /** @var DataObjectHelper $dataObjectHelper */ $dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); @@ -218,9 +212,9 @@ function assignSourceToStock(): void $links[] = $link; } $stockSourceLinksSave->execute($links); -} +}; -function createProducts() +$createProducts = static function () { $objectManager = Bootstrap::getObjectManager(); /** @var ProductInterfaceFactory $productFactory */ @@ -276,9 +270,9 @@ function createProducts() ->setStatus(Status::STATUS_ENABLED); $productRepository->save($product); } -} +}; -function assignProductsToSources(): void +$assignProductsToSources = static function (): void { /** @var DataObjectHelper $dataObjectHelper */ $dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); @@ -334,4 +328,10 @@ function assignProductsToSources(): void $sourceItems[] = $sourceItem; } $sourceItemsSave->execute($sourceItems); -} \ No newline at end of file +}; + +$createStocks(); +$createSources(); +$assignSourceToStock(); +$createProducts(); +$assignProductsToSources(); diff --git a/InventoryDataExporter/Test/_files/simple_product.php b/InventoryDataExporter/Test/_files/simple_product.php new file mode 100644 index 00000000..765ea38d --- /dev/null +++ b/InventoryDataExporter/Test/_files/simple_product.php @@ -0,0 +1,39 @@ +get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); + +$stockData = [ + 'product_without_assigned_source' => [ + 'qty' => 8.5, + 'is_in_stock' => true, + 'manage_stock' => true, + ], +]; + +foreach ($stockData as $sku => $productStockData) { + $product = $productFactory->create(); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple Product ' . $sku) + ->setSku($sku) + ->setPrice(10) + ->setStockData($productStockData) + ->setStatus(Status::STATUS_ENABLED); + $productRepository->save($product); +} diff --git a/InventoryDataExporter/Test/_files/simple_product_rollback.php b/InventoryDataExporter/Test/_files/simple_product_rollback.php new file mode 100644 index 00000000..9bd90db2 --- /dev/null +++ b/InventoryDataExporter/Test/_files/simple_product_rollback.php @@ -0,0 +1,38 @@ +create(ProductRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + + +$currentArea = $registry->registry('isSecureArea'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +foreach ($skusToDelete as $productSku) { + $productRepository->deleteById($productSku); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', $currentArea); From 1846c20b34ff15cc00cd0e13841d73d85047a960 Mon Sep 17 00:00:00 2001 From: Misha Slabko Date: Tue, 26 Oct 2021 21:58:39 -0500 Subject: [PATCH 07/18] MDEE-40:[Commerce Export] Implement stock_item_status feed - fix empty query --- .../Model/Provider/StockStatus.php | 13 ++++++++----- .../Model/Query/InventoryStockQuery.php | 10 ++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/InventoryDataExporter/Model/Provider/StockStatus.php b/InventoryDataExporter/Model/Provider/StockStatus.php index 35c4f20b..fb9917cf 100644 --- a/InventoryDataExporter/Model/Provider/StockStatus.php +++ b/InventoryDataExporter/Model/Provider/StockStatus.php @@ -80,12 +80,15 @@ public function get(array $values): array $output = []; try { - $select = $this->query->getQuery($skus); - $cursor = $connection->query($select); $processedSkus = []; - while ($row = $cursor->fetch()) { - $processedSkus[] = $row['sku']; - $output[] = $this->fillWithDefaultValues($row); + $select = $this->query->getQuery($skus); + // $select can be null if no stocks exists except default + if ($select) { + $cursor = $connection->query($select); + while ($row = $cursor->fetch()) { + $processedSkus[] = $row['sku']; + $output[] = $this->fillWithDefaultValues($row); + } } $select = $this->query->getQueryForDefaultStock(\array_diff($skus, $processedSkus)); diff --git a/InventoryDataExporter/Model/Query/InventoryStockQuery.php b/InventoryDataExporter/Model/Query/InventoryStockQuery.php index 21dda11e..19a9cafd 100644 --- a/InventoryDataExporter/Model/Query/InventoryStockQuery.php +++ b/InventoryDataExporter/Model/Query/InventoryStockQuery.php @@ -53,9 +53,9 @@ private function getTable(string $tableName): string * * @param array $skus * @param bool $defaultStock - * @return Select + * @return Select|null */ - public function getQuery(array $skus): Select + public function getQuery(array $skus): ?Select { $connection = $this->resourceConnection->getConnection(); $selects = []; @@ -108,7 +108,7 @@ public function getQuery(array $skus): Select $selects[] = $select; } - return $connection->select()->union($selects, Select::SQL_UNION_ALL); + return $selects ? $connection->select()->union($selects, Select::SQL_UNION_ALL) : null; } /** @@ -122,7 +122,7 @@ public function getQueryForDefaultStock(array $skus): Select { $connection = $this->resourceConnection->getConnection(); $stockId = $this->defaultStockProvider->getId(); - $select = $connection->select() + return $connection->select() ->from(['isi' => $this->getTable(sprintf('inventory_stock_%s', $stockId))], []) ->where('isi.sku IN (?)', $skus) ->joinInner( @@ -144,7 +144,6 @@ public function getQueryForDefaultStock(array $skus): Select 'useConfigMinQty' => 'stock_item.use_config_min_qty', 'minQty' => 'stock_item.min_qty', ]); - return $select; } /** @@ -154,7 +153,6 @@ public function getQueryForDefaultStock(array $skus): Select */ private function getStocks(): array { - // TODO: add batching $connection = $this->resourceConnection->getConnection(); return $connection->fetchCol($connection->select() ->from(['stock' => $this->getTable('inventory_stock')], ['stock_id'])); From ce51061902f3fd496865f356c32abb9e456f47a2 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Wed, 27 Oct 2021 18:53:26 -0500 Subject: [PATCH 08/18] Cover isDeleted and Reservations by tests --- .../Integration/ExportStockStatusTest.php | 133 ++++++++++++++ .../Integration/PartialReindexCheckTest.php | 1 - .../UnassignProductFromStockTest.php | 164 ++++++++++++++++++ 3 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php diff --git a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php index 4fdb60c5..fd7f7de6 100644 --- a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php +++ b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php @@ -68,6 +68,44 @@ public function testExportStockStatuses() } } + /** + * @magentoDataFixture Magento_InventoryDataExporter::Test/_files/products_with_sources_and_reservations.php + */ + public function testExportStockStatusesWithReservations() + { + $actualStockStatus = $this->processor->process( + 'stock_statuses', + [ + ['sku' => 'product_in_EU_stock_with_2_sources'], + ['sku' => 'product_in_Global_stock_with_3_sources'], + ['sku' => 'product_with_default_stock_only'], + ['sku' => 'product_with_disabled_manage_stock'], + ['sku' => 'product_with_enabled_backorders'], + ['sku' => 'product_in_US_stock_with_disabled_source'], + ] + ); + + $actualStockStatusFormatted = []; + foreach ($actualStockStatus as $stockStatus) { + $actualStockStatusFormatted[$stockStatus['stockId']][$stockStatus['sku']] = $stockStatus; + } + foreach ($this->getExpectedStockStatusForReservations() as $stockId => $stockStatuses) { + foreach ($stockStatuses as $sku => $stockStatus) { + if (!isset($actualStockStatusFormatted[$stockId][$sku])) { + self::fail("Cannot find stock status for stock $stockId & sku $sku"); + } + $actualStockStatus = $actualStockStatusFormatted[$stockId][$sku]; + // ignore fields for now + unset($actualStockStatus['id'], $actualStockStatus['lowStock'], $actualStockStatus['updatedAt']); + self::assertEquals( + $stockStatus, + $actualStockStatus, + "Wrong stock status for stock $stockId & sku $sku" + ); + } + } + } + /** * @return \array[][] */ @@ -160,4 +198,99 @@ private function getExpectedStockStatus(): array ], ]; } + + /** + * @return \array[][] + */ + private function getExpectedStockStatusForReservations(): array + { + return [ + // default stock + '1' => [ + 'product_with_default_stock_only' => [ + 'stockId' => '1', + 'sku' => 'product_with_default_stock_only', + 'qty' => 8.5, + 'qtyForSale' => 6.3, + 'infiniteStock' => false, + 'isSalable' => true, + ], + 'product_with_disabled_manage_stock' => [ + 'stockId' => '1', + 'sku' => 'product_with_disabled_manage_stock', + 'qty' => 0, + 'qtyForSale' => 0, + 'infiniteStock' => true, + 'isSalable' => true, + ], + 'product_with_enabled_backorders' => [ + 'stockId' => '1', + 'sku' => 'product_with_enabled_backorders', + 'qty' => 5, + //'qtyForSale' => 0, + // Remove it after qtyForSale will be fixed + 'qtyForSale' => -2.2, + 'infiniteStock' => true, + 'isSalable' => true, + ], + ], + // EU Stock + '10' => [ + 'product_in_EU_stock_with_2_sources' => [ + 'stockId' => '10', + 'sku' => 'product_in_EU_stock_with_2_sources', + 'qty' => 9.5, // 5.5 (eu-1) + 4 (eu-2) + 'qtyForSale' => 0, + 'infiniteStock' => false, + 'isSalable' => true, + ], + 'product_in_Global_stock_with_3_sources' => [ + 'stockId' => '10', + 'sku' => 'product_in_Global_stock_with_3_sources', + 'qty' => 3, // eu1 + eu2 + 'qtyForSale' => 0, + 'infiniteStock' => false, + 'isSalable' => true, + ], + ], + // US Stock + '20' => [ + 'product_in_Global_stock_with_3_sources' => [ + 'stockId' => '20', + 'sku' => 'product_in_Global_stock_with_3_sources', + 'qty' => 4, // us-1 source assigned to both stocks: US & Global + 'qtyForSale' => 2, + 'infiniteStock' => false, + 'isSalable' => true, + ], + 'product_in_US_stock_with_disabled_source' => [ + 'stockId' => '20', + 'sku' => 'product_in_US_stock_with_disabled_source', + 'qty' => 0, + 'qtyForSale' => 0, + 'infiniteStock' => false, + 'isSalable' => false, + ], + ], + // Global Stock + '30' => [ + 'product_in_Global_stock_with_3_sources' => [ + 'stockId' => '30', + 'sku' => 'product_in_Global_stock_with_3_sources', + 'qty' => 5, // 1 (eu-1) + 4 (us-1) + 'qtyForSale' => 2.5, + 'infiniteStock' => false, + 'isSalable' => true, + ], + 'product_in_EU_stock_with_2_sources' => [ + 'stockId' => '30', + 'sku' => 'product_in_EU_stock_with_2_sources', + 'qty' => 5.5, // eu-1 only + 'qtyForSale' => 5.5, + 'infiniteStock' => false, + 'isSalable' => true, + ], + ], + ]; + } } diff --git a/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php b/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php index 4053e7b1..ccb74fd6 100644 --- a/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php +++ b/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php @@ -105,7 +105,6 @@ public function testSourceItemQtyUpdated() 'qty' => $feedData[30][$sku]['qty'], ] ); - } /** diff --git a/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php b/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php new file mode 100644 index 00000000..fde042cf --- /dev/null +++ b/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php @@ -0,0 +1,164 @@ +stockStatusFeed = Bootstrap::getObjectManager()->get(FeedPool::class)->getFeed('stock_statuses'); + $this->sourceItemProcessor = Bootstrap::getObjectManager()->get(SourceItemsProcessorInterface::class); + } + + /** + * @dataProvider stocksDataProvider + * @param string $sku + * @param array $sourcesToLeave + * @param array $expectedData + * @throws InputException + * @magentoDataFixture Magento_InventoryDataExporter::Test/_files/products_with_sources.php + */ + public function testSourceItemStockUnassigned(string $sku, array $sourcesToLeave, array $expectedData) + { + $sourceItems = $this->getSourcesData($sku, $sourcesToLeave); + $this->sourceItemProcessor->execute($sku, $sourceItems); + + $feedData = $this->getFeedData([$sku]); + + $this->verifyResults($feedData, $sku, $expectedData); + } + + /** + * @param array $skus + * @return array[stock][sku] + * @throws \Zend_Db_Statement_Exception + */ + private function getFeedData(array $skus): array + { + $output = []; + foreach ($this->stockStatusFeed->getFeedSince('1')['feed'] as $item) { + if (\in_array($item['sku'], $skus, true)) { + $output[$item['stockId']][$item['sku']] = $item; + } + } + return $output; + } + + /** + * @return array[] + */ + public function stocksDataProvider(): array + { + return [ + 'one_stock_unassign' => [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'sources_to_leave' => ['eu-2'], + 'expected_data' => [ + '10' => [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'stock_id' => 10, + 'deleted' => false + ], + '30' => [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'stock_id' => 30, + 'deleted' => true + ] + ] + ], + 'unassign_sources_from_multiple_stocks' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'sources_to_leave' => ['eu-2'], + 'expected_data' => [ + '10' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 10, + 'deleted' => false + ], + '20' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 20, + 'deleted' => true + ], + '30' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 30, + 'deleted' => true + ] + ] + ] + ]; + } + + /** + * @param string $sku + * @param array $sourcesToUnassign + * @return array + */ + private function getSourcesData(string $sku, array $sourcesToUnassign): array + { + $sourcesData = []; + foreach ($sourcesToUnassign as $sourceCode) { + $sourcesData[] = [ + SourceItemInterface::SOURCE_CODE => $sourceCode, + SourceItemInterface::SKU => $sku + ]; + } + + return $sourcesData; + } + + /** + * @param array $feedData + * @param string $sku + * @param array $expectedData + */ + private function verifyResults(array $feedData, string $sku, array $expectedData): void + { + foreach ($expectedData as $expectedStockId => $expectedStockData) { + self::assertEquals( + $expectedStockData, + [ + 'sku' => $feedData[$expectedStockId][$sku]['sku'], + 'stock_id' => $feedData[$expectedStockId][$sku]['stockId'], + 'deleted' => $feedData[$expectedStockId][$sku]['deleted'], + ] + ); + } + } +} From e3179ca36fed644c193acd8fc2c599fb60bd2727 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Wed, 27 Oct 2021 18:54:02 -0500 Subject: [PATCH 09/18] Cover isDeleted and Reservations by tests --- ...products_with_sources_and_reservations.php | 96 +++++++++++++++++++ ...with_sources_and_reservations_rollback.php | 36 +++++++ 2 files changed, 132 insertions(+) create mode 100644 InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php create mode 100644 InventoryDataExporter/Test/_files/products_with_sources_and_reservations_rollback.php diff --git a/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php new file mode 100644 index 00000000..86c3f2b6 --- /dev/null +++ b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php @@ -0,0 +1,96 @@ +requireDataFixture( + 'Magento/InventoryDataExporter/Test/_files/products_with_sources.php' +); + +/** + * Create reservations + */ +$createReservations = static function (): void +{ + $productsList = [ + [ + 'sku' => 'product_with_default_stock_only', + 'qty_by_stocks' => [ + ['stock_id' => 1, 'qty' => -2.2] //default stock 5.5 left + ], + ], + [ + 'sku' => 'product_with_disabled_manage_stock', + 'qty_by_stocks' => [ + ['stock_id' => 1, 'qty' => -2.2] //unlimited + ] + ], + [ + 'sku' => 'product_with_enabled_backorders', + 'qty_by_stocks' => [ + ['stock_id' => 1, 'qty' => -7.2] //unlimited + ] + ], + [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'qty_by_stocks' => [ + ['stock_id' => 10, 'qty' => -9.5] //eu-1, eu-2 - 4.5 left + ] + ], + [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'qty_by_stocks' => [ + ['stock_id' => 10, 'qty' => -3], //eu-1, eu-2 - 4.5 left + ['stock_id' => 20, 'qty' => -2], //us-1 - 2 left + ['stock_id' => 30, 'qty' => -2.5], + ] + ] + ]; + + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->create(ProductRepositoryInterface::class); + /** @var ReservationBuilderInterface $reservationBuilder */ + $reservationBuilder = $objectManager->get(ReservationBuilderInterface::class); + $isSourceItemManagementAllowedForProductType = $objectManager->get( + IsSourceItemManagementAllowedForProductTypeInterface::class + ); + /** @var AppendReservationsInterface $appendReservations */ + $appendReservations = $objectManager->get(AppendReservationsInterface::class); + $reservations = []; + foreach ($productsList as $productData) { + $product = $productRepository->get($productData['sku']); + $skusToReindex[] = $productData['sku']; + if ($isSourceItemManagementAllowedForProductType->execute($product->getTypeId())) { + foreach ($productData['qty_by_stocks'] as $stockData) { + $reservations[] = $reservationBuilder + ->setSku($productData['sku']) + ->setQuantity((float)$stockData['qty']) + ->setStockId($stockData['stock_id']) + ->setMetadata() + ->build(); + } + } + } + + $appendReservations->execute($reservations); +}; + +$createReservations(); diff --git a/InventoryDataExporter/Test/_files/products_with_sources_and_reservations_rollback.php b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations_rollback.php new file mode 100644 index 00000000..d3d42380 --- /dev/null +++ b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations_rollback.php @@ -0,0 +1,36 @@ +get(CleanupReservationsInterface::class); +$cleanupReservations->execute(); + +/** @var \Magento\Framework\App\ResourceConnection $resourceConnection */ +$resourceConnection = Bootstrap::getObjectManager()->create(\Magento\Framework\App\ResourceConnection::class); +$connection = $resourceConnection->getConnection(); +$reservationTable = $connection->getTableName('inventory_reservation'); + +$select = $connection->select() + ->from( + $reservationTable, + ['GROUP_CONCAT(' . ReservationInterface::RESERVATION_ID . ')'] + ); +$reservationIds = implode(',', $connection->fetchCol($select)); + +$condition = [ReservationInterface::RESERVATION_ID . ' IN (?)' => explode(',', $reservationIds)]; +$connection->delete($reservationTable, $condition); + +Resolver::getInstance()->requireDataFixture( + 'Magento/InventoryDataExporter/Test/_files/products_with_sources_rollback.php' +); From 57e908d2a13ea05bf60938a2f3770360e7b0db4a Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Wed, 27 Oct 2021 18:54:46 -0500 Subject: [PATCH 10/18] Use interface in plugin --- InventoryDataExporter/Plugin/BulkSourceUnassign.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InventoryDataExporter/Plugin/BulkSourceUnassign.php b/InventoryDataExporter/Plugin/BulkSourceUnassign.php index c501c959..b294f16b 100644 --- a/InventoryDataExporter/Plugin/BulkSourceUnassign.php +++ b/InventoryDataExporter/Plugin/BulkSourceUnassign.php @@ -30,7 +30,7 @@ public function __construct( /** * Check which stocks will be unassigned from products and mark them as deleted in feed table * - * @param \Magento\InventoryCatalog\Model\ResourceModel\BulkSourceUnassign $subject + * @param \Magento\InventoryCatalogApi\Api\BulkSourceUnassignInterface $subject * @param array $skus * @param array $sourceCodes * @return void From 32f6a7f16bf3aee3f332f06828c987aad12801e0 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Thu, 28 Oct 2021 16:54:37 -0500 Subject: [PATCH 11/18] MDEE-57: Handle is_deleted updates --- .../Model/Provider/StockStatus.php | 3 +- .../Model/Query/InventoryStockQuery.php | 12 +- .../Model/Query/StockStatusDeleteQuery.php | 25 ++ .../Plugin/BulkSourceUnassign.php | 47 ++- .../Integration/ExportStockStatusTest.php | 34 ++ .../UnassignProductFromStockTest.php | 291 +++++++++++++++++- .../Test/_files/products_with_sources.php | 24 ++ ...products_with_sources_and_reservations.php | 8 +- .../_files/products_with_sources_rollback.php | 7 +- InventoryDataExporter/etc/di.xml | 5 +- 10 files changed, 422 insertions(+), 34 deletions(-) diff --git a/InventoryDataExporter/Model/Provider/StockStatus.php b/InventoryDataExporter/Model/Provider/StockStatus.php index fb9917cf..34a6e36e 100644 --- a/InventoryDataExporter/Model/Provider/StockStatus.php +++ b/InventoryDataExporter/Model/Provider/StockStatus.php @@ -86,12 +86,11 @@ public function get(array $values): array if ($select) { $cursor = $connection->query($select); while ($row = $cursor->fetch()) { - $processedSkus[] = $row['sku']; $output[] = $this->fillWithDefaultValues($row); } } - $select = $this->query->getQueryForDefaultStock(\array_diff($skus, $processedSkus)); + $select = $this->query->getQueryForDefaultStock($skus); $cursor = $connection->query($select); while ($row = $cursor->fetch()) { $output[] = $this->fillWithDefaultValues($row); diff --git a/InventoryDataExporter/Model/Query/InventoryStockQuery.php b/InventoryDataExporter/Model/Query/InventoryStockQuery.php index 19a9cafd..9fac27ee 100644 --- a/InventoryDataExporter/Model/Query/InventoryStockQuery.php +++ b/InventoryDataExporter/Model/Query/InventoryStockQuery.php @@ -131,7 +131,17 @@ public function getQueryForDefaultStock(array $skus): Select ], 'stock_item.product_id = isi.product_id', [] - )->columns( + )->joinLeft( + [ + 'stock_link' => $this->resourceConnection->getTableName('inventory_source_stock_link') + ], + 'stock_link.stock_id = 1' + )->joinLeft( + [ + 'source_item' => 'inventory_source_item' + ], + 'source_item.source_code = stock_link.stock_id and source_item.sku = isi.sku') + ->columns( [ 'qty' => "isi.quantity", 'isSalable' => "isi.is_salable", diff --git a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php index 06b79551..e68899bf 100644 --- a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php +++ b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php @@ -63,6 +63,31 @@ public function getStocksAssignedToSkus(array $skus): array return $fetchedSourceItems; } + /** + * Get stocks which are assigned to the list of provided SKUs + * + * @param array $sourceCodes + * @return array + */ + public function getStocksWithSources(array $sourceCodes): array + { + $connection = $this->resourceConnection->getConnection(); + $sourceLinkTableName = $this->resourceConnection->getTableName('inventory_source_stock_link'); + $select = $connection->select() + ->from( + ['source_stock_link' => $sourceLinkTableName], + ['source_stock_link.stock_id', 'source_stock_link_all_sources.source_code'] + )->joinLeft( + ['source_stock_link_all_sources' => $sourceLinkTableName], + 'source_stock_link_all_sources.source_code = source_stock_link.source_code' + )->group('source_stock_link_all_sources.link_id'); + $stocks = []; + foreach ($connection->fetchAll($select) as $stockData) { + $stocks[$stockData['stock_id']][] = $stockData['source_code']; + } + return $stocks; + } + /** * Mark stock statuses as deleted * diff --git a/InventoryDataExporter/Plugin/BulkSourceUnassign.php b/InventoryDataExporter/Plugin/BulkSourceUnassign.php index 424e4886..a1fd053c 100644 --- a/InventoryDataExporter/Plugin/BulkSourceUnassign.php +++ b/InventoryDataExporter/Plugin/BulkSourceUnassign.php @@ -30,49 +30,66 @@ public function __construct( /** * Check which stocks will be unassigned from products and mark them as deleted in feed table * - * @param \Magento\InventoryCatalogApi\Api\BulkSourceUnassignInterface $subject + * @param \Magento\InventoryCatalog\Model\ResourceModel\BulkSourceUnassign $subject + * @param int $result * @param array $skus * @param array $sourceCodes - * @return void + * @return int * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeExecute( + public function afterExecute( \Magento\InventoryCatalog\Model\ResourceModel\BulkSourceUnassign $subject, + int $result, array $skus, array $sourceCodes - ): void { - $fetchedSourceItems = $this->stockStatusDeleteQuery->getStocksAssignedToSkus($skus); - $stocksToDelete = $this->getStocksToDelete($skus, $sourceCodes, $fetchedSourceItems); + ): int { + $sourcesAssignedToProducts = $this->stockStatusDeleteQuery->getStocksAssignedToSkus($skus); + $sourcesByStocks = $this->stockStatusDeleteQuery->getStocksWithSources($sourceCodes); + $stocksToDelete = $this->getStocksToDelete($skus, $sourcesByStocks, $sourcesAssignedToProducts); if (!empty($stocksToDelete)) { $this->stockStatusDeleteQuery->markStockStatusesAsDeleted($stocksToDelete); } + + return $result; } /** * @param array $affectedSkus - * @param array $deletedSources - * @param $fetchedSourceItems + * @param array $sourcesByStocks + * @param array $sourcesAssignedToProducts * @return array */ - private function getStocksToDelete(array $affectedSkus, array $deletedSources, array $fetchedSourceItems): array - { + private function getStocksToDelete( + array $affectedSkus, + array $sourcesByStocks, + array $sourcesAssignedToProducts + ): array { $stocksToDelete = []; foreach ($affectedSkus as $deletedItemSku) { - if (!isset($fetchedSourceItems[$deletedItemSku])) { + foreach (array_keys($sourcesByStocks) as $stockId) { + $stocksToDelete[] = StockStatusIdBuilder::build( + ['stockId' => (string)$stockId, 'sku' => $deletedItemSku] + ); + } + if (!isset($sourcesAssignedToProducts[$deletedItemSku])) { continue ; } - foreach ($fetchedSourceItems[$deletedItemSku] as $fetchedItemStockId => $fetchedItemSources) { - if ($this->getContainsAllKeys($fetchedItemSources, $deletedSources)) { - $stocksToDelete[] = StockStatusIdBuilder::build( + + foreach ($sourcesAssignedToProducts[$deletedItemSku] as $fetchedItemStockId => $fetchedItemSources) { + if ($this->getContainsAllKeys($fetchedItemSources, $sourcesByStocks[$fetchedItemStockId])) { + $stockStatusId = StockStatusIdBuilder::build( ['stockId' => (string)$fetchedItemStockId, 'sku' => $deletedItemSku] ); + if ($key = \array_search($stockStatusId, $stocksToDelete, false)) { + unset($stocksToDelete[(int)$key]); + } } } } - return $stocksToDelete; + return array_filter($stocksToDelete); } /** diff --git a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php index f7d4f771..177a08b0 100644 --- a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php +++ b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php @@ -41,6 +41,7 @@ public function testExportStockStatuses() ['sku' => 'product_in_EU_stock_with_2_sources'], ['sku' => 'product_in_Global_stock_with_3_sources'], ['sku' => 'product_with_default_stock_only'], + ['sku' => 'product_in_default_and_2_EU_sources'], ['sku' => 'product_with_disabled_manage_stock'], ['sku' => 'product_with_enabled_backorders'], ['sku' => 'product_in_US_stock_with_disabled_source'], @@ -79,6 +80,7 @@ public function testExportStockStatusesWithReservations() ['sku' => 'product_in_EU_stock_with_2_sources'], ['sku' => 'product_in_Global_stock_with_3_sources'], ['sku' => 'product_with_default_stock_only'], + ['sku' => 'product_in_default_and_2_EU_sources'], ['sku' => 'product_with_disabled_manage_stock'], ['sku' => 'product_with_enabled_backorders'], ['sku' => 'product_in_US_stock_with_disabled_source'], @@ -122,6 +124,14 @@ private function getExpectedStockStatus(): array 'infiniteStock' => false, 'isSalable' => true, ], + 'product_in_default_and_2_EU_sources' => [ + 'stockId' => '1', + 'sku' => 'product_in_default_and_2_EU_sources', + 'qty' => 2, + 'qtyForSale' => 2, + 'infiniteStock' => false, + 'isSalable' => true, + ], 'product_with_disabled_manage_stock' => [ 'stockId' => '1', 'sku' => 'product_with_disabled_manage_stock', @@ -157,6 +167,14 @@ private function getExpectedStockStatus(): array 'infiniteStock' => false, 'isSalable' => true, ], + 'product_in_default_and_2_EU_sources' => [ + 'stockId' => '10', + 'sku' => 'product_in_default_and_2_EU_sources', + 'qty' => 9.5, + 'qtyForSale' => 9.5, + 'infiniteStock' => false, + 'isSalable' => true, + ], ], // US Stock '20' => [ @@ -215,6 +233,14 @@ private function getExpectedStockStatusForReservations(): array 'infiniteStock' => false, 'isSalable' => true, ], + 'product_in_default_and_2_EU_sources' => [ + 'stockId' => '1', + 'sku' => 'product_in_default_and_2_EU_sources', + 'qty' => 2.0, + 'qtyForSale' => 1, + 'infiniteStock' => false, + 'isSalable' => true, + ], 'product_with_disabled_manage_stock' => [ 'stockId' => '1', 'sku' => 'product_with_disabled_manage_stock', @@ -252,6 +278,14 @@ private function getExpectedStockStatusForReservations(): array 'infiniteStock' => false, 'isSalable' => true, ], + 'product_in_default_and_2_EU_sources' => [ + 'stockId' => '10', + 'sku' => 'product_in_default_and_2_EU_sources', + 'qty' => 9.5, + 'qtyForSale' => 5.5, + 'infiniteStock' => false, + 'isSalable' => true, + ], ], // US Stock '20' => [ diff --git a/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php b/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php index fde042cf..378355cf 100644 --- a/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php +++ b/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php @@ -8,14 +8,11 @@ namespace Magento\ProductVariantDataExporter\Test\Integration; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\DataExporter\Model\FeedInterface; use Magento\DataExporter\Model\FeedPool; use Magento\Framework\Exception\InputException; -use Magento\Indexer\Model\Indexer; use Magento\InventoryApi\Api\Data\SourceItemInterface; -use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory; -use Magento\InventoryApi\Api\SourceItemsSaveInterface; +use Magento\InventoryCatalogApi\Api\BulkSourceUnassignInterface; use Magento\InventoryCatalogApi\Model\SourceItemsProcessorInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -36,6 +33,11 @@ class UnassignProductFromStockTest extends TestCase */ private $sourceItemProcessor; + /** + * @var BulkSourceUnassignInterface + */ + private $bulkSourceUnassign; + /** * @inheritDoc */ @@ -43,17 +45,18 @@ protected function setUp(): void { $this->stockStatusFeed = Bootstrap::getObjectManager()->get(FeedPool::class)->getFeed('stock_statuses'); $this->sourceItemProcessor = Bootstrap::getObjectManager()->get(SourceItemsProcessorInterface::class); + $this->bulkSourceUnassign = Bootstrap::getObjectManager()->get(BulkSourceUnassignInterface::class); } /** - * @dataProvider stocksDataProvider + * @dataProvider stocksUnassignDataProvider * @param string $sku * @param array $sourcesToLeave * @param array $expectedData * @throws InputException * @magentoDataFixture Magento_InventoryDataExporter::Test/_files/products_with_sources.php */ - public function testSourceItemStockUnassigned(string $sku, array $sourcesToLeave, array $expectedData) + public function te1stSourceItemStockUnassigned(string $sku, array $sourcesToLeave, array $expectedData) { $sourceItems = $this->getSourcesData($sku, $sourcesToLeave); $this->sourceItemProcessor->execute($sku, $sourceItems); @@ -63,6 +66,30 @@ public function testSourceItemStockUnassigned(string $sku, array $sourcesToLeave $this->verifyResults($feedData, $sku, $expectedData); } + /** + * @dataProvider stocksBulkUnassignDataProvider + * @param array $skus + * @param array $sourcesToUnassign + * @param array $expectedData + * @throws InputException + * @throws \Zend_Db_Statement_Exception + * @throws \Magento\Framework\Validation\ValidationException + * @magentoDataFixture Magento_InventoryDataExporter::Test/_files/products_with_sources.php + */ + public function testSourceItemsBulkUnassign(array $skus, array $sourcesToUnassign, array $expectedData) + { + $this->bulkSourceUnassign->execute( + $skus, + $sourcesToUnassign + ); + + $feedData = $this->getFeedData($skus); + + foreach ($skus as $sku) { + $this->verifyResults($feedData, $sku, $expectedData[$sku]); + } + } + /** * @param array $skus * @return array[stock][sku] @@ -82,7 +109,7 @@ private function getFeedData(array $skus): array /** * @return array[] */ - public function stocksDataProvider(): array + public function stocksUnassignDataProvider(): array { return [ 'one_stock_unassign' => [ @@ -121,7 +148,255 @@ public function stocksDataProvider(): array 'deleted' => true ] ] - ] + ], + 'default_stock_unassign_from_only_default_stock_product' => [ + 'sku' => 'product_with_default_stock_only', + 'sources_to_leave' => [], + 'expected_data' => [ + '1' => [ + 'sku' => 'product_with_default_stock_only', + 'stock_id' => 1, + 'deleted' => true + ] + ] + ], + 'default_stock_unassign_from_default_and_custom_stocks_product' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'sources_to_leave' => ['eu1', 'eu2'], + 'expected_data' => [ + '1' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 1, + 'deleted' => true + ], + '10' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 10, + 'deleted' => false + ] + ] + ], + 'custom_stock_unassign_from_default_and_custom_stocks_product' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'sources_to_leave' => ['default'], + 'expected_data' => [ + '1' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 1, + 'deleted' => false + ], + '10' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 10, + 'deleted' => true + ] + ] + ], + ]; + } + + /** + * @return array[] + */ + public function stocksBulkUnassignDataProvider(): array + { + return [ + 'one_stock_unassign' => [ + 'skus' => [ + 'product_in_EU_stock_with_2_sources', + 'product_in_Global_stock_with_3_sources', + 'product_with_default_stock_only', + 'product_in_default_and_2_EU_sources' + ], + 'sources_to_unassign' => ['eu-1', 'eu-2'], + 'expected_data' => [ + 'product_with_default_stock_only' => [ + '1' => [ + 'sku' => 'product_with_default_stock_only', + 'stock_id' => 1, + 'deleted' => false + ], + ], + 'product_in_default_and_2_EU_sources' => [ + '1' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 1, + 'deleted' => false + ], + '10' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 10, + 'deleted' => true + ], + ], + 'product_in_EU_stock_with_2_sources' => [ + '10' => [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'stock_id' => 10, + 'deleted' => true + ], + ], + 'product_in_Global_stock_with_3_sources' => [ + '10' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 10, + 'deleted' => true + ], + '30' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 30, + 'deleted' => false + ], + ] + ] + ], + 'two_stocks_unassign' => [ + 'skus' => [ + 'product_in_EU_stock_with_2_sources', + 'product_in_Global_stock_with_3_sources', + 'product_with_default_stock_only', + 'product_in_default_and_2_EU_sources' + ], + 'sources_to_unassign' => ['eu-1', 'eu-2', 'us-1'], + 'expected_data' => [ + 'product_with_default_stock_only' => [ + '1' => [ + 'sku' => 'product_with_default_stock_only', + 'stock_id' => 1, + 'deleted' => false + ], + ], + 'product_in_default_and_2_EU_sources' => [ + '1' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 1, + 'deleted' => false + ], + '10' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 10, + 'deleted' => true + ], + ], + 'product_in_EU_stock_with_2_sources' => [ + '10' => [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'stock_id' => 10, + 'deleted' => true + ], + ], + 'product_in_Global_stock_with_3_sources' => [ + '10' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 10, + 'deleted' => true + ], + '30' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 30, + 'deleted' => true + ], + ] + ] + ], + 'two_stocks_and_default_unassign' => [ + 'skus' => [ + 'product_in_EU_stock_with_2_sources', + 'product_in_Global_stock_with_3_sources', + 'product_with_default_stock_only', + 'product_in_default_and_2_EU_sources' + ], + 'sources_to_unassign' => ['eu-1', 'eu-2', 'us-1', 'default'], + 'expected_data' => [ + 'product_with_default_stock_only' => [ + '1' => [ + 'sku' => 'product_with_default_stock_only', + 'stock_id' => 1, + 'deleted' => true + ], + ], + 'product_in_default_and_2_EU_sources' => [ + '1' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 1, + 'deleted' => true + ], + '10' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 1, + 'deleted' => true + ], + ], + 'product_in_EU_stock_with_2_sources' => [ + '10' => [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'stock_id' => 10, + 'deleted' => true + ], + ], + 'product_in_Global_stock_with_3_sources' => [ + '10' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 10, + 'deleted' => true + ], + '30' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 30, + 'deleted' => true + ], + ] + ] + ], + 'only_default_stock_unassign' => [ + 'skus' => [ + 'product_in_EU_stock_with_2_sources', + 'product_in_Global_stock_with_3_sources', + 'product_with_default_stock_only', + 'product_in_default_and_2_EU_sources' + ], + 'sources_to_unassign' => ['default'], + 'expected_data' => [ + 'product_with_default_stock_only' => [ + '1' => [ + 'sku' => 'product_with_default_stock_only', + 'stock_id' => 1, + 'deleted' => true + ], + ], + 'product_in_default_and_2_EU_sources' => [ + '1' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 1, + 'deleted' => true + ], + '10' => [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'stock_id' => 1, + 'deleted' => false + ], + ], + 'product_in_EU_stock_with_2_sources' => [ + '10' => [ + 'sku' => 'product_in_EU_stock_with_2_sources', + 'stock_id' => 10, + 'deleted' => false + ], + ], + 'product_in_Global_stock_with_3_sources' => [ + '10' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 10, + 'deleted' => false + ], + '30' => [ + 'sku' => 'product_in_Global_stock_with_3_sources', + 'stock_id' => 30, + 'deleted' => false + ], + ] + ] + ], ]; } diff --git a/InventoryDataExporter/Test/_files/products_with_sources.php b/InventoryDataExporter/Test/_files/products_with_sources.php index 4adb7817..99633636 100644 --- a/InventoryDataExporter/Test/_files/products_with_sources.php +++ b/InventoryDataExporter/Test/_files/products_with_sources.php @@ -230,6 +230,12 @@ 'manage_stock' => true, 'is_qty_decimal' => true ], + 'product_in_default_and_2_EU_sources' => [ + 'qty' => 11.5, + 'is_in_stock' => true, + 'manage_stock' => true, + 'is_qty_decimal' => true + ], 'product_with_disabled_manage_stock' => [ 'use_config_manage_stock' => false, 'manage_stock' => false, @@ -282,6 +288,24 @@ $sourceItemsSave = Bootstrap::getObjectManager()->get(SourceItemsSaveInterface::class); $sourcesItemsData = [ + [ + SourceItemInterface::SOURCE_CODE => 'default', + SourceItemInterface::SKU => 'product_in_default_and_2_EU_sources', + SourceItemInterface::QUANTITY => 2, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ], + [ + SourceItemInterface::SOURCE_CODE => 'eu-1', + SourceItemInterface::SKU => 'product_in_default_and_2_EU_sources', + SourceItemInterface::QUANTITY => 5.5, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ], + [ + SourceItemInterface::SOURCE_CODE => 'eu-2', + SourceItemInterface::SKU => 'product_in_default_and_2_EU_sources', + SourceItemInterface::QUANTITY => 4, + SourceItemInterface::STATUS => SourceItemInterface::STATUS_IN_STOCK, + ], [ SourceItemInterface::SOURCE_CODE => 'eu-1', SourceItemInterface::SKU => 'product_in_EU_stock_with_2_sources', diff --git a/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php index 86c3f2b6..1c11ab2b 100644 --- a/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php +++ b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php @@ -10,7 +10,6 @@ */ declare(strict_types=1); -use Magento\Indexer\Model\Indexer; use Magento\InventoryReservationsApi\Model\AppendReservationsInterface; use Magento\InventoryReservationsApi\Model\ReservationBuilderInterface; use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForProductTypeInterface; @@ -54,6 +53,13 @@ ['stock_id' => 10, 'qty' => -9.5] //eu-1, eu-2 - 4.5 left ] ], + [ + 'sku' => 'product_in_default_and_2_EU_sources', + 'qty_by_stocks' => [ + ['stock_id' => 10, 'qty' => -4], //eu-1, eu-2 - 5.5 left + ['stock_id' => 1, 'qty' => -1], //default - 1 left + ] + ], [ 'sku' => 'product_in_Global_stock_with_3_sources', 'qty_by_stocks' => [ diff --git a/InventoryDataExporter/Test/_files/products_with_sources_rollback.php b/InventoryDataExporter/Test/_files/products_with_sources_rollback.php index b2f90af5..5ff3032d 100644 --- a/InventoryDataExporter/Test/_files/products_with_sources_rollback.php +++ b/InventoryDataExporter/Test/_files/products_with_sources_rollback.php @@ -7,11 +7,7 @@ */ declare(strict_types=1); -use Magento\Catalog\Api\Data\ProductInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\InventoryApi\Api\Data\SourceInterfaceFactory; -use Magento\InventoryApi\Api\Data\StockInterfaceFactory; -use Magento\InventoryApi\Api\Data\StockSourceLinkInterfaceFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\App\ResourceConnection; use Magento\InventoryApi\Api\Data\SourceInterface; @@ -21,7 +17,8 @@ // Delete products $skusToDelete = [ 'product_with_default_stock_only', 'product_with_disabled_manage_stock', 'product_with_enabled_backorders', - 'product_in_EU_stock_with_2_sources', 'product_in_Global_stock_with_3_sources' + 'product_in_EU_stock_with_2_sources', 'product_in_Global_stock_with_3_sources', + 'product_in_default_and_2_EU_sources' ]; $objectManager = Bootstrap::getObjectManager(); diff --git a/InventoryDataExporter/etc/di.xml b/InventoryDataExporter/etc/di.xml index ecc181b9..3e7abd51 100644 --- a/InventoryDataExporter/etc/di.xml +++ b/InventoryDataExporter/etc/di.xml @@ -65,7 +65,8 @@ + type="Magento\InventoryDataExporter\Plugin\BulkSourceUnassign" + sortOrder="10"/> @@ -80,6 +81,6 @@ - + From f15c638a41b73697613b54ab410c17d84f12e188 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Thu, 28 Oct 2021 17:02:02 -0500 Subject: [PATCH 12/18] MDEE-57: Handle is_deleted updates, MDEE-55: Prepare get updatedAt query --- .../Test/Integration/ExportStockStatusTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php index 177a08b0..4773f687 100644 --- a/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php +++ b/InventoryDataExporter/Test/Integration/ExportStockStatusTest.php @@ -252,10 +252,10 @@ private function getExpectedStockStatusForReservations(): array 'product_with_enabled_backorders' => [ 'stockId' => '1', 'sku' => 'product_with_enabled_backorders', - 'qty' => 5, + 'qty' => 5.0, + // Uncomment it after qtyForSale will be fixed //'qtyForSale' => 0, - // Remove it after qtyForSale will be fixed - 'qtyForSale' => -2.2, + 'qtyForSale' => -2.2, // JUST TEMPORARILY FIX. We should not allow negative values 'infiniteStock' => true, 'isSalable' => true, ], From 85683a7293fde745b159e6d779b0ce5238098a47 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Thu, 28 Oct 2021 17:54:59 -0500 Subject: [PATCH 13/18] MDEE-57: Handle is_deleted updates --- InventoryDataExporter/Model/Query/InventoryStockQuery.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InventoryDataExporter/Model/Query/InventoryStockQuery.php b/InventoryDataExporter/Model/Query/InventoryStockQuery.php index 9fac27ee..3dea4ad0 100644 --- a/InventoryDataExporter/Model/Query/InventoryStockQuery.php +++ b/InventoryDataExporter/Model/Query/InventoryStockQuery.php @@ -131,12 +131,12 @@ public function getQueryForDefaultStock(array $skus): Select ], 'stock_item.product_id = isi.product_id', [] - )->joinLeft( + )->joinInner( [ 'stock_link' => $this->resourceConnection->getTableName('inventory_source_stock_link') ], 'stock_link.stock_id = 1' - )->joinLeft( + )->joinInner( [ 'source_item' => 'inventory_source_item' ], From 75d45dbd1f855448f93aee9a116bc333d9c17fd8 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Thu, 28 Oct 2021 20:05:50 -0500 Subject: [PATCH 14/18] MDEE-57: Handle is_deleted updates --- .../Model/Query/InventoryStockQuery.php | 4 ++-- InventoryDataExporter/Plugin/BulkSourceUnassign.php | 5 +++-- .../Plugin/SourceItem/SourceItemUpdate.php | 3 +-- .../Test/Integration/UnassignProductFromStockTest.php | 6 +++--- InventoryDataExporter/etc/di.xml | 10 +++++----- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/InventoryDataExporter/Model/Query/InventoryStockQuery.php b/InventoryDataExporter/Model/Query/InventoryStockQuery.php index 3dea4ad0..9fac27ee 100644 --- a/InventoryDataExporter/Model/Query/InventoryStockQuery.php +++ b/InventoryDataExporter/Model/Query/InventoryStockQuery.php @@ -131,12 +131,12 @@ public function getQueryForDefaultStock(array $skus): Select ], 'stock_item.product_id = isi.product_id', [] - )->joinInner( + )->joinLeft( [ 'stock_link' => $this->resourceConnection->getTableName('inventory_source_stock_link') ], 'stock_link.stock_id = 1' - )->joinInner( + )->joinLeft( [ 'source_item' => 'inventory_source_item' ], diff --git a/InventoryDataExporter/Plugin/BulkSourceUnassign.php b/InventoryDataExporter/Plugin/BulkSourceUnassign.php index a1fd053c..0685921f 100644 --- a/InventoryDataExporter/Plugin/BulkSourceUnassign.php +++ b/InventoryDataExporter/Plugin/BulkSourceUnassign.php @@ -5,6 +5,7 @@ */ namespace Magento\InventoryDataExporter\Plugin; +use Magento\InventoryCatalogApi\Api\BulkSourceUnassignInterface; use Magento\InventoryDataExporter\Model\Provider\StockStatusIdBuilder; use Magento\InventoryDataExporter\Model\Query\StockStatusDeleteQuery; @@ -30,7 +31,7 @@ public function __construct( /** * Check which stocks will be unassigned from products and mark them as deleted in feed table * - * @param \Magento\InventoryCatalog\Model\ResourceModel\BulkSourceUnassign $subject + * * @param BulkSourceUnassignInterface $subject * @param int $result * @param array $skus * @param array $sourceCodes @@ -39,7 +40,7 @@ public function __construct( * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterExecute( - \Magento\InventoryCatalog\Model\ResourceModel\BulkSourceUnassign $subject, + BulkSourceUnassignInterface $subject, int $result, array $skus, array $sourceCodes diff --git a/InventoryDataExporter/Plugin/SourceItem/SourceItemUpdate.php b/InventoryDataExporter/Plugin/SourceItem/SourceItemUpdate.php index 2dd1247a..c48b2d65 100644 --- a/InventoryDataExporter/Plugin/SourceItem/SourceItemUpdate.php +++ b/InventoryDataExporter/Plugin/SourceItem/SourceItemUpdate.php @@ -43,8 +43,7 @@ public function afterExecute( SourceItemsSaveInterface $subject, $result, array $sourceItems - ): void - { + ): void { $stockStatusIndexer = $this->indexer->load(self::STOCK_STATUS_FEED_INDEXER); if (!$stockStatusIndexer->isScheduled()) { $skus = \array_map( diff --git a/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php b/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php index 378355cf..e7db2d9a 100644 --- a/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php +++ b/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php @@ -6,7 +6,7 @@ declare(strict_types=1); -namespace Magento\ProductVariantDataExporter\Test\Integration; +namespace Magento\InventoryDataExporter\Test\Integration; use Magento\DataExporter\Model\FeedInterface; use Magento\DataExporter\Model\FeedPool; @@ -323,7 +323,7 @@ public function stocksBulkUnassignDataProvider(): array ], '10' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 1, + 'stock_id' => 10, 'deleted' => true ], ], @@ -372,7 +372,7 @@ public function stocksBulkUnassignDataProvider(): array ], '10' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 1, + 'stock_id' => 10, 'deleted' => false ], ], diff --git a/InventoryDataExporter/etc/di.xml b/InventoryDataExporter/etc/di.xml index 3e7abd51..b9d434e5 100644 --- a/InventoryDataExporter/etc/di.xml +++ b/InventoryDataExporter/etc/di.xml @@ -63,10 +63,10 @@ - + + sortOrder="200"/> @@ -77,10 +77,10 @@ - + - - + + From 6a46eb609868dd03b9faabca6d67941b183c74d1 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Mon, 1 Nov 2021 11:20:53 -0500 Subject: [PATCH 15/18] MDEE-57: Handle is_deleted updates --- .../Model/Query/StockStatusDeleteQuery.php | 19 ++++++++++++++----- .../Plugin/BulkSourceUnassign.php | 3 ++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php index e68899bf..30ddd8d8 100644 --- a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php +++ b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php @@ -49,10 +49,11 @@ public function getStocksAssignedToSkus(array $skus): array $select = $connection->select() ->from( ['source_item' => $this->resourceConnection->getTableName('inventory_source_item')], - ['source_item.sku', 'source_stock_link.stock_id'] + ['source_item.sku', 'source_stock_link.stock_id', 'source_stock_link.source_code'] )->joinLeft( ['source_stock_link' => $this->resourceConnection->getTableName('inventory_source_stock_link')], - 'source_item.source_code = source_stock_link.source_code' + 'source_item.source_code = source_stock_link.source_code', + [] )->where('source_item.sku IN (?)', $skus); $fetchedSourceItems = []; @@ -77,10 +78,18 @@ public function getStocksWithSources(array $sourceCodes): array ->from( ['source_stock_link' => $sourceLinkTableName], ['source_stock_link.stock_id', 'source_stock_link_all_sources.source_code'] - )->joinLeft( + )->joinInner( ['source_stock_link_all_sources' => $sourceLinkTableName], - 'source_stock_link_all_sources.source_code = source_stock_link.source_code' - )->group('source_stock_link_all_sources.link_id'); + 'source_stock_link_all_sources.stock_id = source_stock_link.stock_id', + [] + )->where( + 'source_stock_link.source_code IN (?)', + $sourceCodes + )->group( + ['source_stock_link.stock_id', + 'source_stock_link_all_sources.source_code' + ] + ); $stocks = []; foreach ($connection->fetchAll($select) as $stockData) { $stocks[$stockData['stock_id']][] = $stockData['source_code']; diff --git a/InventoryDataExporter/Plugin/BulkSourceUnassign.php b/InventoryDataExporter/Plugin/BulkSourceUnassign.php index 0685921f..d9e6575e 100644 --- a/InventoryDataExporter/Plugin/BulkSourceUnassign.php +++ b/InventoryDataExporter/Plugin/BulkSourceUnassign.php @@ -79,7 +79,8 @@ private function getStocksToDelete( } foreach ($sourcesAssignedToProducts[$deletedItemSku] as $fetchedItemStockId => $fetchedItemSources) { - if ($this->getContainsAllKeys($fetchedItemSources, $sourcesByStocks[$fetchedItemStockId])) { + if (isset($sourcesByStocks[$fetchedItemStockId]) + && $this->getContainsAllKeys($fetchedItemSources, $sourcesByStocks[$fetchedItemStockId])) { $stockStatusId = StockStatusIdBuilder::build( ['stockId' => (string)$fetchedItemStockId, 'sku' => $deletedItemSku] ); From 02ca99b03bb4ae7fbad5d60822426e6ee3fd4b1a Mon Sep 17 00:00:00 2001 From: Misha Slabko Date: Mon, 1 Nov 2021 11:26:16 -0500 Subject: [PATCH 16/18] MDEE-40:[Commerce Export] Implement stock_item_status feed --- .../Model/Provider/StockStatus.php | 1 - .../Model/Query/InventoryStockQuery.php | 19 +++++++++++-------- ...products_with_sources_and_reservations.php | 2 +- ...with_sources_and_reservations_rollback.php | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/InventoryDataExporter/Model/Provider/StockStatus.php b/InventoryDataExporter/Model/Provider/StockStatus.php index 34a6e36e..ad406776 100644 --- a/InventoryDataExporter/Model/Provider/StockStatus.php +++ b/InventoryDataExporter/Model/Provider/StockStatus.php @@ -80,7 +80,6 @@ public function get(array $values): array $output = []; try { - $processedSkus = []; $select = $this->query->getQuery($skus); // $select can be null if no stocks exists except default if ($select) { diff --git a/InventoryDataExporter/Model/Query/InventoryStockQuery.php b/InventoryDataExporter/Model/Query/InventoryStockQuery.php index 9fac27ee..9e808b8f 100644 --- a/InventoryDataExporter/Model/Query/InventoryStockQuery.php +++ b/InventoryDataExporter/Model/Query/InventoryStockQuery.php @@ -21,13 +21,17 @@ class InventoryStockQuery * @var ResourceConnection */ private $resourceConnection; + /** * @var DefaultStockProviderInterface */ private $defaultStockProvider; + private const DEFAULT_STOCK_SOURCE = 'default'; + /** * @param ResourceConnection $resourceConnection + * @param DefaultStockProviderInterface $defaultStockProvider */ public function __construct( ResourceConnection $resourceConnection, @@ -131,17 +135,16 @@ public function getQueryForDefaultStock(array $skus): Select ], 'stock_item.product_id = isi.product_id', [] - )->joinLeft( - [ - 'stock_link' => $this->resourceConnection->getTableName('inventory_source_stock_link') - ], - 'stock_link.stock_id = 1' - )->joinLeft( + )->joinInner( [ 'source_item' => 'inventory_source_item' ], - 'source_item.source_code = stock_link.stock_id and source_item.sku = isi.sku') - ->columns( + $connection->quoteInto( + 'source_item.source_code = ? and source_item.sku = isi.sku', + self::DEFAULT_STOCK_SOURCE + ), + [] + )->columns( [ 'qty' => "isi.quantity", 'isSalable' => "isi.is_salable", diff --git a/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php index 1c11ab2b..0b30d917 100644 --- a/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php +++ b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations.php @@ -20,7 +20,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; Resolver::getInstance()->requireDataFixture( - 'Magento/InventoryDataExporter/Test/_files/products_with_sources.php' + 'Magento_InventoryDataExporter::Test/_files/products_with_sources.php' ); /** diff --git a/InventoryDataExporter/Test/_files/products_with_sources_and_reservations_rollback.php b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations_rollback.php index d3d42380..b1fe3f1f 100644 --- a/InventoryDataExporter/Test/_files/products_with_sources_and_reservations_rollback.php +++ b/InventoryDataExporter/Test/_files/products_with_sources_and_reservations_rollback.php @@ -32,5 +32,5 @@ $connection->delete($reservationTable, $condition); Resolver::getInstance()->requireDataFixture( - 'Magento/InventoryDataExporter/Test/_files/products_with_sources_rollback.php' + 'Magento_InventoryDataExporter::Test/_files/products_with_sources_rollback.php' ); From 8040c7b076fc45301e8bd86aaff25bfdded4d158 Mon Sep 17 00:00:00 2001 From: Misha Slabko Date: Mon, 1 Nov 2021 12:08:05 -0500 Subject: [PATCH 17/18] MDEE-40:[Commerce Export] Implement stock_item_status feed --- InventoryDataExporter/README.md | 15 ++++++++------- InventoryDataExporter/etc/di.xml | 5 +++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/InventoryDataExporter/README.md b/InventoryDataExporter/README.md index a4a8fb66..d5391d02 100644 --- a/InventoryDataExporter/README.md +++ b/InventoryDataExporter/README.md @@ -1,9 +1,10 @@ -## Release notes +*Magento_InventoryDataExporter* module is responsible for collecting inventory data -*Magento_InventoryDataExporter* module +## Stock Status -https://docs.magento.com/user-guide/catalog/inventory-backorders.html?itm_source=devdocs&itm_medium=quick_search&itm_campaign=federated_search&itm_term=backorer - - -Zero -With Backorders enabled, entering 0 allows for infinite backorders. \ No newline at end of file +- Collects aggregated value of Stock Status described in [et_schema.xml](etc/et_schema.xml) +- Depends on Inventory indexer which is used to get `isSalable` status and `qty` in stock. +- `qtyForSale` calculated based on Reservations API +- Stock is considered as infinite in the following cases: + - Manage Stock disabled + - [Backorders](https://docs.magento.com/user-guide/catalog/inventory-backorders.html?itm_source=devdocs&itm_medium=quick_search&itm_campaign=federated_search&itm_term=backorer) enabled and Out-of-Stock threshold is set to 0. diff --git a/InventoryDataExporter/etc/di.xml b/InventoryDataExporter/etc/di.xml index b9d434e5..876084c1 100644 --- a/InventoryDataExporter/etc/di.xml +++ b/InventoryDataExporter/etc/di.xml @@ -76,9 +76,10 @@ - + - + + From 1cbfd19075dd450377704181ecea3d56d79399e5 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov Date: Mon, 1 Nov 2021 17:39:34 -0500 Subject: [PATCH 18/18] Cover changes by tests --- .../Model/Query/StockStatusDeleteQuery.php | 68 +++++- .../Plugin/BulkSourceUnassign.php | 10 +- .../Plugin/MarkItemsAsDeleted.php | 6 +- .../Integration/PartialReindexCheckTest.php | 6 +- .../UnassignProductFromStockTest.php | 213 +++++++++++++++--- 5 files changed, 255 insertions(+), 48 deletions(-) diff --git a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php index 30ddd8d8..1b7f1734 100644 --- a/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php +++ b/InventoryDataExporter/Model/Query/StockStatusDeleteQuery.php @@ -9,6 +9,8 @@ use Magento\DataExporter\Model\Indexer\FeedIndexMetadata; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\Stdlib\DateTime; /** * Stock Status mark as deleted query builder @@ -25,16 +27,32 @@ class StockStatusDeleteQuery */ private $metadata; + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var DateTime + */ + private $dateTime; + /** * @param ResourceConnection $resourceConnection * @param FeedIndexMetadata $metadata + * @param SerializerInterface $serializer + * @param DateTime $dateTime */ public function __construct( ResourceConnection $resourceConnection, - FeedIndexMetadata $metadata + FeedIndexMetadata $metadata, + SerializerInterface $serializer, + DateTime $dateTime ) { $this->resourceConnection = $resourceConnection; $this->metadata = $metadata; + $this->serializer = $serializer; + $this->dateTime = $dateTime; } /** @@ -104,14 +122,52 @@ public function getStocksWithSources(array $sourceCodes): array */ public function markStockStatusesAsDeleted(array $idsToDelete): void { + $records = []; + foreach ($idsToDelete as $deletedItemId => $stockStatusData) { + $records[] = $this->buildFeedData($deletedItemId, $stockStatusData); + } $connection = $this->resourceConnection->getConnection(); $feedTableName = $this->resourceConnection->getTableName($this->metadata->getFeedTableName()); - $connection->update( + $connection->insertOnDuplicate( $feedTableName, - ['is_deleted' => new \Zend_Db_Expr('1')], - [ - 'id IN (?)' => $idsToDelete - ] + $records ); } + + /** + * @param string $stockStatusId + * @param array $stockIdAndSku + * @return array + */ + private function buildFeedData(string $stockStatusId, array $stockIdAndSku): array + { + if (!isset($stockIdAndSku['stock_id'], $stockIdAndSku['sku'])) { + throw new \RuntimeException( + sprintf( + "inventory_data_exporter_stock_status indexer error: cannot build unique id from %s", + \var_export($stockIdAndSku, true) + ) + ); + } + $feedData = [ + 'id' => $stockStatusId, + 'stockId' => $stockIdAndSku['stock_id'], + 'sku' => $stockIdAndSku['sku'], + 'qty' => 0, + 'qtyForSale' => 0, + 'infiniteStock' => false, + 'isSalable' => false, + 'updatedAt' => $this->dateTime->formatDate(time()) + + ]; + + return [ + 'id' => $stockStatusId, + 'stock_id' => $stockIdAndSku['stock_id'], + 'sku' => $stockIdAndSku['sku'], + 'feed_data' => $this->serializer->serialize($feedData), + 'is_deleted' => 1, + 'modified_at' => $this->dateTime->formatDate(time()) + ]; + } } diff --git a/InventoryDataExporter/Plugin/BulkSourceUnassign.php b/InventoryDataExporter/Plugin/BulkSourceUnassign.php index d9e6575e..30870041 100644 --- a/InventoryDataExporter/Plugin/BulkSourceUnassign.php +++ b/InventoryDataExporter/Plugin/BulkSourceUnassign.php @@ -70,9 +70,13 @@ private function getStocksToDelete( $stocksToDelete = []; foreach ($affectedSkus as $deletedItemSku) { foreach (array_keys($sourcesByStocks) as $stockId) { - $stocksToDelete[] = StockStatusIdBuilder::build( + $stockStatusId = StockStatusIdBuilder::build( ['stockId' => (string)$stockId, 'sku' => $deletedItemSku] ); + $stocksToDelete[$stockStatusId] = [ + 'stock_id' => (string)$stockId, + 'sku' => $deletedItemSku + ]; } if (!isset($sourcesAssignedToProducts[$deletedItemSku])) { continue ; @@ -84,9 +88,7 @@ private function getStocksToDelete( $stockStatusId = StockStatusIdBuilder::build( ['stockId' => (string)$fetchedItemStockId, 'sku' => $deletedItemSku] ); - if ($key = \array_search($stockStatusId, $stocksToDelete, false)) { - unset($stocksToDelete[(int)$key]); - } + unset($stocksToDelete[$stockStatusId]); } } } diff --git a/InventoryDataExporter/Plugin/MarkItemsAsDeleted.php b/InventoryDataExporter/Plugin/MarkItemsAsDeleted.php index 0e883038..e1f4b068 100644 --- a/InventoryDataExporter/Plugin/MarkItemsAsDeleted.php +++ b/InventoryDataExporter/Plugin/MarkItemsAsDeleted.php @@ -66,9 +66,13 @@ private function getStocksToDelete(array $deletedSourceItems, $fetchedSourceItem foreach ($deletedSourceItems as $deletedItemSku => $deletedItemSources) { foreach ($fetchedSourceItems[$deletedItemSku] as $fetchedItemStockId => $fetchedItemSources) { if ($this->getContainsAllKeys($fetchedItemSources, $deletedItemSources)) { - $stocksToDelete[] = StockStatusIdBuilder::build( + $stockStatusId = StockStatusIdBuilder::build( ['stockId' => (string)$fetchedItemStockId, 'sku' => $deletedItemSku] ); + $stocksToDelete[$stockStatusId] = [ + 'stock_id' => (string)$fetchedItemStockId, + 'sku' => $deletedItemSku + ]; } } } diff --git a/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php b/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php index 7502e7b4..5ad83e54 100644 --- a/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php +++ b/InventoryDataExporter/Test/Integration/PartialReindexCheckTest.php @@ -102,7 +102,11 @@ public function testSourceItemQtyUpdated() */ public function testSourceBulkUnassign() { - $skus = ['product_in_EU_stock_with_2_sources', 'product_in_Global_stock_with_3_sources', 'product_with_default_stock_only']; + $skus = [ + 'product_in_EU_stock_with_2_sources', + 'product_in_Global_stock_with_3_sources', + 'product_with_default_stock_only' + ]; $this->bulkSourceUnassign->execute( $skus, diff --git a/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php b/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php index e7db2d9a..56a1efbc 100644 --- a/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php +++ b/InventoryDataExporter/Test/Integration/UnassignProductFromStockTest.php @@ -54,9 +54,10 @@ protected function setUp(): void * @param array $sourcesToLeave * @param array $expectedData * @throws InputException + * @throws \Zend_Db_Statement_Exception * @magentoDataFixture Magento_InventoryDataExporter::Test/_files/products_with_sources.php */ - public function te1stSourceItemStockUnassigned(string $sku, array $sourcesToLeave, array $expectedData) + public function testSourceItemStockUnassigned(string $sku, array $sourcesToLeave, array $expectedData) { $sourceItems = $this->getSourcesData($sku, $sourcesToLeave); $this->sourceItemProcessor->execute($sku, $sourceItems); @@ -118,12 +119,20 @@ public function stocksUnassignDataProvider(): array 'expected_data' => [ '10' => [ 'sku' => 'product_in_EU_stock_with_2_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => false ], '30' => [ 'sku' => 'product_in_EU_stock_with_2_sources', - 'stock_id' => 30, + 'stock_id' => '30', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ] ] @@ -134,17 +143,29 @@ public function stocksUnassignDataProvider(): array 'expected_data' => [ '10' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => false ], '20' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 20, + 'stock_id' => '20', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], '30' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 30, + 'stock_id' => '30', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ] ] @@ -155,23 +176,35 @@ public function stocksUnassignDataProvider(): array 'expected_data' => [ '1' => [ 'sku' => 'product_with_default_stock_only', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ] ] ], 'default_stock_unassign_from_default_and_custom_stocks_product' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'sources_to_leave' => ['eu1', 'eu2'], + 'sources_to_leave' => ['eu-1', 'eu-2'], 'expected_data' => [ '1' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], '10' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => false ] ] @@ -182,12 +215,20 @@ public function stocksUnassignDataProvider(): array 'expected_data' => [ '1' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => false ], '10' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ] ] @@ -213,38 +254,62 @@ public function stocksBulkUnassignDataProvider(): array 'product_with_default_stock_only' => [ '1' => [ 'sku' => 'product_with_default_stock_only', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 8.5, + 'qty_for_sale' => 8.5, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], ], 'product_in_default_and_2_EU_sources' => [ '1' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 2, + 'qty_for_sale' => 2, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], '10' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ], 'product_in_EU_stock_with_2_sources' => [ '10' => [ 'sku' => 'product_in_EU_stock_with_2_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ], 'product_in_Global_stock_with_3_sources' => [ '10' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], '30' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 30, + 'stock_id' => '30', + 'qty' => 4, + 'qty_for_sale' => 4, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], ] @@ -262,38 +327,62 @@ public function stocksBulkUnassignDataProvider(): array 'product_with_default_stock_only' => [ '1' => [ 'sku' => 'product_with_default_stock_only', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 8.5, + 'qty_for_sale' => 8.5, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], ], 'product_in_default_and_2_EU_sources' => [ '1' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 2, + 'qty_for_sale' => 2, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], '10' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ], 'product_in_EU_stock_with_2_sources' => [ '10' => [ 'sku' => 'product_in_EU_stock_with_2_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ], 'product_in_Global_stock_with_3_sources' => [ '10' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], '30' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 30, + 'stock_id' => '30', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ] @@ -311,38 +400,62 @@ public function stocksBulkUnassignDataProvider(): array 'product_with_default_stock_only' => [ '1' => [ 'sku' => 'product_with_default_stock_only', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ], 'product_in_default_and_2_EU_sources' => [ '1' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], '10' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ], 'product_in_EU_stock_with_2_sources' => [ '10' => [ 'sku' => 'product_in_EU_stock_with_2_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ], 'product_in_Global_stock_with_3_sources' => [ '10' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], '30' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 30, + 'stock_id' => '30', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ] @@ -360,38 +473,62 @@ public function stocksBulkUnassignDataProvider(): array 'product_with_default_stock_only' => [ '1' => [ 'sku' => 'product_with_default_stock_only', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], ], 'product_in_default_and_2_EU_sources' => [ '1' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 1, + 'stock_id' => '1', + 'qty' => 0, + 'qty_for_sale' => 0, + 'infinite_stock' => false, + 'is_salable' => false, 'deleted' => true ], '10' => [ 'sku' => 'product_in_default_and_2_EU_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 9.5, + 'qty_for_sale' => 9.5, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], ], 'product_in_EU_stock_with_2_sources' => [ '10' => [ 'sku' => 'product_in_EU_stock_with_2_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 9.5, + 'qty_for_sale' => 9.5, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], ], 'product_in_Global_stock_with_3_sources' => [ '10' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 10, + 'stock_id' => '10', + 'qty' => 3, + 'qty_for_sale' => 3, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], '30' => [ 'sku' => 'product_in_Global_stock_with_3_sources', - 'stock_id' => 30, + 'stock_id' => '30', + 'qty' => 5, + 'qty_for_sale' => 5, + 'infinite_stock' => false, + 'is_salable' => true, 'deleted' => false ], ] @@ -431,6 +568,10 @@ private function verifyResults(array $feedData, string $sku, array $expectedData [ 'sku' => $feedData[$expectedStockId][$sku]['sku'], 'stock_id' => $feedData[$expectedStockId][$sku]['stockId'], + 'qty' => $feedData[$expectedStockId][$sku]['qty'], + 'qty_for_sale' => $feedData[$expectedStockId][$sku]['qtyForSale'], + 'infinite_stock' => $feedData[$expectedStockId][$sku]['infiniteStock'], + 'is_salable' => $feedData[$expectedStockId][$sku]['isSalable'], 'deleted' => $feedData[$expectedStockId][$sku]['deleted'], ] );