From 8e4b69bc00606bee10ee5ac46b354b04fed8b894 Mon Sep 17 00:00:00 2001 From: Aleksandrs Mokans Date: Mon, 11 Jul 2022 07:50:22 +0300 Subject: [PATCH 1/2] Reworking stock post processor and stock_item resolver as batch contracts --- .../AreProductsAssignedToStockInterface.php | 20 ++ .../Inventory/AreProductsSalableInterface.php | 27 ++ .../GetReservationsQuantitiesInterface.php | 27 ++ .../GetStockItemsConfigurationsInterface.php | 32 ++ .../Inventory/GetStockItemsDataInterface.php | 36 +++ .../Inventory/AreProductsAssignedToStock.php | 113 +++++++ .../AreSalableWithReservationsCondition.php | 116 +++++++ .../BackOrderCondition.php | 60 ++++ .../IsAnySourceItemInStockCondition.php | 160 ++++++++++ ...ockStatusForCompositeProductsCondition.php | 67 ++++ .../ManageStockCondition.php | 67 ++++ .../AreProductsSalableConditionChain.php | 145 +++++++++ src/Model/Inventory/GetLegacyStockItems.php | 140 +++++++++ src/Model/Inventory/GetProductIdsBySkus.php | 47 +++ .../Inventory/GetProductIdsBySkusCache.php | 71 +++++ .../Inventory/GetReservationsQuantities.php | 106 +++++++ .../Inventory/GetStockItemsConfigurations.php | 115 +++++++ src/Model/Inventory/GetStockItemsData.php | 278 +++++++++++++++++ .../Products/DataPostProcessor/Stocks.php | 292 ------------------ src/Model/Resolver/Products/StockItem.php | 70 +++++ .../Products/StockItem/GetStockItem.php | 120 +++++++ .../Products/StockItem/ProductCriteria.php | 34 ++ src/Model/Resolver/Products/StockStatus.php | 63 ++++ .../StockStatus/AreProductsSalable.php | 134 ++++++++ .../Products/StockStatus/ProductCriteria.php | 34 ++ src/etc/di.xml | 47 ++- src/etc/schema.graphqls | 9 +- 27 files changed, 2136 insertions(+), 294 deletions(-) create mode 100644 src/Api/Inventory/AreProductsAssignedToStockInterface.php create mode 100644 src/Api/Inventory/AreProductsSalableInterface.php create mode 100644 src/Api/Inventory/GetReservationsQuantitiesInterface.php create mode 100644 src/Api/Inventory/GetStockItemsConfigurationsInterface.php create mode 100644 src/Api/Inventory/GetStockItemsDataInterface.php create mode 100644 src/Model/Inventory/AreProductsAssignedToStock.php create mode 100644 src/Model/Inventory/AreProductsSalableCondition/AreSalableWithReservationsCondition.php create mode 100644 src/Model/Inventory/AreProductsSalableCondition/BackOrderCondition.php create mode 100644 src/Model/Inventory/AreProductsSalableCondition/IsAnySourceItemInStockCondition.php create mode 100644 src/Model/Inventory/AreProductsSalableCondition/IsSetInStockStatusForCompositeProductsCondition.php create mode 100644 src/Model/Inventory/AreProductsSalableCondition/ManageStockCondition.php create mode 100644 src/Model/Inventory/AreProductsSalableConditionChain.php create mode 100644 src/Model/Inventory/GetLegacyStockItems.php create mode 100644 src/Model/Inventory/GetProductIdsBySkus.php create mode 100644 src/Model/Inventory/GetProductIdsBySkusCache.php create mode 100644 src/Model/Inventory/GetReservationsQuantities.php create mode 100644 src/Model/Inventory/GetStockItemsConfigurations.php create mode 100644 src/Model/Inventory/GetStockItemsData.php delete mode 100644 src/Model/Resolver/Products/DataPostProcessor/Stocks.php create mode 100644 src/Model/Resolver/Products/StockItem.php create mode 100644 src/Model/Resolver/Products/StockItem/GetStockItem.php create mode 100644 src/Model/Resolver/Products/StockItem/ProductCriteria.php create mode 100644 src/Model/Resolver/Products/StockStatus.php create mode 100644 src/Model/Resolver/Products/StockStatus/AreProductsSalable.php create mode 100644 src/Model/Resolver/Products/StockStatus/ProductCriteria.php diff --git a/src/Api/Inventory/AreProductsAssignedToStockInterface.php b/src/Api/Inventory/AreProductsAssignedToStockInterface.php new file mode 100644 index 0000000..6cc1a3e --- /dev/null +++ b/src/Api/Inventory/AreProductsAssignedToStockInterface.php @@ -0,0 +1,20 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Api\Inventory; + +interface AreProductsAssignedToStockInterface +{ + /** + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array; +} diff --git a/src/Api/Inventory/AreProductsSalableInterface.php b/src/Api/Inventory/AreProductsSalableInterface.php new file mode 100644 index 0000000..6e4d9a0 --- /dev/null +++ b/src/Api/Inventory/AreProductsSalableInterface.php @@ -0,0 +1,27 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Api\Inventory; + +/** + * Service which detects whether products are salable for given stock (stock data + reservations). + * + * @api + */ +interface AreProductsSalableInterface +{ + /** + * Get products salable status for given SKUs and given Stock. + * + * @param string[] $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array; +} diff --git a/src/Api/Inventory/GetReservationsQuantitiesInterface.php b/src/Api/Inventory/GetReservationsQuantitiesInterface.php new file mode 100644 index 0000000..b3ba4ae --- /dev/null +++ b/src/Api/Inventory/GetReservationsQuantitiesInterface.php @@ -0,0 +1,27 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Api\Inventory; + +/** + * Responsible for retrieving Reservation Quantity (without stock data) for SKU array + * + * @api + */ +interface GetReservationsQuantitiesInterface +{ + /** + * Given a product sku array and a stock id, return reservation quantity for each sku + * + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array; +} diff --git a/src/Api/Inventory/GetStockItemsConfigurationsInterface.php b/src/Api/Inventory/GetStockItemsConfigurationsInterface.php new file mode 100644 index 0000000..1fc1851 --- /dev/null +++ b/src/Api/Inventory/GetStockItemsConfigurationsInterface.php @@ -0,0 +1,32 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Api\Inventory; + +use Magento\Framework\Exception\LocalizedException; +use Magento\InventoryConfigurationApi\Api\Data\StockItemConfigurationInterface; +use Magento\InventoryConfigurationApi\Exception\SkuIsNotAssignedToStockException; + +/** + * Returns stock item configuration data by sku and stock id. + * + * @api + */ +interface GetStockItemsConfigurationsInterface +{ + /** + * @param array $skuArray + * @param int $stockId + * @return StockItemConfigurationInterface[] + */ + public function execute( + array $skuArray, + int $stockId + ): array; +} diff --git a/src/Api/Inventory/GetStockItemsDataInterface.php b/src/Api/Inventory/GetStockItemsDataInterface.php new file mode 100644 index 0000000..4dff0b2 --- /dev/null +++ b/src/Api/Inventory/GetStockItemsDataInterface.php @@ -0,0 +1,36 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Api\Inventory; + +/** + * Responsible for retrieving StockItem Data for SKU array + * + * @api + */ +interface GetStockItemsDataInterface +{ + /** + * Constants for represent fields in result array + */ + const SKU = 'sku'; + const QUANTITY = 'quantity'; + const IS_SALABLE = 'is_salable'; + + /**#@-*/ + + /** + * Given a product sku array and a stock id, return stock item data for each sku + * + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array; +} diff --git a/src/Model/Inventory/AreProductsAssignedToStock.php b/src/Model/Inventory/AreProductsAssignedToStock.php new file mode 100644 index 0000000..bafdf46 --- /dev/null +++ b/src/Model/Inventory/AreProductsAssignedToStock.php @@ -0,0 +1,113 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Magento\Framework\App\ResourceConnection; +use Magento\Inventory\Model\ResourceModel\SourceItem; +use Magento\Inventory\Model\ResourceModel\StockSourceLink; +use Magento\InventoryApi\Api\Data\SourceItemInterface; +use Magento\InventoryApi\Api\Data\StockSourceLinkInterface; +use ScandiPWA\Performance\Api\Inventory\AreProductsAssignedToStockInterface; + +class AreProductsAssignedToStock implements AreProductsAssignedToStockInterface +{ + /** + * @var ResourceConnection + */ + protected ResourceConnection $resource; + + /** + * @var array + */ + protected $resultsByStockAndSku = []; + + /** + * @param ResourceConnection $resource + */ + public function __construct( + ResourceConnection $resource + ) { + $this->resource = $resource; + } + + /** + * Cache wrapper for actual loading + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array + { + $resultsBySku = []; + $loadSkus = []; + + foreach ($skuArray as $sku) { + if (isset($this->resultsByStockAndSku[$stockId][$sku])) { + $resultsBySku[$sku] = $this->resultsByStockAndSku[$stockId][$sku]; + } else { + $loadSkus[] = $sku; + $resultsBySku[$sku] = null; + } + } + + if (count($loadSkus)) { + $results = $this->getAreProductsAssignedToStock($loadSkus, $stockId); + + foreach ($results as $sku => $result) { + $this->resultsByStockAndSku[$stockId][$sku] = $result; + $resultsBySku[$sku] = $result; + } + } + + return $resultsBySku; + } + + /** + * Checks if products are assigned to stock, by sku list and stock id + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function getAreProductsAssignedToStock(array $skuArray, int $stockId): array + { + $finalResults = []; + + foreach ($skuArray as $sku) { + $finalResults[$sku] = false; + } + + $connection = $this->resource->getConnection(); + $select = $connection->select() + ->from( + ['stock_source_link' => $this->resource->getTableName(StockSourceLink::TABLE_NAME_STOCK_SOURCE_LINK)] + )->join( + ['inventory_source_item' => $this->resource->getTableName(SourceItem::TABLE_NAME_SOURCE_ITEM)], + 'inventory_source_item.' . SourceItemInterface::SOURCE_CODE . ' + = stock_source_link.' . SourceItemInterface::SOURCE_CODE, + ['inventory_source_item.sku'] + )->where( + 'stock_source_link.' . StockSourceLinkInterface::STOCK_ID . ' = ?', + $stockId + )->where( + 'inventory_source_item.' . SourceItemInterface::SKU . ' IN (?)', + $skuArray + )->group('inventory_source_item.' . SourceItemInterface::SKU); + + $results = $connection->fetchAll($select); + + foreach ($results as $result) { + if (isset($result['sku'])) { + $finalResults[$result['sku']] = true; + } + } + + return $finalResults; + } +} diff --git a/src/Model/Inventory/AreProductsSalableCondition/AreSalableWithReservationsCondition.php b/src/Model/Inventory/AreProductsSalableCondition/AreSalableWithReservationsCondition.php new file mode 100644 index 0000000..0d523bc --- /dev/null +++ b/src/Model/Inventory/AreProductsSalableCondition/AreSalableWithReservationsCondition.php @@ -0,0 +1,116 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition; + +use Magento\InventoryCatalogApi\Model\GetProductTypesBySkusInterface; +use Magento\InventoryConfigurationApi\Api\GetStockItemConfigurationInterface; +use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForProductTypeInterface; +use ScandiPWA\Performance\Api\Inventory\GetReservationsQuantitiesInterface; +use ScandiPWA\Performance\Api\Inventory\GetStockItemsDataInterface; +use ScandiPWA\Performance\Api\Inventory\AreProductsSalableInterface; +use ScandiPWA\Performance\Api\Inventory\GetStockItemsConfigurationsInterface; + +class AreSalableWithReservationsCondition implements AreProductsSalableInterface +{ + /** + * @var GetStockItemsConfigurationsInterface + */ + protected GetStockItemsConfigurationsInterface $getStockItemsConfigurations; + + /** + * @var GetProductTypesBySkusInterface + */ + protected GetProductTypesBySkusInterface $getProductTypesBySkus; + + /** + * @var IsSourceItemManagementAllowedForProductTypeInterface + */ + protected IsSourceItemManagementAllowedForProductTypeInterface $isSourceItemManagementAllowedForProductType; + + /** + * @var GetStockItemsDataInterface + */ + protected GetStockItemsDataInterface $getStockItemsData; + + /** + * @var GetReservationsQuantitiesInterface + */ + protected GetReservationsQuantitiesInterface $getReservationsQuantities; + + /** + * @var GetStockItemConfigurationInterface + */ + protected GetStockItemConfigurationInterface $getStockItemConfiguration; + + /** + * @param GetStockItemsConfigurationsInterface $getStockItemsConfigurations + * @param GetStockItemsDataInterface $getStockItemsData + * @param GetReservationsQuantitiesInterface $getReservationsQuantities + * @param IsSourceItemManagementAllowedForProductTypeInterface $isSourceItemManagementAllowedForProductType + * @param GetProductTypesBySkusInterface $getProductTypesBySkus + */ + public function __construct( + GetStockItemsConfigurationsInterface $getStockItemsConfigurations, + GetStockItemsDataInterface $getStockItemsData, + GetReservationsQuantitiesInterface $getReservationsQuantities, + IsSourceItemManagementAllowedForProductTypeInterface $isSourceItemManagementAllowedForProductType, + GetProductTypesBySkusInterface $getProductTypesBySkus + ) { + $this->getStockItemsConfigurations = $getStockItemsConfigurations; + $this->getStockItemsData = $getStockItemsData; + $this->getReservationsQuantities = $getReservationsQuantities; + $this->isSourceItemManagementAllowedForProductType = $isSourceItemManagementAllowedForProductType; + $this->getProductTypesBySkus = $getProductTypesBySkus; + } + + /** + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array + { + $result = []; + $skusToCheck = array_flip($skuArray); + + $stockItemDataArray = $this->getStockItemsData->execute($skuArray, $stockId); + + foreach ($stockItemDataArray as $sku => $stockItemData) { + if (null === $stockItemData) { + // Sku is not assigned to Stock + $result[$sku] = false; + unset($skusToCheck[$sku]); + continue; + } + + // these values will be taken from cache, so can be executed individually + $productType = $this->getProductTypesBySkus->execute([$sku])[$sku]; + + // source item management not active for product type, do not check reservations + if (false === $this->isSourceItemManagementAllowedForProductType->execute($productType)) { + $result[$sku] = (bool)$stockItemData[GetStockItemsDataInterface::IS_SALABLE]; + unset($skusToCheck[$sku]); + } + } + + if (count($skusToCheck)) { + // need to check reservations for the remaining skus + $stockItemConfigurations = $this->getStockItemsConfigurations->execute(array_keys($skusToCheck), $stockId); + $reservationQtys = $this->getReservationsQuantities->execute(array_keys($skusToCheck), $stockId); + + foreach ($stockItemConfigurations as $sku => $stockItemConfiguration) { + $qtyWithReservation = $stockItemDataArray[$sku][GetStockItemsDataInterface::QUANTITY] + $reservationQtys[$sku]; + $result[$sku] = $qtyWithReservation > $stockItemConfiguration->getMinQty(); + } + } + + return $result; + } +} diff --git a/src/Model/Inventory/AreProductsSalableCondition/BackOrderCondition.php b/src/Model/Inventory/AreProductsSalableCondition/BackOrderCondition.php new file mode 100644 index 0000000..b06eb01 --- /dev/null +++ b/src/Model/Inventory/AreProductsSalableCondition/BackOrderCondition.php @@ -0,0 +1,60 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition; + +use Magento\InventoryConfigurationApi\Api\Data\StockItemConfigurationInterface; +use ScandiPWA\Performance\Api\Inventory\AreProductsSalableInterface; +use ScandiPWA\Performance\Api\Inventory\GetStockItemsConfigurationsInterface; + +class BackOrderCondition implements AreProductsSalableInterface +{ + /** + * @var GetStockItemsConfigurationsInterface + */ + protected GetStockItemsConfigurationsInterface $getStockItemsConfigurations; + + /** + * @param GetStockItemsConfigurationsInterface $getStockItemsConfigurations + */ + public function __construct( + GetStockItemsConfigurationsInterface $getStockItemsConfigurations + ) { + $this->getStockItemsConfigurations = $getStockItemsConfigurations; + } + + /** + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array + { + $result = []; + + $stockItemConfigurations = $this->getStockItemsConfigurations->execute($skuArray, $stockId); + + foreach ($stockItemConfigurations as $sku => $stockItemConfiguration) { + if (!$stockItemConfiguration) { + $result[$sku] = false; + + continue; + } + + if ($stockItemConfiguration->getBackorders() !== StockItemConfigurationInterface::BACKORDERS_NO + && $stockItemConfiguration->getMinQty() >= 0) { + $result[$sku] = true; + } else { + $result[$sku] = false; + } + } + + return $result; + } +} diff --git a/src/Model/Inventory/AreProductsSalableCondition/IsAnySourceItemInStockCondition.php b/src/Model/Inventory/AreProductsSalableCondition/IsAnySourceItemInStockCondition.php new file mode 100644 index 0000000..99bea5d --- /dev/null +++ b/src/Model/Inventory/AreProductsSalableCondition/IsAnySourceItemInStockCondition.php @@ -0,0 +1,160 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Inventory\Model\ResourceModel\SourceItem\Collection; +use Magento\Inventory\Model\SourceItem; +use Magento\InventoryApi\Api\Data\SourceItemInterface; +use Magento\InventoryApi\Api\GetSourcesAssignedToStockOrderedByPriorityInterface; +use Magento\InventoryApi\Api\SourceItemRepositoryInterface; +use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForSkuInterface; +use Magento\Inventory\Model\ResourceModel\SourceItem\CollectionFactory; +use ScandiPWA\Performance\Api\Inventory\AreProductsSalableInterface; + +class IsAnySourceItemInStockCondition implements AreProductsSalableInterface +{ + /** + * @var SourceItemRepositoryInterface + */ + protected SourceItemRepositoryInterface $sourceItemRepository; + + /** + * @var SearchCriteriaBuilder + */ + protected SearchCriteriaBuilder $searchCriteriaBuilder; + + /** + * @var GetSourcesAssignedToStockOrderedByPriorityInterface + */ + protected GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority; + + /** + * @var IsSourceItemManagementAllowedForSkuInterface + */ + protected IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; + + /** + * @var ManageStockCondition + */ + protected ManageStockCondition $manageStockCondition; + + /** + * @var CollectionProcessorInterface + */ + protected CollectionProcessorInterface $collectionProcessor; + + /** + * @var CollectionFactory + */ + protected CollectionFactory $sourceItemCollectionFactory; + + /** + * @param SourceItemRepositoryInterface $sourceItemRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority + * @param IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku + * @param ManageStockCondition $manageStockCondition + * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionFactory $sourceItemCollectionFactory + */ + public function __construct( + SourceItemRepositoryInterface $sourceItemRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority, + IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku, + ManageStockCondition $manageStockCondition, + CollectionProcessorInterface $collectionProcessor, + CollectionFactory $sourceItemCollectionFactory + ) { + $this->sourceItemRepository = $sourceItemRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->getSourcesAssignedToStockOrderedByPriority = $getSourcesAssignedToStockOrderedByPriority; + $this->isSourceItemManagementAllowedForSku = $isSourceItemManagementAllowedForSku; + $this->manageStockCondition = $manageStockCondition; + $this->collectionProcessor = $collectionProcessor; + $this->sourceItemCollectionFactory = $sourceItemCollectionFactory; + } + + /** + * @param array $skuArray + * @param int $stockId + * @return array + * @throws InputException + * @throws LocalizedException + */ + public function execute(array $skuArray, int $stockId): array + { + $result = $this->manageStockCondition->execute($skuArray, $stockId); + + $skusToCheck = []; + + foreach ($result as $sku => $value) { + // if value is true, that is a final result; otherwise, proceed with the next checks + if (!$value) { + $skusToCheck[$sku] = null; + } + } + + foreach (array_keys($skusToCheck) as $sku) { + if (!$this->isSourceItemManagementAllowedForSku->execute($sku)) { + // if source item management is not allowed, that is a final result for that sku + $result[$sku] = true; + + unset($skusToCheck[$sku]); + } + } + + $sourceCodes = $this->getSourceCodesAssignedToStock($stockId); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(SourceItemInterface::SKU, array_keys($skusToCheck), 'in') + ->addFilter(SourceItemInterface::SOURCE_CODE, $sourceCodes, 'in') + ->addFilter(SourceItemInterface::STATUS, SourceItemInterface::STATUS_IN_STOCK) + ->create(); + + /** @var Collection $collection */ + $collection = $this->sourceItemCollectionFactory->create(); + $this->collectionProcessor->process($searchCriteria, $collection); + + /** @var SourceItem $item */ + foreach ($collection as $item) { + $result[$item->getSku()] = true; + } + + return $result; + } + + /** + * Provides source codes for certain stock + * + * @param int $stockId + * + * @return array + * @throws InputException + * @throws LocalizedException + */ + protected function getSourceCodesAssignedToStock(int $stockId): array + { + $sources = $this->getSourcesAssignedToStockOrderedByPriority->execute($stockId); + $sourceCodes = []; + + foreach ($sources as $source) { + if ($source->isEnabled()) { + $sourceCodes[] = $source->getSourceCode(); + } + } + + return $sourceCodes; + } +} diff --git a/src/Model/Inventory/AreProductsSalableCondition/IsSetInStockStatusForCompositeProductsCondition.php b/src/Model/Inventory/AreProductsSalableCondition/IsSetInStockStatusForCompositeProductsCondition.php new file mode 100644 index 0000000..d2bb9d2 --- /dev/null +++ b/src/Model/Inventory/AreProductsSalableCondition/IsSetInStockStatusForCompositeProductsCondition.php @@ -0,0 +1,67 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition; + +use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForSkuInterface; +use ScandiPWA\Performance\Api\Inventory\AreProductsSalableInterface; +use ScandiPWA\Performance\Api\Inventory\GetStockItemsConfigurationsInterface; + +class IsSetInStockStatusForCompositeProductsCondition implements AreProductsSalableInterface +{ + /** + * @var GetStockItemsConfigurationsInterface + */ + protected GetStockItemsConfigurationsInterface $getStockItemsConfigurations; + + /** + * @var IsSourceItemManagementAllowedForSkuInterface + */ + protected IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; + + /** + * @param IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku + * @param GetStockItemsConfigurationsInterface $getStockItemsConfigurations + */ + public function __construct( + IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku, + GetStockItemsConfigurationsInterface $getStockItemsConfigurations + ) { + $this->getStockItemsConfigurations = $getStockItemsConfigurations; + $this->isSourceItemManagementAllowedForSku = $isSourceItemManagementAllowedForSku; + } + + /** + * @inheritdoc + */ + public function execute(array $skuArray, int $stockId): array + { + $result = []; + $skusToCheck = array_flip($skuArray); + + foreach ($skuArray as $sku) { + if ($this->isSourceItemManagementAllowedForSku->execute($sku)) { + $result[$sku] = true; + unset($skusToCheck[$sku]); + } + } + + $stockItemsConfigurations = $this->getStockItemsConfigurations->execute(array_keys($skusToCheck), $stockId); + + foreach ($stockItemsConfigurations as $sku => $stockItemConfiguration) { + if ($stockItemConfiguration) { + $result[$sku] = $stockItemConfiguration->getExtensionAttributes()->getIsInStock(); + } else { + $result[$sku] = false; + } + } + + return $result; + } +} diff --git a/src/Model/Inventory/AreProductsSalableCondition/ManageStockCondition.php b/src/Model/Inventory/AreProductsSalableCondition/ManageStockCondition.php new file mode 100644 index 0000000..d15b87b --- /dev/null +++ b/src/Model/Inventory/AreProductsSalableCondition/ManageStockCondition.php @@ -0,0 +1,67 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition; + +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use ScandiPWA\Performance\Api\Inventory\AreProductsSalableInterface; +use ScandiPWA\Performance\Api\Inventory\GetStockItemsConfigurationsInterface; + +class ManageStockCondition implements AreProductsSalableInterface +{ + /** + * @var StockConfigurationInterface + */ + protected StockConfigurationInterface $configuration; + + /** + * @var GetStockItemsConfigurationsInterface + */ + protected GetStockItemsConfigurationsInterface $getStockItemsConfigurations; + + /** + * @param StockConfigurationInterface $configuration + * @param GetStockItemsConfigurationsInterface $getStockItemsConfigurations + */ + public function __construct( + StockConfigurationInterface $configuration, + GetStockItemsConfigurationsInterface $getStockItemsConfigurations + ) { + $this->getStockItemsConfigurations = $getStockItemsConfigurations; + $this->configuration = $configuration; + } + + /** + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array + { + $result = []; + + $stockItemConfigurations = $this->getStockItemsConfigurations->execute($skuArray, $stockId); + + foreach ($stockItemConfigurations as $sku => $stockItemConfiguration) { + if (!$stockItemConfiguration) { + $result[$sku] = false; + + continue; + } + + if ($stockItemConfiguration->isUseConfigManageStock()) { + $result[$sku] = $this->configuration->getManageStock() !== 1; + } else { + $result[$sku] = !$stockItemConfiguration->isManageStock(); + } + } + + return $result; + } +} diff --git a/src/Model/Inventory/AreProductsSalableConditionChain.php b/src/Model/Inventory/AreProductsSalableConditionChain.php new file mode 100644 index 0000000..f8f1c79 --- /dev/null +++ b/src/Model/Inventory/AreProductsSalableConditionChain.php @@ -0,0 +1,145 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Magento\Framework\Exception\LocalizedException; +use ScandiPWA\Performance\Api\Inventory\AreProductsSalableInterface; + +class AreProductsSalableConditionChain implements AreProductsSalableInterface +{ + /** + * @var AreProductsSalableInterface[] + */ + protected array $unrequiredConditions; + + /** + * @var AreProductsSalableInterface[] + */ + protected array $requiredConditions; + + /** + * @param array $conditions + * @throws LocalizedException + */ + public function __construct( + array $conditions + ) { + $this->setConditions($conditions); + } + + /** + * @param array $conditions + * @throws LocalizedException + */ + protected function setConditions(array $conditions) + { + $this->validateConditions($conditions); + + $unrequiredConditions = array_filter( + $conditions, + function ($item) { + return !isset($item['required']); + } + ); + + $this->unrequiredConditions = array_column($this->sortConditions($unrequiredConditions), 'object'); + + $requiredConditions = array_filter( + $conditions, + function ($item) { + return isset($item['required']) && $item['required']; + } + ); + + $this->requiredConditions = array_column($requiredConditions, 'object'); + } + + /** + * @param array $conditions + * @throws LocalizedException + */ + protected function validateConditions(array $conditions) + { + foreach ($conditions as $condition) { + if (empty($condition['object'])) { + throw new LocalizedException(__('Parameter "object" must be present.')); + } + + if (empty($condition['required']) && empty($condition['sort_order'])) { + throw new LocalizedException(__('Parameter "sort_order" must be present for unrequired conditions.')); + } + + if (!$condition['object'] instanceof AreProductsSalableInterface) { + throw new LocalizedException( + __('Condition has to implement AreProductsSalableInterface.') + ); + } + } + } + + /** + * @param array $conditions + * @return array + */ + protected function sortConditions(array $conditions): array + { + usort($conditions, function (array $conditionLeft, array $conditionRight) { + if ($conditionLeft['sort_order'] == $conditionRight['sort_order']) { + return 0; + } + + return ($conditionLeft['sort_order'] < $conditionRight['sort_order']) ? -1 : 1; + }); + + return $conditions; + } + + /** + * @inheritdoc + */ + public function execute(array $skuArray, int $stockId): array + { + $finalResults = []; + + // default value - false for all skus + foreach ($skuArray as $sku) { + $finalResults[$sku] = false; + } + + // if one of the required check fails, mark this result and skip unrequired conditions + $skusToCheck = array_flip($skuArray); + + foreach ($this->requiredConditions as $requiredCondition) { + $results = $requiredCondition->execute(array_keys($skusToCheck), $stockId); + + foreach ($results as $sku => $result) { + if (!$result) { + $finalResults[$sku] = $result; + unset($skusToCheck[$sku]); + } + } + } + + // if least one of these conditions returns True, final result for that sku can change + // (stock not managed, or backorders enabled, or available despite reservations) + foreach ($this->unrequiredConditions as $unrequiredCondition) { + $results = $unrequiredCondition->execute(array_keys($skusToCheck), $stockId); + + foreach ($results as $sku => $result) { + if ($result) { + $finalResults[$sku] = $result; + unset($skusToCheck[$sku]); + } + } + } + + return $finalResults; + } +} diff --git a/src/Model/Inventory/GetLegacyStockItems.php b/src/Model/Inventory/GetLegacyStockItems.php new file mode 100644 index 0000000..82602c0 --- /dev/null +++ b/src/Model/Inventory/GetLegacyStockItems.php @@ -0,0 +1,140 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Model\Stock; + +class GetLegacyStockItems +{ + /** + * @var StockItemInterfaceFactory + */ + protected StockItemInterfaceFactory $stockItemFactory; + + /** + * @var StockItemCriteriaInterfaceFactory + */ + protected StockItemCriteriaInterfaceFactory $legacyStockItemCriteriaFactory; + + /** + * @var StockItemRepositoryInterface + */ + protected StockItemRepositoryInterface $legacyStockItemRepository; + + /** + * @var GetProductIdsBySkus + */ + protected GetProductIdsBySkus $getProductIdsBySkus; + + /** + * Cached results + * @var array + */ + protected array $legacyStockItemsBySku = []; + + /** + * @param StockItemInterfaceFactory $stockItemFactory + * @param StockItemCriteriaInterfaceFactory $legacyStockItemCriteriaFactory + * @param StockItemRepositoryInterface $legacyStockItemRepository + * @param GetProductIdsBySkus $getProductIdsBySkus + */ + public function __construct( + StockItemInterfaceFactory $stockItemFactory, + StockItemCriteriaInterfaceFactory $legacyStockItemCriteriaFactory, + StockItemRepositoryInterface $legacyStockItemRepository, + GetProductIdsBySkus $getProductIdsBySkus + ) { + $this->stockItemFactory = $stockItemFactory; + $this->legacyStockItemCriteriaFactory = $legacyStockItemCriteriaFactory; + $this->legacyStockItemRepository = $legacyStockItemRepository; + $this->getProductIdsBySkus = $getProductIdsBySkus; + } + + /** + * Get legacy stock item entity by sku. + * + * @param array $skuArray + * @return StockItemInterface[] + */ + public function execute(array $skuArray): array + { + $resultsBySku = []; + $loadSkus = []; + + foreach ($skuArray as $sku) { + if (isset($this->legacyStockItemsBySku[$sku])) { + $resultsBySku[$sku] = $this->legacyStockItemsBySku[$sku]; + } else { + $loadSkus[] = $sku; + $resultsBySku[$sku] = null; + } + } + + if (count($loadSkus)) { + $legacyStockItems = $this->getLegacyStockItems($loadSkus); + + foreach ($legacyStockItems as $sku => $legacyStockItem) { + $this->legacyStockItemsBySku[$sku] = $legacyStockItem; + $resultsBySku[$sku] = $legacyStockItem; + } + } + + return $resultsBySku; + } + + /** + * Get legacy stock item entities by skus + * + * @param array $skuArray + * @return StockItemInterface[] + */ + public function getLegacyStockItems(array $skuArray): array + { + $results = []; + $productIds = $this->getProductIdsBySkus->execute($skuArray); + + foreach ($productIds as $sku => $productId) { + if ($productId === null) { + $stockItem = $this->stockItemFactory->create(); + // Make possible to Manage Stock for Products removed from Catalog + $stockItem->setManageStock(true); + $results[$sku] = $stockItem; + unset($productIds[$sku]); + } + } + + $productSkusById = array_flip($productIds); + + $searchCriteria = $this->legacyStockItemCriteriaFactory->create(); + $searchCriteria->setProductsFilter($productIds); + + // Stock::DEFAULT_STOCK_ID is used until we have proper multi-stock item configuration + $searchCriteria->addFilter(StockItemInterface::STOCK_ID, StockItemInterface::STOCK_ID, Stock::DEFAULT_STOCK_ID); + $stockItemCollection = $this->legacyStockItemRepository->getList($searchCriteria); + + $stockItems = $stockItemCollection->getItems(); + + foreach ($stockItems as $stockItem) { + $results[$productSkusById[$stockItem->getProductId()]] = $stockItem; + } + + foreach ($skuArray as $sku) { + if (!isset($results[$sku])) { + $results[$sku] = $this->stockItemFactory->create(); + } + } + + return $results; + } +} diff --git a/src/Model/Inventory/GetProductIdsBySkus.php b/src/Model/Inventory/GetProductIdsBySkus.php new file mode 100644 index 0000000..878992f --- /dev/null +++ b/src/Model/Inventory/GetProductIdsBySkus.php @@ -0,0 +1,47 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; +use Magento\InventoryCatalogApi\Model\GetProductIdsBySkusInterface; + +class GetProductIdsBySkus implements GetProductIdsBySkusInterface +{ + /** + * @var ProductResourceModel + */ + protected ProductResourceModel $productResource; + + /** + * @param ProductResourceModel $productResource + */ + public function __construct( + ProductResourceModel $productResource + ) { + $this->productResource = $productResource; + } + + /** + * @param array $skus + * @return array + */ + public function execute(array $skus): array + { + $idsBySkus = $this->productResource->getProductsIdsBySkus($skus); + $notFoundSkus = array_diff($skus, array_keys($idsBySkus)); + + foreach ($notFoundSkus as $sku) { + // Rewrite: as opposed to throwing NoSuchEntityException + $idsBySkus[$sku] = null; + } + + return $idsBySkus; + } +} diff --git a/src/Model/Inventory/GetProductIdsBySkusCache.php b/src/Model/Inventory/GetProductIdsBySkusCache.php new file mode 100644 index 0000000..732ae3f --- /dev/null +++ b/src/Model/Inventory/GetProductIdsBySkusCache.php @@ -0,0 +1,71 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Magento\InventoryCatalog\Model\Cache\ProductIdsBySkusStorage; +use Magento\InventoryCatalogApi\Model\GetProductIdsBySkusInterface; + +class GetProductIdsBySkusCache implements GetProductIdsBySkusInterface +{ + /** + * @var GetProductIdsBySkus + */ + protected GetProductIdsBySkus $getProductIdsBySkus; + + /** + * @var ProductIdsBySkusStorage + */ + protected ProductIdsBySkusStorage $cache; + + /** + * @param GetProductIdsBySkus $getProductIdsBySkus + * @param ProductIdsBySkusStorage $cache + */ + public function __construct( + GetProductIdsBySkus $getProductIdsBySkus, + ProductIdsBySkusStorage $cache + ) { + $this->getProductIdsBySkus = $getProductIdsBySkus; + $this->cache = $cache; + } + + /** + * Compared to core M2 - instead of throwing an exception, returns null for when sku does not exist + * @param array $skus + * @return array + */ + public function execute(array $skus): array + { + $idsBySkus = []; + $loadSkus = []; + + foreach ($skus as $sku) { + $id = $this->cache->get((string) $sku); + + if ($id !== null) { + $idsBySkus[$sku] = $id; + } else { + $loadSkus[] = $sku; + $idsBySkus[$sku] = null; + } + } + + if (count($loadSkus)) { + $loadedIdsBySkus = $this->getProductIdsBySkus->execute($loadSkus); + + foreach ($loadedIdsBySkus as $sku => $id) { + $idsBySkus[$sku] = (int) $id; + $this->cache->set((string) $sku, (int) $id); + } + } + + return $idsBySkus; + } +} diff --git a/src/Model/Inventory/GetReservationsQuantities.php b/src/Model/Inventory/GetReservationsQuantities.php new file mode 100644 index 0000000..5b18a6c --- /dev/null +++ b/src/Model/Inventory/GetReservationsQuantities.php @@ -0,0 +1,106 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Magento\Framework\App\ResourceConnection; +use Magento\InventoryReservationsApi\Model\ReservationInterface; +use ScandiPWA\Performance\Api\Inventory\GetReservationsQuantitiesInterface; + +class GetReservationsQuantities implements GetReservationsQuantitiesInterface +{ + /** + * @var ResourceConnection + */ + protected ResourceConnection $resource; + /** + * Cached results + * @var array + */ + protected array $reservationQuantitiesByStockAndSku = []; + + /** + * @param ResourceConnection $resource + */ + public function __construct( + ResourceConnection $resource + ) { + $this->resource = $resource; + } + + /** + * Get item reservations by sku array and stock + * Cache wrapper + * + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function execute(array $skuArray, int $stockId): array + { + $resultsBySku = []; + $loadSkus = []; + + foreach ($skuArray as $sku) { + if (isset($this->reservationQuantitiesByStockAndSku[$stockId][$sku])) { + $resultsBySku[$sku] = $this->reservationQuantitiesByStockAndSku[$stockId][$sku]; + } else { + $loadSkus[] = $sku; + $resultsBySku[$sku] = null; + } + } + + if (count($loadSkus)) { + $reservationsDatas = $this->getReservationsData($loadSkus, $stockId); + + foreach ($reservationsDatas as $sku => $reservationsData) { + $this->reservationQuantitiesByStockAndSku[$stockId][$sku] = $reservationsData; + $resultsBySku[$sku] = $reservationsData; + } + } + + return $resultsBySku; + } + + /** + * Get item reservations by skus and stock + * + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function getReservationsData(array $skuArray, int $stockId): array + { + $result = []; + + foreach ($skuArray as $sku) { + $result[$sku] = (float)0; + } + + $connection = $this->resource->getConnection(); + $reservationTable = $this->resource->getTableName('inventory_reservation'); + + $select = $connection->select() + ->from($reservationTable, + [ + ReservationInterface::QUANTITY => 'SUM(' . ReservationInterface::QUANTITY . ')', + ReservationInterface::SKU + ] + ) + ->where(ReservationInterface::SKU . ' IN (?)', $skuArray) + ->where(ReservationInterface::STOCK_ID . ' = ?', $stockId) + ->group(ReservationInterface::SKU); + + foreach ($connection->fetchAll($select) as $row) { + $result[$row[ReservationInterface::SKU]] = (float)$row[ReservationInterface::QUANTITY]; + } + + return $result; + } +} diff --git a/src/Model/Inventory/GetStockItemsConfigurations.php b/src/Model/Inventory/GetStockItemsConfigurations.php new file mode 100644 index 0000000..3e9c04f --- /dev/null +++ b/src/Model/Inventory/GetStockItemsConfigurations.php @@ -0,0 +1,115 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Magento\InventoryCatalogApi\Api\DefaultStockProviderInterface; +use Magento\InventoryCatalogApi\Model\GetProductTypesBySkusInterface; +use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForSkuInterface; +use Magento\InventoryConfiguration\Model\StockItemConfigurationFactory; +use ScandiPWA\Performance\Api\Inventory\GetStockItemsConfigurationsInterface; +use ScandiPWA\Performance\Api\Inventory\AreProductsAssignedToStockInterface; + +class GetStockItemsConfigurations implements GetStockItemsConfigurationsInterface +{ + /** + * @var GetLegacyStockItems + */ + protected GetLegacyStockItems $getLegacyStockItems; + + /** + * @var StockItemConfigurationFactory + */ + protected StockItemConfigurationFactory $stockItemConfigurationFactory; + + /** + * @var DefaultStockProviderInterface + */ + protected DefaultStockProviderInterface $defaultStockProvider; + + /** + * @var IsSourceItemManagementAllowedForSkuInterface + */ + protected IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; + + /** + * @var GetProductTypesBySkusInterface + */ + protected GetProductTypesBySkusInterface $getProductTypesBySkus; + + /** + * @var AreProductsAssignedToStockInterface + */ + protected AreProductsAssignedToStockInterface $areProductsAssignedToStock; + + /** + * @param GetLegacyStockItems $getLegacyStockItems + * @param StockItemConfigurationFactory $stockItemConfigurationFactory + * @param AreProductsAssignedToStockInterface $areProductsAssignedToStock + * @param DefaultStockProviderInterface $defaultStockProvider + * @param IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku + * @param GetProductTypesBySkusInterface $getProductTypesBySkus + */ + public function __construct( + GetLegacyStockItems $getLegacyStockItems, + StockItemConfigurationFactory $stockItemConfigurationFactory, + AreProductsAssignedToStockInterface $areProductsAssignedToStock, + DefaultStockProviderInterface $defaultStockProvider, + IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku, + GetProductTypesBySkusInterface $getProductTypesBySkus + ) { + $this->getLegacyStockItems = $getLegacyStockItems; + $this->stockItemConfigurationFactory = $stockItemConfigurationFactory; + $this->defaultStockProvider = $defaultStockProvider; + $this->isSourceItemManagementAllowedForSku = $isSourceItemManagementAllowedForSku; + $this->getProductTypesBySkus = $getProductTypesBySkus; + $this->areProductsAssignedToStock = $areProductsAssignedToStock; + } + + /** + * @inheritDoc + */ + public function execute(array $skuArray, int $stockId): array + { + // this will load at once & cache the product types for all skus, is used in multiple operations later + $this->getProductTypesBySkus->execute($skuArray); + $areProductsAssignedToStock = $this->areProductsAssignedToStock->execute($skuArray, $stockId); + + $result = []; + $skusToCheck = array_flip($skuArray); + + foreach ($skuArray as $sku) { + if ($this->defaultStockProvider->getId() !== $stockId + && true === $this->isSourceItemManagementAllowedForSku->execute($sku) + && false === $areProductsAssignedToStock[$sku]) { + // used instead of SkuIsNotAssignedToStockException in core + $result[$sku] = false; + + unset($skusToCheck[$sku]); + } + } + + $stockItems = $this->getLegacyStockItems->execute(array_keys($skusToCheck)); + + foreach ($stockItems as $sku => $stockItem) { + $result[$sku] = $this->stockItemConfigurationFactory->create( + [ + 'stockItem' => $stockItem + ] + ); + + // logic from LoadIsInStockPlugin + $extensionAttributes = $result[$sku]->getExtensionAttributes(); + $extensionAttributes->setIsInStock((bool)(int)$stockItem->getIsInStock()); + $result[$sku]->setExtensionAttributes($extensionAttributes); + } + + return $result; + } +} diff --git a/src/Model/Inventory/GetStockItemsData.php b/src/Model/Inventory/GetStockItemsData.php new file mode 100644 index 0000000..7e83ebb --- /dev/null +++ b/src/Model/Inventory/GetStockItemsData.php @@ -0,0 +1,278 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Exception; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\InventoryCatalogApi\Api\DefaultStockProviderInterface; +use Magento\InventoryCatalogApi\Model\IsSingleSourceModeInterface; +use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForSkuInterface; +use Magento\InventoryIndexer\Indexer\IndexStructure; +use Magento\InventoryIndexer\Model\StockIndexTableNameResolverInterface; +use ScandiPWA\Performance\Api\Inventory\GetStockItemsDataInterface; + +class GetStockItemsData implements GetStockItemsDataInterface +{ + /** + * @var ResourceConnection + */ + protected ResourceConnection $resource; + + /** + * @var StockIndexTableNameResolverInterface + */ + protected StockIndexTableNameResolverInterface $stockIndexTableNameResolver; + + /** + * @var DefaultStockProviderInterface + */ + protected DefaultStockProviderInterface $defaultStockProvider; + + /** + * @var GetProductIdsBySkus + */ + protected GetProductIdsBySkus $getProductIdsBySkus; + + /** + * @var IsSingleSourceModeInterface|null + */ + protected ?IsSingleSourceModeInterface $isSingleSourceMode; + + /** + * @var IsSourceItemManagementAllowedForSkuInterface|null + */ + protected ?IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; + + /** + * Cached results + * @var array + */ + protected array $stockItemDatasByStockAndSku = []; + + /** + * @param ResourceConnection $resource + * @param StockIndexTableNameResolverInterface $stockIndexTableNameResolver + * @param DefaultStockProviderInterface $defaultStockProvider + * @param GetProductIdsBySkus $getProductIdsBySkus + * @param IsSingleSourceModeInterface|null $isSingleSourceMode + * @param IsSourceItemManagementAllowedForSkuInterface|null $isSourceItemManagementAllowedForSku + */ + public function __construct( + ResourceConnection $resource, + StockIndexTableNameResolverInterface $stockIndexTableNameResolver, + DefaultStockProviderInterface $defaultStockProvider, + GetProductIdsBySkus $getProductIdsBySkus, + ?IsSingleSourceModeInterface $isSingleSourceMode = null, + ?IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku = null + ) { + $this->resource = $resource; + $this->stockIndexTableNameResolver = $stockIndexTableNameResolver; + $this->defaultStockProvider = $defaultStockProvider; + $this->getProductIdsBySkus = $getProductIdsBySkus; + $this->isSingleSourceMode = $isSingleSourceMode; + $this->isSourceItemManagementAllowedForSku = $isSourceItemManagementAllowedForSku; + } + + /** + * Get stock item entities by sku array and stock + * Cache wrapper + * + * @param array $skuArray + * @param int $stockId + * @return array + * @throws LocalizedException + */ + public function execute(array $skuArray, int $stockId): array + { + $resultsBySku = []; + $loadSkus = []; + + foreach ($skuArray as $sku) { + if (isset($this->stockItemDatasByStockAndSku[$stockId][$sku])) { + $resultsBySku[$sku] = $this->stockItemDatasByStockAndSku[$stockId][$sku]; + } else { + $loadSkus[] = $sku; + $resultsBySku[$sku] = null; + } + } + + if (count($loadSkus)) { + $stockItemDatas = $this->getStockItems($loadSkus, $stockId); + + foreach ($stockItemDatas as $sku => $stockItemData) { + $this->stockItemDatasByStockAndSku[$stockId][$sku] = $stockItemData; + $resultsBySku[$sku] = $stockItemData; + } + } + + return $resultsBySku; + } + + /** + * Get stock item entities by skus and stock + * + * @param array $skuArray + * @param int $stockId + * @return array + */ + public function getStockItems(array $skuArray, int $stockId): array + { + $result = []; + + foreach ($skuArray as $sku) { + $result[$sku] = null; + } + + $connection = $this->resource->getConnection(); + $select = $connection->select(); + + if ($this->defaultStockProvider->getId() === $stockId) { + $productIds = $this->getProductIdsBySkus->execute($skuArray); + + foreach ($productIds as $sku => $productId) { + if ($productId === null) { + $result[$sku] = null; + unset($productIds[$sku]); + } + } + + $productSkusById = array_flip($productIds); + + $select->from( + $this->resource->getTableName('cataloginventory_stock_status'), + [ + GetStockItemsDataInterface::QUANTITY => 'qty', + GetStockItemsDataInterface::IS_SALABLE => 'stock_status', + 'product_id' + ] + )->where( + 'product_id IN (?)', + $productIds + ); + } else { + $select->from( + $this->stockIndexTableNameResolver->execute($stockId), + [ + GetStockItemsDataInterface::QUANTITY => IndexStructure::QUANTITY, + GetStockItemsDataInterface::IS_SALABLE => IndexStructure::IS_SALABLE, + IndexStructure::SKU + ] + )->where( + IndexStructure::SKU . ' IN (?)', + $skuArray + ); + } + + try { + foreach ($connection->fetchAll($select) as $row) { + if ($this->defaultStockProvider->getId() === $stockId) { + $result[$productSkusById[$row['product_id']]] = [ + GetStockItemsDataInterface::QUANTITY => $row[GetStockItemsDataInterface::QUANTITY], + GetStockItemsDataInterface::IS_SALABLE => $row[GetStockItemsDataInterface::IS_SALABLE] + ]; + } else { + $result[$row[IndexStructure::SKU]] = [ + GetStockItemsDataInterface::QUANTITY => $row[GetStockItemsDataInterface::QUANTITY], + GetStockItemsDataInterface::IS_SALABLE => $row[GetStockItemsDataInterface::IS_SALABLE] + ]; + } + } + } catch (Exception $e) { + throw new LocalizedException(__('Could not receive Stock Item data'), $e); + } + + /** + * Fallback to the legacy cataloginventory_stock_item table. + * Caused by data absence in legacy cataloginventory_stock_status table + * for disabled products assigned to the default stock. + */ + $missingStockItemSkus = []; + + foreach ($skuArray as $sku) { + if ($result[$sku] === null) { + $missingStockItemSkus[] = $sku; + } + } + + if (count($missingStockItemSkus)) { + $missingStockItemData = $this->getStockItemDataFromStockItemTable($missingStockItemSkus, $stockId); + + foreach ($missingStockItemData as $sku => $row) { + $result[$sku] = $row; + } + } + + return $result; + } + + /** + * Retrieve stock item data for products assigned to the default stock. + * + * @param array $skuArray + * @param int $stockId + * @return array + */ + protected function getStockItemDataFromStockItemTable(array $skuArray, int $stockId): array + { + $result = []; + + foreach ($skuArray as $sku) { + $result[$sku] = null; + } + + $skusToCheck = array_flip($skuArray); + + foreach ($skuArray as $sku) { + if ($this->defaultStockProvider->getId() !== $stockId + || $this->isSingleSourceMode->execute() + || !$this->isSourceItemManagementAllowedForSku->execute($sku) + ) { + $result[$sku] = null; + unset($skusToCheck[$sku]); + } + } + + $productIds = $this->getProductIdsBySkus->execute(array_keys($skusToCheck)); + + foreach ($productIds as $sku => $productId) { + if ($productId === null) { + $result[$sku] = null; + unset($skusToCheck[$sku]); + unset($productIds[$sku]); + } + } + + $productSkusById = array_flip($productIds); + + $connection = $this->resource->getConnection(); + $select = $connection->select(); + $select->from( + $this->resource->getTableName('cataloginventory_stock_item'), + [ + GetStockItemsDataInterface::QUANTITY => 'qty', + GetStockItemsDataInterface::IS_SALABLE => 'is_in_stock', + 'product_id' + ] + )->where( + 'product_id IN (?)', + $productIds + ); + + foreach ($connection->fetchAll($select) as $row) { + $result[$productSkusById[$row['product_id']]] = [ + GetStockItemsDataInterface::QUANTITY => $row[GetStockItemsDataInterface::QUANTITY], + GetStockItemsDataInterface::IS_SALABLE => $row[GetStockItemsDataInterface::IS_SALABLE] + ]; + } + + return $result; + } +} diff --git a/src/Model/Resolver/Products/DataPostProcessor/Stocks.php b/src/Model/Resolver/Products/DataPostProcessor/Stocks.php deleted file mode 100644 index 749d4a5..0000000 --- a/src/Model/Resolver/Products/DataPostProcessor/Stocks.php +++ /dev/null @@ -1,292 +0,0 @@ - - * @copyright Copyright (c) 2019 Scandiweb, Ltd (https://scandiweb.com) - */ - -declare(strict_types=1); - -namespace ScandiPWA\Performance\Model\Resolver\Products\DataPostProcessor; - -use Magento\CatalogInventory\Model\Configuration; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\InventoryApi\Api\Data\SourceItemInterface; -use Magento\InventoryApi\Api\SourceItemRepositoryInterface; -use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForProductTypeInterface; -use Magento\Store\Model\ScopeInterface; -use ScandiPWA\Performance\Api\ProductsDataPostProcessorInterface; -use ScandiPWA\Performance\Model\Resolver\ResolveInfoFieldsTrait; -use Magento\InventoryApi\Api\GetStockSourceLinksInterface; -use Magento\InventoryApi\Api\Data\StockSourceLinkInterface; -use Magento\InventoryCatalog\Model\GetStockIdForCurrentWebsite; -use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface; -use Magento\InventoryConfigurationApi\Api\GetStockItemConfigurationInterface; - -/** - * Class Images - * @package ScandiPWA\Performance\Model\Resolver\Products\DataPostProcessor - */ -class Stocks implements ProductsDataPostProcessorInterface -{ - use ResolveInfoFieldsTrait; - - const ONLY_X_LEFT_IN_STOCK = 'only_x_left_in_stock'; - - const STOCK_STATUS = 'stock_status'; - - const SALABLE_QTY = 'salable_qty'; - - const IN_STOCK = 'IN_STOCK'; - - const OUT_OF_STOCK = 'OUT_OF_STOCK'; - - /** - * @var SourceItemRepositoryInterface - */ - protected $stockRepository; - - /** - * @var SearchCriteriaBuilder - */ - protected $searchCriteriaBuilder; - - /** - * @var ScopeConfigInterface - */ - protected $scopeConfig; - - /** - * @var GetStockSourceLinksInterface - */ - protected $getStockSourceLinks; - - /** - * @var GetStockIdForCurrentWebsite - */ - protected $getStockIdForCurrentWebsite; - - /** - * @var GetProductSalableQtyInterface - */ - protected $getProductSalableQty; - - /** - * @var GetStockItemConfigurationInterface - */ - protected $getStockItemConfiguration; - - /** - * @var IsSourceItemManagementAllowedForProductTypeInterface - */ - protected $isSourceItemManagementAllowedForProductType; - - /** - * Stocks constructor. - * @param SourceItemRepositoryInterface $stockRepository - * @param SearchCriteriaBuilder $searchCriteriaBuilder - * @param ScopeConfigInterface $scopeConfig - */ - public function __construct( - SourceItemRepositoryInterface $stockRepository, - SearchCriteriaBuilder $searchCriteriaBuilder, - ScopeConfigInterface $scopeConfig, - GetStockSourceLinksInterface $getStockSourceLinks, - GetStockIdForCurrentWebsite $getStockIdForCurrentWebsite, - GetStockItemConfigurationInterface $getStockItemConfiguration, - GetProductSalableQtyInterface $getProductSalableQty, - IsSourceItemManagementAllowedForProductTypeInterface $isSourceItemManagementAllowedForProductType - ) { - $this->searchCriteriaBuilder = $searchCriteriaBuilder; - $this->stockRepository = $stockRepository; - $this->scopeConfig = $scopeConfig; - $this->getStockSourceLinks = $getStockSourceLinks; - $this->getStockIdForCurrentWebsite = $getStockIdForCurrentWebsite; - $this->getStockItemConfiguration = $getStockItemConfiguration; - $this->getProductSalableQty = $getProductSalableQty; - $this->isSourceItemManagementAllowedForProductType = $isSourceItemManagementAllowedForProductType; - } - - /** - * @param $node - * @return string[] - */ - protected function getFieldContent($node) - { - $stocks = []; - $validFields = [ - self::ONLY_X_LEFT_IN_STOCK, - self::STOCK_STATUS - ]; - - foreach ($node->selectionSet->selections as $selection) { - if (!isset($selection->name)) { - continue; - }; - - $name = $selection->name->value; - - if (in_array($name, $validFields)) { - $stocks[] = $name; - } - } - - return $stocks; - } - - /** - * @inheritDoc - */ - public function process( - array $products, - string $graphqlResolvePath, - $graphqlResolveInfo, - ?array $processorOptions = [] - ): callable { - $productStocks = []; - $productTypes = []; - - $fields = $this->getFieldsFromProductInfo( - $graphqlResolveInfo, - $graphqlResolvePath - ); - - if (!count($fields)) { - return function (&$productData) { - }; - } - - $stockId = $this->getStockIdForCurrentWebsite->execute(); - - if (!$stockId) { - return function (&$productData) { - }; - } - - foreach ($products as $product) { - $productTypes[$product->getSku()] = $product->getTypeId(); - } - - $productSKUs = array_keys($productTypes); - - $thresholdQty = 0; - - if (in_array(self::ONLY_X_LEFT_IN_STOCK, $fields)) { - $thresholdQty = (float) $this->scopeConfig->getValue( - Configuration::XML_PATH_STOCK_THRESHOLD_QTY, - ScopeInterface::SCOPE_STORE - ); - } - - $criteria = $this->searchCriteriaBuilder - ->addFilter(StockSourceLinkInterface::STOCK_ID, $stockId) - ->create(); - - $sourceLinks = $this->getStockSourceLinks->execute($criteria)->getItems(); - - if (!count($sourceLinks)) { - return function (&$productData) { - }; - } - - $sourceCodes = array_map(function ($sourceLink) { - return $sourceLink->getSourceCode(); - }, $sourceLinks); - - $criteria = $this->searchCriteriaBuilder - ->addFilter(SourceItemInterface::SKU, $productSKUs, 'in') - ->addFilter(SourceItemInterface::SOURCE_CODE, $sourceCodes, 'in') - ->create(); - - $stockItems = $this->stockRepository->getList($criteria)->getItems(); - - if (!count($stockItems)) { - return function (&$productData) { - }; - } - - $formattedStocks = []; - - foreach ($stockItems as $stockItem) { - $leftInStock = null; - $productSalableQty = null; - $qty = $stockItem->getQuantity(); - $sku = $stockItem->getSku(); - - $inStock = $stockItem->getStatus() === SourceItemInterface::STATUS_IN_STOCK; - - if (isset($productTypes[$sku]) - && !$this->isSourceItemManagementAllowedForProductType->execute($productTypes[$sku]) - ) { - $formattedStocks[$sku] = [ - self::STOCK_STATUS => $inStock ? self::IN_STOCK : self::OUT_OF_STOCK, - ]; - - continue; - } - - $inStock = $qty > 0; - - if ($inStock) { - $productSalableQty = $this->getProductSalableQty->execute($sku, $stockId); - - if ($productSalableQty > 0) { - $stockItemConfiguration = $this->getStockItemConfiguration->execute($sku, $stockId); - $minQty = $stockItemConfiguration->getMinQty(); - - if ($productSalableQty >= $minQty) { - $stockLeft = $productSalableQty - $minQty; - $thresholdQty = $stockItemConfiguration->getStockThresholdQty(); - - if ($thresholdQty != 0) { - $leftInStock = $stockLeft <= $thresholdQty ? (float)$stockLeft : null; - } - } else { - $inStock = false; - } - } else { - $inStock = false; - } - - } - - if (isset($formattedStocks[$sku]) - && $formattedStocks[$sku][self::STOCK_STATUS] == self::IN_STOCK) { - continue; - } - - $formattedStocks[$sku] = [ - self::STOCK_STATUS => $inStock ? self::IN_STOCK : self::OUT_OF_STOCK, - self::ONLY_X_LEFT_IN_STOCK => $leftInStock, - self::SALABLE_QTY => $productSalableQty - ]; - } - - foreach ($products as $product) { - $productId = $product->getId(); - $productSku = $product->getSku(); - - if (isset($formattedStocks[$productSku])) { - $productStocks[$productId] = $formattedStocks[$productSku]; - } - } - - return function (&$productData) use ($productStocks) { - if (!isset($productData['entity_id'])) { - return; - } - - $productId = $productData['entity_id']; - - if (!isset($productStocks[$productId])) { - return; - } - - foreach ($productStocks[$productId] as $stockType => $stockData) { - $productData[$stockType] = $stockData; - } - }; - } -} diff --git a/src/Model/Resolver/Products/StockItem.php b/src/Model/Resolver/Products/StockItem.php new file mode 100644 index 0000000..3d34b3d --- /dev/null +++ b/src/Model/Resolver/Products/StockItem.php @@ -0,0 +1,70 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Resolver\Products; + +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\Framework\GraphQl\Query\Resolver\BatchServiceContractResolverInterface; +use Magento\Framework\GraphQl\Query\Resolver\ResolveRequestInterface; +use ScandiPWA\Performance\Model\Resolver\Products\StockItem\GetStockItem; +use ScandiPWA\Performance\Model\Resolver\Products\StockItem\ProductCriteria; + +class StockItem implements BatchServiceContractResolverInterface +{ + /** + * @var GetStockItem + */ + protected GetStockItem $getStockItem; + + /** + * @param GetStockItem $getStockItem + */ + public function __construct(GetStockItem $getStockItem) + { + $this->getStockItem = $getStockItem; + } + + /** + * @inheritDoc + */ + public function getServiceContract(): array + { + return [GetStockItem::class, 'execute']; + } + + /** + * @inheritDoc + */ + public function convertToServiceArgument(ResolveRequestInterface $request): ProductCriteria + { + // add criteria to a "shared" queue to load data + // this is necessary, because otherwise BatchContractResolverWrapper + // is going to make an individual GetStockItem batch per product type, which is unnecessary + $this->getStockItem->addSkuToQueue($request->getValue()['model']->getSku()); + + // criteria for this particular request + return new ProductCriteria($request->getValue()['model']->getSku()); + } + + /** + * @param StockItemInterface $result + * @param ResolveRequestInterface $request + * @return array + */ + public function convertFromServiceResult( + $result, + ResolveRequestInterface $request + ): array { + return [ + 'min_sale_qty' => $result->getMinSaleQty(), + 'max_sale_qty' => $result->getMaxSaleQty(), + 'qty_increments' => $result->getQtyIncrements() === false ? 1 : $result->getQtyIncrements() + ]; + } +} diff --git a/src/Model/Resolver/Products/StockItem/GetStockItem.php b/src/Model/Resolver/Products/StockItem/GetStockItem.php new file mode 100644 index 0000000..0cee029 --- /dev/null +++ b/src/Model/Resolver/Products/StockItem/GetStockItem.php @@ -0,0 +1,120 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Resolver\Products\StockItem; + +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\InventoryCatalog\Model\GetStockIdForCurrentWebsite; +use ScandiPWA\Performance\Model\Inventory\GetLegacyStockItems; + +class GetStockItem +{ + /** + * @var GetStockIdForCurrentWebsite + */ + protected GetStockIdForCurrentWebsite $getStockIdForCurrentWebsite; + + /** + * @var GetLegacyStockItems + */ + protected GetLegacyStockItems $getLegacyStockItems; + + /** + * @var array + */ + protected array $unprocessedSkuQueue = []; + + /** + * @var StockItemInterface[] + */ + protected array $processedResults = []; + + /** + * @param GetStockIdForCurrentWebsite $getStockIdForCurrentWebsite + * @param GetLegacyStockItems $getLegacyStockItems + */ + public function __construct( + GetStockIdForCurrentWebsite $getStockIdForCurrentWebsite, + GetLegacyStockItems $getLegacyStockItems + ) { + $this->getStockIdForCurrentWebsite = $getStockIdForCurrentWebsite; + $this->getLegacyStockItems = $getLegacyStockItems; + } + + /** + * @param ProductCriteria[] $criteriaList + * @return IsProductSalableResultInterface[] + */ + public function execute(array $criteriaList): array + { + if (count($this->unprocessedSkuQueue)) { + $this->processQueue(); + } + + $skuArray = $finalResults = []; + + foreach ($criteriaList as $productCriteria) { + $skuArray[] = $productCriteria->getSku(); + } + + // mapping a request index to specific sku + // single SKU request may have more than one index, in case the criteria for current batch + // somehow repeats the same sku multiple times + $skuIndexes = []; + + foreach ($skuArray as $index => $sku) { + $skuIndexes[$sku][] = $index; + } + + foreach ($skuArray as $sku) { + foreach ($skuIndexes[$sku] as $index) { + $finalResults[$index] = $this->processedResults[$sku]; + } + } + + return $finalResults; + } + + /** + * @return void + */ + public function processQueue(): void + { + $skuArray = $this->unprocessedSkuQueue; + $stockId = $this->getStockIdForCurrentWebsite->execute(); + + // note: getLegacyStockItems caches its results, and is also used in stock_status fetch + $results = $this->getLegacyStockItems->execute($skuArray); + + foreach ($results as $sku => $stockItem) { + $this->processedResults[$sku] = $stockItem; + } + + $this->unprocessedSkuQueue = []; + } + + /** + * Adds SKU to a queue + * Will skip it if already processed + * This is implemented, because the execution flow looks like this: + * 1. StockStatus::convertToServiceArgument is run many times + * but within individual BatchContractResolverWrapper instances per product type + * 2. AreProductsSalable::execute is called for one BatchContractResolverWrapper instance & its request at a time + * 3. Variants / bundles are creating their own BatchContractResolverWrapper instances and are building arguments + * after the parent item requests resolve + * This mechanism of adding skus to queue will resolve them for all product types at once, for parent products + * @param string $sku + */ + public function addSkuToQueue(string $sku): void + { + if (!isset($this->processedResults[$sku]) && !in_array($sku, $this->unprocessedSkuQueue)) { + $this->unprocessedSkuQueue[] = $sku; + } + } +} diff --git a/src/Model/Resolver/Products/StockItem/ProductCriteria.php b/src/Model/Resolver/Products/StockItem/ProductCriteria.php new file mode 100644 index 0000000..7c06811 --- /dev/null +++ b/src/Model/Resolver/Products/StockItem/ProductCriteria.php @@ -0,0 +1,34 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Resolver\Products\StockItem; + +class ProductCriteria +{ + /** + * @var string + */ + protected string $sku; + + /** + * @param string $sku + */ + public function __construct(string $sku) + { + $this->sku = $sku; + } + + /** + * @return string + */ + public function getSku(): string + { + return $this->sku; + } +} diff --git a/src/Model/Resolver/Products/StockStatus.php b/src/Model/Resolver/Products/StockStatus.php new file mode 100644 index 0000000..3dd936b --- /dev/null +++ b/src/Model/Resolver/Products/StockStatus.php @@ -0,0 +1,63 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Resolver\Products; + +use Magento\Framework\GraphQl\Query\Resolver\BatchServiceContractResolverInterface; +use Magento\Framework\GraphQl\Query\Resolver\ResolveRequestInterface; +use ScandiPWA\Performance\Model\Resolver\Products\StockStatus\AreProductsSalable; +use ScandiPWA\Performance\Model\Resolver\Products\StockStatus\ProductCriteria; + +class StockStatus implements BatchServiceContractResolverInterface +{ + /** + * @var AreProductsSalable + */ + protected AreProductsSalable $areProductsSalable; + + /** + * @param AreProductsSalable $areProductsSalable + */ + public function __construct(AreProductsSalable $areProductsSalable) + { + $this->areProductsSalable = $areProductsSalable; + } + + /** + * @inheritDoc + */ + public function getServiceContract(): array + { + return [AreProductsSalable::class, 'execute']; + } + + /** + * @inheritDoc + */ + public function convertToServiceArgument(ResolveRequestInterface $request): ProductCriteria + { + // add criteria to a "shared" queue to load data + // this is necessary, because otherwise BatchContractResolverWrapper + // is going to make an individual AreProductsSalable batch per product type, which is unnecessary + $this->areProductsSalable->addSkuToQueue($request->getValue()['model']->getSku()); + + // criteria for this particular request + return new ProductCriteria($request->getValue()['model']->getSku()); + } + + /** + * @inheritDoc + */ + public function convertFromServiceResult( + $result, + ResolveRequestInterface $request + ): string { + return $result->isSalable() ? 'IN_STOCK' : 'OUT_OF_STOCK'; + } +} diff --git a/src/Model/Resolver/Products/StockStatus/AreProductsSalable.php b/src/Model/Resolver/Products/StockStatus/AreProductsSalable.php new file mode 100644 index 0000000..1819677 --- /dev/null +++ b/src/Model/Resolver/Products/StockStatus/AreProductsSalable.php @@ -0,0 +1,134 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Resolver\Products\StockStatus; + +use Magento\InventoryCatalog\Model\GetStockIdForCurrentWebsite; +use Magento\InventorySalesApi\Api\Data\IsProductSalableResultInterface; +use Magento\InventorySalesApi\Api\Data\IsProductSalableResultInterfaceFactory; +use ScandiPWA\Performance\Model\Inventory\AreProductsSalableConditionChain; + +class AreProductsSalable +{ + /** + * @var GetStockIdForCurrentWebsite + */ + protected GetStockIdForCurrentWebsite $getStockIdForCurrentWebsite; + + /** + * @var AreProductsSalableConditionChain + */ + protected AreProductsSalableConditionChain $areProductsSalableConditionChain; + + /** + * @var IsProductSalableResultInterfaceFactory + */ + protected IsProductSalableResultInterfaceFactory $isProductSalableResultFactory; + + /** + * @var array + */ + protected array $unprocessedSkuQueue = []; + + /** + * @var IsProductSalableResultInterface[] + */ + protected array $processedResults = []; + + /** + * @param GetStockIdForCurrentWebsite $getStockIdForCurrentWebsite + * @param AreProductsSalableConditionChain $areProductsSalableConditionChain + * @param IsProductSalableResultInterfaceFactory $isProductSalableResultFactory + */ + public function __construct( + GetStockIdForCurrentWebsite $getStockIdForCurrentWebsite, + AreProductsSalableConditionChain $areProductsSalableConditionChain, + IsProductSalableResultInterfaceFactory $isProductSalableResultFactory + ) { + $this->getStockIdForCurrentWebsite = $getStockIdForCurrentWebsite; + $this->areProductsSalableConditionChain = $areProductsSalableConditionChain; + $this->isProductSalableResultFactory = $isProductSalableResultFactory; + } + + /** + * @param ProductCriteria[] $criteriaList + * @return IsProductSalableResultInterface[] + */ + public function execute(array $criteriaList): array + { + if (count($this->unprocessedSkuQueue)) { + $this->processQueue(); + } + + $skuArray = $finalResults = []; + + foreach ($criteriaList as $productCriteria) { + $skuArray[] = $productCriteria->getSku(); + } + + // mapping a request index to specific sku + // single SKU request may have more than one index, in case the criteria for current batch + // somehow repeats the same sku multiple times + $skuIndexes = []; + + foreach ($skuArray as $index => $sku) { + $skuIndexes[$sku][] = $index; + } + + foreach ($skuArray as $sku) { + foreach ($skuIndexes[$sku] as $index) { + $finalResults[$index] = $this->processedResults[$sku]; + } + } + + return $finalResults; + } + + /** + * @return void + */ + public function processQueue(): void + { + $skuArray = $this->unprocessedSkuQueue; + + $stockId = $this->getStockIdForCurrentWebsite->execute(); + $areProductsSalable = $this->areProductsSalableConditionChain->execute($skuArray, $stockId); + + foreach ($areProductsSalable as $sku => $isSalable) { + $this->processedResults[$sku] = $this->isProductSalableResultFactory->create( + [ + 'sku' => $sku, + 'stockId' => $stockId, + 'isSalable' => $isSalable, + ] + ); + } + + $this->unprocessedSkuQueue = []; + } + + /** + * Adds SKU to a queue + * Will skip it if already processed + * This is implemented, because the execution flow looks like this: + * 1. StockStatus::convertToServiceArgument is run many times + * but within individual BatchContractResolverWrapper instances per product type + * 2. AreProductsSalable::execute is called for one BatchContractResolverWrapper instance & its request at a time + * 3. Variants / bundles are creating their own BatchContractResolverWrapper instances and are building arguments + * after the parent item requests resolve + * This mechanism of adding skus to queue will resolve them for all product types at once, for parent products + * @param string $sku + */ + public function addSkuToQueue(string $sku): void + { + if (!isset($this->processedResults[$sku]) && !in_array($sku, $this->unprocessedSkuQueue)) { + $this->unprocessedSkuQueue[] = $sku; + } + } +} diff --git a/src/Model/Resolver/Products/StockStatus/ProductCriteria.php b/src/Model/Resolver/Products/StockStatus/ProductCriteria.php new file mode 100644 index 0000000..3717e94 --- /dev/null +++ b/src/Model/Resolver/Products/StockStatus/ProductCriteria.php @@ -0,0 +1,34 @@ + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Resolver\Products\StockStatus; + +class ProductCriteria +{ + /** + * @var string + */ + protected string $sku; + + /** + * @param string $sku + */ + public function __construct(string $sku) + { + $this->sku = $sku; + } + + /** + * @return string + */ + public function getSku(): string + { + return $this->sku; + } +} diff --git a/src/etc/di.xml b/src/etc/di.xml index 703663d..9678bf6 100644 --- a/src/etc/di.xml +++ b/src/etc/di.xml @@ -15,7 +15,6 @@ ScandiPWA\Performance\Model\Resolver\Products\DataPostProcessor\Attributes ScandiPWA\Performance\Model\Resolver\Products\DataPostProcessor\Images - ScandiPWA\Performance\Model\Resolver\Products\DataPostProcessor\Stocks @@ -30,4 +29,50 @@ + + + + + + true + + ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition\IsSetInStockStatusForCompositeProductsCondition + + + + true + + ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition\IsAnySourceItemInStockCondition + + + + 10 + + ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition\BackOrderCondition + + + + 20 + + ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition\ManageStockCondition + + + + 30 + + ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition\AreSalableWithReservationsCondition + + + + + + + + + + diff --git a/src/etc/schema.graphqls b/src/etc/schema.graphqls index 436afd4..bea2764 100644 --- a/src/etc/schema.graphqls +++ b/src/etc/schema.graphqls @@ -8,11 +8,18 @@ interface ProductInterface { small_image: OptimizedProductImage @resolver(class: "ScandiPWA\\Performance\\Model\\Resolver\\Value") thumbnail: OptimizedProductImage @resolver(class: "ScandiPWA\\Performance\\Model\\Resolver\\Value") only_x_left_in_stock: Float @resolver(class: "ScandiPWA\\Performance\\Model\\Resolver\\Value") - stock_status: ProductStockStatus @resolver(class: "ScandiPWA\\Performance\\Model\\Resolver\\Value") + stock_status: ProductStockStatus @doc(description: "Stock status of the product") @resolver(class: "ScandiPWA\\Performance\\Model\\Resolver\\Products\\StockStatus") + stock_item: ProductStockItem @resolver(class: "ScandiPWA\\Performance\\Model\\Resolver\\Products\\StockItem") salable_qty: Float @resolver(class: "ScandiPWA\\Performance\\Model\\Resolver\\Value") s_attributes: [AttributeWithValue] } +type ProductStockItem { + min_sale_qty: Int @doc(description: "Minimal amount of item that can be bought") + max_sale_qty: Int @doc(description: "Maximal amount of item that can be bought") + qty_increments: Int @doc(description: "Increment for number of items that can be bought") +} + type AttributeWithValue { attribute_code: String entity_type: String From 51578040f83af419c9711f3e96e7ffda6620e2a9 Mon Sep 17 00:00:00 2001 From: Aleksandrs Mokans Date: Mon, 1 Aug 2022 05:08:02 +0300 Subject: [PATCH 2/2] QA fixes for class props + typecasting skus just in case --- .../IsAnySourceItemInStockCondition.php | 320 +++++++++--------- ...ockStatusForCompositeProductsCondition.php | 2 +- .../Inventory/GetStockItemsConfigurations.php | 230 ++++++------- src/Model/Inventory/GetStockItemsData.php | 14 +- 4 files changed, 283 insertions(+), 283 deletions(-) diff --git a/src/Model/Inventory/AreProductsSalableCondition/IsAnySourceItemInStockCondition.php b/src/Model/Inventory/AreProductsSalableCondition/IsAnySourceItemInStockCondition.php index 99bea5d..7af2a0a 100644 --- a/src/Model/Inventory/AreProductsSalableCondition/IsAnySourceItemInStockCondition.php +++ b/src/Model/Inventory/AreProductsSalableCondition/IsAnySourceItemInStockCondition.php @@ -1,160 +1,160 @@ - - * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) - * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) - */ -declare(strict_types=1); - -namespace ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition; - -use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Exception\InputException; -use Magento\Framework\Exception\LocalizedException; -use Magento\Inventory\Model\ResourceModel\SourceItem\Collection; -use Magento\Inventory\Model\SourceItem; -use Magento\InventoryApi\Api\Data\SourceItemInterface; -use Magento\InventoryApi\Api\GetSourcesAssignedToStockOrderedByPriorityInterface; -use Magento\InventoryApi\Api\SourceItemRepositoryInterface; -use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForSkuInterface; -use Magento\Inventory\Model\ResourceModel\SourceItem\CollectionFactory; -use ScandiPWA\Performance\Api\Inventory\AreProductsSalableInterface; - -class IsAnySourceItemInStockCondition implements AreProductsSalableInterface -{ - /** - * @var SourceItemRepositoryInterface - */ - protected SourceItemRepositoryInterface $sourceItemRepository; - - /** - * @var SearchCriteriaBuilder - */ - protected SearchCriteriaBuilder $searchCriteriaBuilder; - - /** - * @var GetSourcesAssignedToStockOrderedByPriorityInterface - */ - protected GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority; - - /** - * @var IsSourceItemManagementAllowedForSkuInterface - */ - protected IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; - - /** - * @var ManageStockCondition - */ - protected ManageStockCondition $manageStockCondition; - - /** - * @var CollectionProcessorInterface - */ - protected CollectionProcessorInterface $collectionProcessor; - - /** - * @var CollectionFactory - */ - protected CollectionFactory $sourceItemCollectionFactory; - - /** - * @param SourceItemRepositoryInterface $sourceItemRepository - * @param SearchCriteriaBuilder $searchCriteriaBuilder - * @param GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority - * @param IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku - * @param ManageStockCondition $manageStockCondition - * @param CollectionProcessorInterface $collectionProcessor - * @param CollectionFactory $sourceItemCollectionFactory - */ - public function __construct( - SourceItemRepositoryInterface $sourceItemRepository, - SearchCriteriaBuilder $searchCriteriaBuilder, - GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority, - IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku, - ManageStockCondition $manageStockCondition, - CollectionProcessorInterface $collectionProcessor, - CollectionFactory $sourceItemCollectionFactory - ) { - $this->sourceItemRepository = $sourceItemRepository; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; - $this->getSourcesAssignedToStockOrderedByPriority = $getSourcesAssignedToStockOrderedByPriority; - $this->isSourceItemManagementAllowedForSku = $isSourceItemManagementAllowedForSku; - $this->manageStockCondition = $manageStockCondition; - $this->collectionProcessor = $collectionProcessor; - $this->sourceItemCollectionFactory = $sourceItemCollectionFactory; - } - - /** - * @param array $skuArray - * @param int $stockId - * @return array - * @throws InputException - * @throws LocalizedException - */ - public function execute(array $skuArray, int $stockId): array - { - $result = $this->manageStockCondition->execute($skuArray, $stockId); - - $skusToCheck = []; - - foreach ($result as $sku => $value) { - // if value is true, that is a final result; otherwise, proceed with the next checks - if (!$value) { - $skusToCheck[$sku] = null; - } - } - - foreach (array_keys($skusToCheck) as $sku) { - if (!$this->isSourceItemManagementAllowedForSku->execute($sku)) { - // if source item management is not allowed, that is a final result for that sku - $result[$sku] = true; - - unset($skusToCheck[$sku]); - } - } - - $sourceCodes = $this->getSourceCodesAssignedToStock($stockId); - - $searchCriteria = $this->searchCriteriaBuilder - ->addFilter(SourceItemInterface::SKU, array_keys($skusToCheck), 'in') - ->addFilter(SourceItemInterface::SOURCE_CODE, $sourceCodes, 'in') - ->addFilter(SourceItemInterface::STATUS, SourceItemInterface::STATUS_IN_STOCK) - ->create(); - - /** @var Collection $collection */ - $collection = $this->sourceItemCollectionFactory->create(); - $this->collectionProcessor->process($searchCriteria, $collection); - - /** @var SourceItem $item */ - foreach ($collection as $item) { - $result[$item->getSku()] = true; - } - - return $result; - } - - /** - * Provides source codes for certain stock - * - * @param int $stockId - * - * @return array - * @throws InputException - * @throws LocalizedException - */ - protected function getSourceCodesAssignedToStock(int $stockId): array - { - $sources = $this->getSourcesAssignedToStockOrderedByPriority->execute($stockId); - $sourceCodes = []; - - foreach ($sources as $source) { - if ($source->isEnabled()) { - $sourceCodes[] = $source->getSourceCode(); - } - } - - return $sourceCodes; - } -} + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory\AreProductsSalableCondition; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Inventory\Model\ResourceModel\SourceItem\Collection; +use Magento\Inventory\Model\SourceItem; +use Magento\InventoryApi\Api\Data\SourceItemInterface; +use Magento\InventoryApi\Api\GetSourcesAssignedToStockOrderedByPriorityInterface; +use Magento\InventoryApi\Api\SourceItemRepositoryInterface; +use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForSkuInterface; +use Magento\Inventory\Model\ResourceModel\SourceItem\CollectionFactory; +use ScandiPWA\Performance\Api\Inventory\AreProductsSalableInterface; + +class IsAnySourceItemInStockCondition implements AreProductsSalableInterface +{ + /** + * @var SourceItemRepositoryInterface + */ + protected SourceItemRepositoryInterface $sourceItemRepository; + + /** + * @var SearchCriteriaBuilder + */ + protected SearchCriteriaBuilder $searchCriteriaBuilder; + + /** + * @var GetSourcesAssignedToStockOrderedByPriorityInterface + */ + protected GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority; + + /** + * @var IsSourceItemManagementAllowedForSkuInterface + */ + protected IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; + + /** + * @var ManageStockCondition + */ + protected ManageStockCondition $manageStockCondition; + + /** + * @var CollectionProcessorInterface + */ + protected CollectionProcessorInterface $collectionProcessor; + + /** + * @var CollectionFactory + */ + protected CollectionFactory $sourceItemCollectionFactory; + + /** + * @param SourceItemRepositoryInterface $sourceItemRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority + * @param IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku + * @param ManageStockCondition $manageStockCondition + * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionFactory $sourceItemCollectionFactory + */ + public function __construct( + SourceItemRepositoryInterface $sourceItemRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority, + IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku, + ManageStockCondition $manageStockCondition, + CollectionProcessorInterface $collectionProcessor, + CollectionFactory $sourceItemCollectionFactory + ) { + $this->sourceItemRepository = $sourceItemRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->getSourcesAssignedToStockOrderedByPriority = $getSourcesAssignedToStockOrderedByPriority; + $this->isSourceItemManagementAllowedForSku = $isSourceItemManagementAllowedForSku; + $this->manageStockCondition = $manageStockCondition; + $this->collectionProcessor = $collectionProcessor; + $this->sourceItemCollectionFactory = $sourceItemCollectionFactory; + } + + /** + * @param array $skuArray + * @param int $stockId + * @return array + * @throws InputException + * @throws LocalizedException + */ + public function execute(array $skuArray, int $stockId): array + { + $result = $this->manageStockCondition->execute($skuArray, $stockId); + + $skusToCheck = []; + + foreach ($result as $sku => $value) { + // if value is true, that is a final result; otherwise, proceed with the next checks + if (!$value) { + $skusToCheck[$sku] = null; + } + } + + foreach (array_keys($skusToCheck) as $sku) { + if (!$this->isSourceItemManagementAllowedForSku->execute((string)$sku)) { + // if source item management is not allowed, that is a final result for that sku + $result[$sku] = true; + + unset($skusToCheck[$sku]); + } + } + + $sourceCodes = $this->getSourceCodesAssignedToStock($stockId); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(SourceItemInterface::SKU, array_keys($skusToCheck), 'in') + ->addFilter(SourceItemInterface::SOURCE_CODE, $sourceCodes, 'in') + ->addFilter(SourceItemInterface::STATUS, SourceItemInterface::STATUS_IN_STOCK) + ->create(); + + /** @var Collection $collection */ + $collection = $this->sourceItemCollectionFactory->create(); + $this->collectionProcessor->process($searchCriteria, $collection); + + /** @var SourceItem $item */ + foreach ($collection as $item) { + $result[$item->getSku()] = true; + } + + return $result; + } + + /** + * Provides source codes for certain stock + * + * @param int $stockId + * + * @return array + * @throws InputException + * @throws LocalizedException + */ + protected function getSourceCodesAssignedToStock(int $stockId): array + { + $sources = $this->getSourcesAssignedToStockOrderedByPriority->execute($stockId); + $sourceCodes = []; + + foreach ($sources as $source) { + if ($source->isEnabled()) { + $sourceCodes[] = $source->getSourceCode(); + } + } + + return $sourceCodes; + } +} diff --git a/src/Model/Inventory/AreProductsSalableCondition/IsSetInStockStatusForCompositeProductsCondition.php b/src/Model/Inventory/AreProductsSalableCondition/IsSetInStockStatusForCompositeProductsCondition.php index d2bb9d2..beed97a 100644 --- a/src/Model/Inventory/AreProductsSalableCondition/IsSetInStockStatusForCompositeProductsCondition.php +++ b/src/Model/Inventory/AreProductsSalableCondition/IsSetInStockStatusForCompositeProductsCondition.php @@ -46,7 +46,7 @@ public function execute(array $skuArray, int $stockId): array $skusToCheck = array_flip($skuArray); foreach ($skuArray as $sku) { - if ($this->isSourceItemManagementAllowedForSku->execute($sku)) { + if ($this->isSourceItemManagementAllowedForSku->execute((string)$sku)) { $result[$sku] = true; unset($skusToCheck[$sku]); } diff --git a/src/Model/Inventory/GetStockItemsConfigurations.php b/src/Model/Inventory/GetStockItemsConfigurations.php index 3e9c04f..b32dee0 100644 --- a/src/Model/Inventory/GetStockItemsConfigurations.php +++ b/src/Model/Inventory/GetStockItemsConfigurations.php @@ -1,115 +1,115 @@ - - * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) - * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) - */ -declare(strict_types=1); - -namespace ScandiPWA\Performance\Model\Inventory; - -use Magento\InventoryCatalogApi\Api\DefaultStockProviderInterface; -use Magento\InventoryCatalogApi\Model\GetProductTypesBySkusInterface; -use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForSkuInterface; -use Magento\InventoryConfiguration\Model\StockItemConfigurationFactory; -use ScandiPWA\Performance\Api\Inventory\GetStockItemsConfigurationsInterface; -use ScandiPWA\Performance\Api\Inventory\AreProductsAssignedToStockInterface; - -class GetStockItemsConfigurations implements GetStockItemsConfigurationsInterface -{ - /** - * @var GetLegacyStockItems - */ - protected GetLegacyStockItems $getLegacyStockItems; - - /** - * @var StockItemConfigurationFactory - */ - protected StockItemConfigurationFactory $stockItemConfigurationFactory; - - /** - * @var DefaultStockProviderInterface - */ - protected DefaultStockProviderInterface $defaultStockProvider; - - /** - * @var IsSourceItemManagementAllowedForSkuInterface - */ - protected IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; - - /** - * @var GetProductTypesBySkusInterface - */ - protected GetProductTypesBySkusInterface $getProductTypesBySkus; - - /** - * @var AreProductsAssignedToStockInterface - */ - protected AreProductsAssignedToStockInterface $areProductsAssignedToStock; - - /** - * @param GetLegacyStockItems $getLegacyStockItems - * @param StockItemConfigurationFactory $stockItemConfigurationFactory - * @param AreProductsAssignedToStockInterface $areProductsAssignedToStock - * @param DefaultStockProviderInterface $defaultStockProvider - * @param IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku - * @param GetProductTypesBySkusInterface $getProductTypesBySkus - */ - public function __construct( - GetLegacyStockItems $getLegacyStockItems, - StockItemConfigurationFactory $stockItemConfigurationFactory, - AreProductsAssignedToStockInterface $areProductsAssignedToStock, - DefaultStockProviderInterface $defaultStockProvider, - IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku, - GetProductTypesBySkusInterface $getProductTypesBySkus - ) { - $this->getLegacyStockItems = $getLegacyStockItems; - $this->stockItemConfigurationFactory = $stockItemConfigurationFactory; - $this->defaultStockProvider = $defaultStockProvider; - $this->isSourceItemManagementAllowedForSku = $isSourceItemManagementAllowedForSku; - $this->getProductTypesBySkus = $getProductTypesBySkus; - $this->areProductsAssignedToStock = $areProductsAssignedToStock; - } - - /** - * @inheritDoc - */ - public function execute(array $skuArray, int $stockId): array - { - // this will load at once & cache the product types for all skus, is used in multiple operations later - $this->getProductTypesBySkus->execute($skuArray); - $areProductsAssignedToStock = $this->areProductsAssignedToStock->execute($skuArray, $stockId); - - $result = []; - $skusToCheck = array_flip($skuArray); - - foreach ($skuArray as $sku) { - if ($this->defaultStockProvider->getId() !== $stockId - && true === $this->isSourceItemManagementAllowedForSku->execute($sku) - && false === $areProductsAssignedToStock[$sku]) { - // used instead of SkuIsNotAssignedToStockException in core - $result[$sku] = false; - - unset($skusToCheck[$sku]); - } - } - - $stockItems = $this->getLegacyStockItems->execute(array_keys($skusToCheck)); - - foreach ($stockItems as $sku => $stockItem) { - $result[$sku] = $this->stockItemConfigurationFactory->create( - [ - 'stockItem' => $stockItem - ] - ); - - // logic from LoadIsInStockPlugin - $extensionAttributes = $result[$sku]->getExtensionAttributes(); - $extensionAttributes->setIsInStock((bool)(int)$stockItem->getIsInStock()); - $result[$sku]->setExtensionAttributes($extensionAttributes); - } - - return $result; - } -} + + * @copyright Copyright (c) 2022 Scandiweb, Inc (https://scandiweb.com) + * @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0) + */ +declare(strict_types=1); + +namespace ScandiPWA\Performance\Model\Inventory; + +use Magento\InventoryCatalogApi\Api\DefaultStockProviderInterface; +use Magento\InventoryCatalogApi\Model\GetProductTypesBySkusInterface; +use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForSkuInterface; +use Magento\InventoryConfiguration\Model\StockItemConfigurationFactory; +use ScandiPWA\Performance\Api\Inventory\GetStockItemsConfigurationsInterface; +use ScandiPWA\Performance\Api\Inventory\AreProductsAssignedToStockInterface; + +class GetStockItemsConfigurations implements GetStockItemsConfigurationsInterface +{ + /** + * @var GetLegacyStockItems + */ + protected GetLegacyStockItems $getLegacyStockItems; + + /** + * @var StockItemConfigurationFactory + */ + protected StockItemConfigurationFactory $stockItemConfigurationFactory; + + /** + * @var DefaultStockProviderInterface + */ + protected DefaultStockProviderInterface $defaultStockProvider; + + /** + * @var IsSourceItemManagementAllowedForSkuInterface + */ + protected IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; + + /** + * @var GetProductTypesBySkusInterface + */ + protected GetProductTypesBySkusInterface $getProductTypesBySkus; + + /** + * @var AreProductsAssignedToStockInterface + */ + protected AreProductsAssignedToStockInterface $areProductsAssignedToStock; + + /** + * @param GetLegacyStockItems $getLegacyStockItems + * @param StockItemConfigurationFactory $stockItemConfigurationFactory + * @param AreProductsAssignedToStockInterface $areProductsAssignedToStock + * @param DefaultStockProviderInterface $defaultStockProvider + * @param IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku + * @param GetProductTypesBySkusInterface $getProductTypesBySkus + */ + public function __construct( + GetLegacyStockItems $getLegacyStockItems, + StockItemConfigurationFactory $stockItemConfigurationFactory, + AreProductsAssignedToStockInterface $areProductsAssignedToStock, + DefaultStockProviderInterface $defaultStockProvider, + IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku, + GetProductTypesBySkusInterface $getProductTypesBySkus + ) { + $this->getLegacyStockItems = $getLegacyStockItems; + $this->stockItemConfigurationFactory = $stockItemConfigurationFactory; + $this->defaultStockProvider = $defaultStockProvider; + $this->isSourceItemManagementAllowedForSku = $isSourceItemManagementAllowedForSku; + $this->getProductTypesBySkus = $getProductTypesBySkus; + $this->areProductsAssignedToStock = $areProductsAssignedToStock; + } + + /** + * @inheritDoc + */ + public function execute(array $skuArray, int $stockId): array + { + // this will load at once & cache the product types for all skus, is used in multiple operations later + $this->getProductTypesBySkus->execute($skuArray); + $areProductsAssignedToStock = $this->areProductsAssignedToStock->execute($skuArray, $stockId); + + $result = []; + $skusToCheck = array_flip($skuArray); + + foreach ($skuArray as $sku) { + if ($this->defaultStockProvider->getId() !== $stockId + && true === $this->isSourceItemManagementAllowedForSku->execute((string)$sku) + && false === $areProductsAssignedToStock[$sku]) { + // used instead of SkuIsNotAssignedToStockException in core + $result[$sku] = false; + + unset($skusToCheck[$sku]); + } + } + + $stockItems = $this->getLegacyStockItems->execute(array_keys($skusToCheck)); + + foreach ($stockItems as $sku => $stockItem) { + $result[$sku] = $this->stockItemConfigurationFactory->create( + [ + 'stockItem' => $stockItem + ] + ); + + // logic from LoadIsInStockPlugin + $extensionAttributes = $result[$sku]->getExtensionAttributes(); + $extensionAttributes->setIsInStock((bool)(int)$stockItem->getIsInStock()); + $result[$sku]->setExtensionAttributes($extensionAttributes); + } + + return $result; + } +} diff --git a/src/Model/Inventory/GetStockItemsData.php b/src/Model/Inventory/GetStockItemsData.php index 7e83ebb..bf171d0 100644 --- a/src/Model/Inventory/GetStockItemsData.php +++ b/src/Model/Inventory/GetStockItemsData.php @@ -42,14 +42,14 @@ class GetStockItemsData implements GetStockItemsDataInterface protected GetProductIdsBySkus $getProductIdsBySkus; /** - * @var IsSingleSourceModeInterface|null + * @var IsSingleSourceModeInterface */ - protected ?IsSingleSourceModeInterface $isSingleSourceMode; + protected IsSingleSourceModeInterface $isSingleSourceMode; /** - * @var IsSourceItemManagementAllowedForSkuInterface|null + * @var IsSourceItemManagementAllowedForSkuInterface */ - protected ?IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; + protected IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku; /** * Cached results @@ -70,8 +70,8 @@ public function __construct( StockIndexTableNameResolverInterface $stockIndexTableNameResolver, DefaultStockProviderInterface $defaultStockProvider, GetProductIdsBySkus $getProductIdsBySkus, - ?IsSingleSourceModeInterface $isSingleSourceMode = null, - ?IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku = null + IsSingleSourceModeInterface $isSingleSourceMode, + IsSourceItemManagementAllowedForSkuInterface $isSourceItemManagementAllowedForSku ) { $this->resource = $resource; $this->stockIndexTableNameResolver = $stockIndexTableNameResolver; @@ -233,7 +233,7 @@ protected function getStockItemDataFromStockItemTable(array $skuArray, int $stoc foreach ($skuArray as $sku) { if ($this->defaultStockProvider->getId() !== $stockId || $this->isSingleSourceMode->execute() - || !$this->isSourceItemManagementAllowedForSku->execute($sku) + || !$this->isSourceItemManagementAllowedForSku->execute((string)$sku) ) { $result[$sku] = null; unset($skusToCheck[$sku]);