diff --git a/InventoryBundleProduct/Model/GetBundleProductStockStatus.php b/InventoryBundleProduct/Model/GetBundleProductStockStatus.php index 54052756928..13625412008 100644 --- a/InventoryBundleProduct/Model/GetBundleProductStockStatus.php +++ b/InventoryBundleProduct/Model/GetBundleProductStockStatus.php @@ -7,16 +7,14 @@ namespace Magento\InventoryBundleProduct\Model; +use Magento\InventoryBundleProduct\Model\GetProductSelection; use Magento\Bundle\Api\Data\OptionInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\Exception\LocalizedException; use Magento\InventoryConfigurationApi\Api\GetStockItemConfigurationInterface; use Magento\InventoryConfigurationApi\Exception\SkuIsNotAssignedToStockException; -use Magento\InventorySalesApi\Api\AreProductsSalableForRequestedQtyInterface; -use Magento\InventorySalesApi\Api\Data\IsProductSalableForRequestedQtyRequestInterfaceFactory; -use Magento\InventorySalesApi\Api\Data\IsProductSalableForRequestedQtyResultInterface; +use Magento\InventorySalesApi\Api\IsProductSalableForRequestedQtyInterface; /** * Get bundle product stock status service. @@ -24,41 +22,22 @@ class GetBundleProductStockStatus { /** - * @var GetProductSelection - */ - private $getProductSelection; - - /** - * @var AreProductsSalableForRequestedQtyInterface - */ - private $areProductsSalableForRequestedQty; - - /** - * @var IsProductSalableForRequestedQtyRequestInterfaceFactory - */ - private $isProductSalableForRequestedQtyRequestFactory; - - /** - * @var GetStockItemConfigurationInterface + * Cache for stock, for scenario where same product is in multiple options. + * + * @var array */ - private $getStockItemConfiguration; + protected array $stockCache = []; /** * @param GetProductSelection $getProductSelection - * @param AreProductsSalableForRequestedQtyInterface $areProductsSalableForRequestedQty - * @param IsProductSalableForRequestedQtyRequestInterfaceFactory $isProductSalableForRequestedQtyRequestFactory + * @param IsProductSalableForRequestedQtyInterface $isProductSalableForRequestedQty * @param GetStockItemConfigurationInterface $getStockItemConfiguration */ public function __construct( - GetProductSelection $getProductSelection, - AreProductsSalableForRequestedQtyInterface $areProductsSalableForRequestedQty, - IsProductSalableForRequestedQtyRequestInterfaceFactory $isProductSalableForRequestedQtyRequestFactory, - GetStockItemConfigurationInterface $getStockItemConfiguration + protected GetProductSelection $getProductSelection, + protected IsProductSalableForRequestedQtyInterface $isProductSalableForRequestedQty, + protected GetStockItemConfigurationInterface $getStockItemConfiguration ) { - $this->getProductSelection = $getProductSelection; - $this->areProductsSalableForRequestedQty = $areProductsSalableForRequestedQty; - $this->isProductSalableForRequestedQtyRequestFactory = $isProductSalableForRequestedQtyRequestFactory; - $this->getStockItemConfiguration = $getStockItemConfiguration; } /** @@ -79,16 +58,53 @@ public function execute(ProductInterface $product, array $bundleOptions, int $st if (!$stockItemConfiguration->getExtensionAttributes()->getIsInStock()) { return false; } + + $requiredOptions = []; + $optionalOptions = []; + foreach ($bundleOptions as $option) { + if ($option->getRequired()) { + $requiredOptions[] = $option; + } else { + $optionalOptions[] = $option; + } + } + + if (!empty($requiredOptions)) { + return $this->checkRequiredOptions($product, $requiredOptions, $stockId); + } else { + return $this->checkOptionalOptions($product, $optionalOptions, $stockId); + } + } + + /** + * Check stock for a bundle that has required options. + * + * In this scenario, we need to ensure that all required options have at least one in-stock selection. + * If any required option has no in-stock selections, the bundle is not salable. + * We don't need to consider optional components in this case. + * + * @param ProductInterface $product + * @param OptionInterface[] $bundleOptions + * @param int $stockId + * @return bool + * @throws LocalizedException + * @throws SkuIsNotAssignedToStockException + */ + private function checkRequiredOptions(ProductInterface $product, array $bundleOptions, int $stockId): bool + { $isSalable = false; foreach ($bundleOptions as $option) { $hasSalable = false; - $results = $this->getAreSalableSelections($product, $option, $stockId); - foreach ($results as $result) { - if ($result->isSalable()) { + + $bundleSelections = $this->getProductSelection->execute($product, $option); + foreach ($bundleSelections->getItems() as $result) { + $qty = $this->getRequestedQty($result, $stockId); + if ($this->isSaleableForSkuAndQuantity((string)$result->getSku(), $stockId, $qty)) { $hasSalable = true; break; } } + if ($hasSalable) { $isSalable = true; } @@ -103,49 +119,77 @@ public function execute(ProductInterface $product, array $bundleOptions, int $st } /** - * Get bundle product selection qty. + * Check the stock for a bundle with no required options. * - * @param Product $product + * In this scenario, we only need to ensure that at least one selection from any optional option is in stock. + * If any optional option has at least one in-stock selection, the bundle is salable. + * + * @param ProductInterface $product + * @param OptionInterface[] $bundleOptions * @param int $stockId - * @return float + * @return bool * @throws LocalizedException * @throws SkuIsNotAssignedToStockException */ - private function getRequestedQty(Product $product, int $stockId): float + private function checkOptionalOptions(ProductInterface $product, array $bundleOptions, int $stockId): bool { - if ((int)$product->getSelectionCanChangeQty()) { - $stockItemConfiguration = $this->getStockItemConfiguration->execute((string)$product->getSku(), $stockId); - return $stockItemConfiguration->getMinSaleQty(); + $isSalable = false; + foreach ($bundleOptions as $option) { + $bundleSelections = $this->getProductSelection->execute($product, $option); + foreach ($bundleSelections->getItems() as $result) { + $qty = $this->getRequestedQty($result, $stockId); + if ($this->isSaleableForSkuAndQuantity((string)$result->getSku(), $stockId, $qty)) { + $isSalable = true; + break 2; + } + } } - return (float)$product->getSelectionQty(); + return $isSalable; } /** - * Get are bundle product selections salable. + * Check if a product is saleable for a given SKU and quantity. * - * @param ProductInterface $product - * @param OptionInterface $option + * @param string $sku * @param int $stockId - * @return IsProductSalableForRequestedQtyResultInterface[] + * @param float $qty + * @return bool * @throws LocalizedException * @throws SkuIsNotAssignedToStockException */ - private function getAreSalableSelections(ProductInterface $product, OptionInterface $option, int $stockId): array + private function isSaleableForSkuAndQuantity($sku, int $stockId, float $qty): bool { - $bundleSelections = $this->getProductSelection->execute($product, $option); - $skuRequests = []; - foreach ($bundleSelections->getItems() as $selection) { - if ((int)$selection->getStatus() === Status::STATUS_ENABLED) { - $skuRequests[] = $this->isProductSalableForRequestedQtyRequestFactory->create( - [ - 'sku' => (string)$selection->getSku(), - 'qty' => $this->getRequestedQty($selection, $stockId), - ] - ); - } + $cacheKey = implode('~', [$sku, $stockId, $qty]); + if (isset($this->stockCache[$cacheKey])) { + return $this->stockCache[$cacheKey]; + } + + $result = $this->isProductSalableForRequestedQty->execute( + $sku, + $stockId, + $qty + ); + $this->stockCache[$cacheKey] = $result->isSalable(); + return $result->isSalable(); + } + + /** + * Get bundle product selection qty. + * + * @param Product $product + * @param int $stockId + * @return float + * @throws LocalizedException + * @throws SkuIsNotAssignedToStockException + */ + private function getRequestedQty(Product $product, int $stockId): float + { + if ((int)$product->getSelectionCanChangeQty()) { + $stockItemConfiguration = $this->getStockItemConfiguration->execute((string)$product->getSku(), $stockId); + return $stockItemConfiguration->getMinSaleQty(); } - return $this->areProductsSalableForRequestedQty->execute($skuRequests, $stockId); + return (float) $product->getSelectionQty(); } }