From 6057db184a9f13504b2845e6fc0ff3321ef08ef4 Mon Sep 17 00:00:00 2001 From: Abhishek Pathak Date: Mon, 11 Dec 2023 19:06:06 +0530 Subject: [PATCH 01/14] LYNX-310_LYNX-302:Unavailable stock status should be returned if the product is in stock but does not have the quantity required (qty added to cart) --- app/code/Magento/Quote/Model/Quote/Item.php | 1 + .../Model/Resolver/CheckAvailability.php | 123 ++++++++++++++++++ .../Magento/QuoteGraphQl/etc/schema.graphqls | 1 + 3 files changed, 125 insertions(+) create mode 100644 app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php diff --git a/app/code/Magento/Quote/Model/Quote/Item.php b/app/code/Magento/Quote/Model/Quote/Item.php index 580aafb15aed4..ab3eb8774ed62 100644 --- a/app/code/Magento/Quote/Model/Quote/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Item.php @@ -348,6 +348,7 @@ public function addQty($qty) if (!$this->getParentItem() || !$this->getId()) { $qty = $this->_prepareQty($qty); $this->setQtyToAdd($qty); + $this->setPreviousQty($this->getQty()); $this->setQty($this->getQty() + $qty); } return $this; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php new file mode 100644 index 0000000000000..fcf1d737637e7 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php @@ -0,0 +1,123 @@ +stockStatusRepository = $stockStatusRepository; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Item $cartItem */ + $cartItem = $value['model']; + + return $this->checkProductQtyStatus($cartItem) ? "available" : "unavailable"; + } + + /** + * @param Item $cartItem + * @return bool + */ + private function checkProductQtyStatus($cartItem):bool + { + $requestedQty = 0; + $previousQty = 0; + + if ($cartItem->getProductType() == self::PRODUCT_TYPE) { + $qtyOptions = $cartItem->getQtyOptions(); + $requestedQty = $cartItem->getQtyToAdd() ? $cartItem->getQtyToAdd() : $cartItem->getQty(); + $previousQty = $cartItem->getPreviousQty() ? $cartItem->getPreviousQty() : 0; + $totalReqQty = $previousQty + $requestedQty; + foreach($qtyOptions as $qtyOption) + { + $productId = (int) $qtyOption->getProductId(); + $requiredItemQty = (float) $qtyOption->getValue(); + if ($totalReqQty) { + $requiredItemQty = $requiredItemQty * $totalReqQty; + } + + if ($this->getProductStockStatus($productId, $requiredItemQty)) { + return false; + } + } + } else { + foreach ($cartItem->getQuote()->getItems() as $item) { + + if($item->getItemId() == $cartItem->getItemId() && $item->getQtyToAdd()) { + $requestedQty = (float)$item->getQtyToAdd(); + $previousQty = $item->getPreviousQty() ? (float)$item->getPreviousQty() : 0; + } + } + $requiredItemQty = $requestedQty + $previousQty; + $productId = (int) $cartItem->getProduct()->getId(); + if ($this->getProductStockStatus($productId, $requiredItemQty)) { + return false; + } + } + return true; + } + + /** + * @param int $productId + * @param float $requiredQuantity + * @return bool + */ + private function getProductStockStatus(int $productId, float $requiredQuantity): bool + { + $stock = $this->stockStatusRepository->get($productId); + return ($stock->getQty() < $requiredQuantity) ? true : false; + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 89473e1554e11..9581fc6da43c3 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -360,6 +360,7 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ id: String! @deprecated(reason: "Use `uid` instead.") uid: ID! @doc(description: "The unique ID for a `CartItemInterface` object.") quantity: Float! @doc(description: "The quantity of this item in the cart.") + status: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CheckAvailability") @doc(description: "If qty is more than stock display status as unavailable else available.") prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") @doc(description: "Contains details about the price of the item, including taxes and discounts.") product: ProductInterface! @doc(description: "Details about an item in the cart.") errors: [CartItemError!] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemErrors") @doc(description: "An array of errors encountered while loading the cart item") From 4784fc2bae4a5973aa1efacafc72085feb3b343f Mon Sep 17 00:00:00 2001 From: Sumesh P Date: Tue, 12 Dec 2023 17:43:46 +0530 Subject: [PATCH 02/14] api-functional test coverage --- .../Catalog/Test/Fixture/ProductStock.php | 52 ++++ .../Model/Resolver/CheckAvailability.php | 18 +- .../Magento/GraphQl/Quote/StockStatusTest.php | 293 ++++++++++++++++++ 3 files changed, 354 insertions(+), 9 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Fixture/ProductStock.php create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php diff --git a/app/code/Magento/Catalog/Test/Fixture/ProductStock.php b/app/code/Magento/Catalog/Test/Fixture/ProductStock.php new file mode 100644 index 0000000000000..ef864bc32f38f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Fixture/ProductStock.php @@ -0,0 +1,52 @@ + null, + 'prod_qty' => 1 + ]; + + /** + * @var DataObjectFactory + */ + protected DataObjectFactory $dataObjectFactory; + + /** + * @var StockRegistryInterface + */ + protected StockRegistryInterface $stockRegistry; + + /** + * @param DataObjectFactory $dataObjectFactory + * @param StockRegistryInterface $stockRegistry + */ + public function __construct( + DataObjectFactory $dataObjectFactory, + StockRegistryInterface $stockRegistry + ) { + $this->dataObjectFactory = $dataObjectFactory; + $this->stockRegistry = $stockRegistry; + } + + /** + * {@inheritdoc} + * @param array $data Parameters. Same format as ReduceProductStock::DEFAULT_DATA + */ + public function apply(array $data = []): ?DataObject + { + $stockItem = $this->stockRegistry->getStockItem($data['prod_id']); + $stockItem->setData('is_in_stock', 1); + $stockItem->setData('qty', 90); + $stockItem->setData('manage_stock', 1); + $stockItem->save(); + + return $this->dataObjectFactory->create(['data' => [$data]]); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php index fcf1d737637e7..a61bf1ee0fcf7 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php @@ -34,7 +34,7 @@ class CheckAvailability implements ResolverInterface /** * Product type code */ - private const PRODUCT_TYPE = "bundle"; + private const PRODUCT_TYPE_BUNDLE = "bundle"; /** * @var StockStatusRepositoryInterface @@ -76,13 +76,13 @@ private function checkProductQtyStatus($cartItem):bool $requestedQty = 0; $previousQty = 0; - if ($cartItem->getProductType() == self::PRODUCT_TYPE) { + if ($cartItem->getProductType() == self::PRODUCT_TYPE_BUNDLE) { $qtyOptions = $cartItem->getQtyOptions(); - $requestedQty = $cartItem->getQtyToAdd() ? $cartItem->getQtyToAdd() : $cartItem->getQty(); - $previousQty = $cartItem->getPreviousQty() ? $cartItem->getPreviousQty() : 0; + $requestedQty = $cartItem->getQtyToAdd() ?? $cartItem->getQty(); + $previousQty = $cartItem->getPreviousQty() ?? 0; $totalReqQty = $previousQty + $requestedQty; - foreach($qtyOptions as $qtyOption) - { + + foreach($qtyOptions as $qtyOption) { $productId = (int) $qtyOption->getProductId(); $requiredItemQty = (float) $qtyOption->getValue(); if ($totalReqQty) { @@ -96,9 +96,9 @@ private function checkProductQtyStatus($cartItem):bool } else { foreach ($cartItem->getQuote()->getItems() as $item) { - if($item->getItemId() == $cartItem->getItemId() && $item->getQtyToAdd()) { - $requestedQty = (float)$item->getQtyToAdd(); - $previousQty = $item->getPreviousQty() ? (float)$item->getPreviousQty() : 0; + if ($item->getItemId() == $cartItem->getItemId()) { + $requestedQty = $item->getQtyToAdd() ?? $item->getQty(); + $previousQty = $item->getPreviousQty() ?? 0; } } $requiredItemQty = $requestedQty + $previousQty; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php new file mode 100644 index 0000000000000..f6d40c0b28b24 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php @@ -0,0 +1,293 @@ +objectManager = Bootstrap::getObjectManager(); + $this->fixtures = DataFixtureStorageManager::getStorage(); + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 100]), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') + ] + public function testStockStatusUnavailableSimpleProduct(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + + self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/status')); + } + + #[ + DataFixture(ProductFixture::class, ['sku' => 'spl-prod', 'price' => 100.00], as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 100]), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') + ] + public function testStockStatusUnavailableAddSimpleProduct(): void + { + $sku = 'spl-prod'; + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $query = $this->mutationAddSimpleProduct($maskedQuoteId, $sku, 100); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + + self::assertEquals('unavailable', $responseDataObject->getData('addProductsToCart/cart/items/0/status')); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product.sku$', 'price' => 100, 'price_type' => 0], as:'link'), + DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', + 'required' => 1,'product_links' => ['$link$']], 'option'), + DataFixture( + BundleProductFixture::class, + ['price' => 90, '_options' => ['$option$']], + as:'bundleProduct' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundleProduct.id$', + 'selections' => [['$product.id$']], + 'qty' => 100 + ], + ), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') + ] + public function testStockStatusUnavailableBundleProduct(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + + self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/status')); + } + + #[ + DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$product.sku$', 'price' => 100, 'price_type' => 0], as:'link'), + DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', + 'required' => 1,'product_links' => ['$link$']], 'option'), + DataFixture( + BundleProductFixture::class, + ['sku' => 'bundle-prod1', 'price' => 90, '_options' => ['$option$']], + as:'bundleProduct' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundleProduct.id$', + 'selections' => [['$product.id$']], + 'qty' => 100 + ], + ), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') + ] + public function testStockStatusUnavailableAddBundleProduct(): void + { + $sku = 'bundle-prod1'; + $product = $this->productRepository->get($sku); + + /** @var $typeInstance \Magento\Bundle\Model\Product\Type */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var $option \Magento\Bundle\Model\Option */ + $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); + /** @var \Magento\Catalog\Model\Product $selection */ + $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); + $optionId = $option->getId(); + $selectionId = $selection->getSelectionId(); + + $bundleOptionIdV2 = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, 1); + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + + $query = $this->mutationAddBundleProduct($maskedQuoteId, $sku, $bundleOptionIdV2, 100); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + + self::assertEquals('unavailable', $responseDataObject->getData('addProductsToCart/cart/items/0/status')); + } + + #[ + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(AttributeFixture::class, as: 'attribute'), + DataFixture( + ConfigurableProductFixture::class, + ['_options' => ['$attribute$'], '_links' => ['$product$']], + 'configurable_product' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture( + AddConfigurableProductToCartFixture::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$configurable_product.id$', + 'child_product_id' => '$product.id$', + 'qty' => 100 + ], + ), + DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 90], 'prodStock') + ] + public function testStockStatusUnavailableConfigurableProduct(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + + self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/status')); + } + + /** + * @param string $cartId + * @return string + */ + private function getQuery(string $cartId): string + { + return << Date: Wed, 13 Dec 2023 16:16:41 +0530 Subject: [PATCH 03/14] Check stock status for bundle product --- .../Model/Resolver/CheckAvailability.php | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php index a61bf1ee0fcf7..91a19b91a0de3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php @@ -41,7 +41,6 @@ class CheckAvailability implements ResolverInterface */ private $stockStatusRepository; - /** * CheckAvailability constructor * @@ -68,6 +67,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } /** + * Check item status available or unavailable + * * @param Item $cartItem * @return bool */ @@ -76,31 +77,31 @@ private function checkProductQtyStatus($cartItem):bool $requestedQty = 0; $previousQty = 0; + if (!$cartItem->getQuote()->getItems()) { + return false; + } + + foreach ($cartItem->getQuote()->getItems() as $item) { + if ($item->getItemId() == $cartItem->getItemId()) { + $requestedQty = $item->getQtyToAdd() ?? $item->getQty(); + $previousQty = $item->getPreviousQty() ?? 0; + } + } + if ($cartItem->getProductType() == self::PRODUCT_TYPE_BUNDLE) { $qtyOptions = $cartItem->getQtyOptions(); - $requestedQty = $cartItem->getQtyToAdd() ?? $cartItem->getQty(); - $previousQty = $cartItem->getPreviousQty() ?? 0; - $totalReqQty = $previousQty + $requestedQty; - - foreach($qtyOptions as $qtyOption) { + $totalRequestedQty = $previousQty + $requestedQty; + foreach ($qtyOptions as $qtyOption) { $productId = (int) $qtyOption->getProductId(); $requiredItemQty = (float) $qtyOption->getValue(); - if ($totalReqQty) { - $requiredItemQty = $requiredItemQty * $totalReqQty; + if ($totalRequestedQty) { + $requiredItemQty = $requiredItemQty * $totalRequestedQty; } - if ($this->getProductStockStatus($productId, $requiredItemQty)) { return false; } } } else { - foreach ($cartItem->getQuote()->getItems() as $item) { - - if ($item->getItemId() == $cartItem->getItemId()) { - $requestedQty = $item->getQtyToAdd() ?? $item->getQty(); - $previousQty = $item->getPreviousQty() ?? 0; - } - } $requiredItemQty = $requestedQty + $previousQty; $productId = (int) $cartItem->getProduct()->getId(); if ($this->getProductStockStatus($productId, $requiredItemQty)) { @@ -111,6 +112,8 @@ private function checkProductQtyStatus($cartItem):bool } /** + * Get product qty + * * @param int $productId * @param float $requiredQuantity * @return bool From 95861f425a0867c360efc6192d3cf36f2621f86e Mon Sep 17 00:00:00 2001 From: Sumesh P Date: Wed, 13 Dec 2023 16:23:29 +0530 Subject: [PATCH 04/14] api-functional test case --- .../Magento/GraphQl/Quote/StockStatusTest.php | 76 ++++++++++++++++--- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php index f6d40c0b28b24..29f7f54d1a2c3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php @@ -60,6 +60,10 @@ class StockStatusTest extends GraphQlAbstract */ private $productRepository; + private const SKU = 'simple_10'; + private const PARENT_SKU_BUNDLE = 'parent_bundle'; + private const PARENT_SKU_CONFIGURABLE = 'parent_configurable'; + /** * @inheritDoc */ @@ -89,16 +93,15 @@ public function testStockStatusUnavailableSimpleProduct(): void } #[ - DataFixture(ProductFixture::class, ['sku' => 'spl-prod', 'price' => 100.00], as: 'product'), + DataFixture(ProductFixture::class, ['sku' => self::SKU, 'price' => 100.00], as: 'product'), DataFixture(GuestCartFixture::class, as: 'cart'), DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 100]), DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] public function testStockStatusUnavailableAddSimpleProduct(): void { - $sku = 'spl-prod'; $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->mutationAddSimpleProduct($maskedQuoteId, $sku, 100); + $query = $this->mutationAddSimpleProduct($maskedQuoteId, self::SKU, 1); $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); @@ -145,7 +148,7 @@ public function testStockStatusUnavailableBundleProduct(): void 'required' => 1,'product_links' => ['$link$']], 'option'), DataFixture( BundleProductFixture::class, - ['sku' => 'bundle-prod1', 'price' => 90, '_options' => ['$option$']], + ['sku' => self::PARENT_SKU_BUNDLE, 'price' => 90, '_options' => ['$option$']], as:'bundleProduct' ), DataFixture(GuestCartFixture::class, as: 'cart'), @@ -162,8 +165,7 @@ public function testStockStatusUnavailableBundleProduct(): void ] public function testStockStatusUnavailableAddBundleProduct(): void { - $sku = 'bundle-prod1'; - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get(self::PARENT_SKU_BUNDLE); /** @var $typeInstance \Magento\Bundle\Model\Product\Type */ $typeInstance = $product->getTypeInstance(); @@ -178,7 +180,7 @@ public function testStockStatusUnavailableAddBundleProduct(): void $bundleOptionIdV2 = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, 1); $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->mutationAddBundleProduct($maskedQuoteId, $sku, $bundleOptionIdV2, 100); + $query = $this->mutationAddBundleProduct($maskedQuoteId, self::PARENT_SKU_BUNDLE, $bundleOptionIdV2); $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); @@ -216,6 +218,36 @@ public function testStockStatusUnavailableConfigurableProduct(): void self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/status')); } + #[ + DataFixture(ProductFixture::class, ['sku' => self::SKU], as: 'product'), + DataFixture(AttributeFixture::class, as: 'attribute'), + DataFixture( + ConfigurableProductFixture::class, + ['sku' => self::PARENT_SKU_CONFIGURABLE, '_options' => ['$attribute$'], '_links' => ['$product$']], + 'configurable_product' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture( + AddConfigurableProductToCartFixture::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$configurable_product.id$', + 'child_product_id' => '$product.id$', + 'qty' => 100 + ], + ) + ] + public function testStockStatusUnavailableAddConfigurableProduct(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $query = $this->mutationAddConfigurableProduct($maskedQuoteId, self::SKU, self::PARENT_SKU_CONFIGURABLE); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + + self::assertEquals('unavailable', $responseDataObject->getData('addProductsToCart/cart/items/0/status')); + } + /** * @param string $cartId * @return string @@ -234,7 +266,7 @@ private function getQuery(string $cartId): string QUERY; } - private function mutationAddSimpleProduct(string $cartId, string $sku, int $qty): string + private function mutationAddSimpleProduct(string $cartId, string $sku, int $qty = 1): string { return << Date: Wed, 13 Dec 2023 16:25:53 +0530 Subject: [PATCH 05/14] api-functional test case --- .../testsuite/Magento/GraphQl/Quote/StockStatusTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php index 29f7f54d1a2c3..15189cd23c839 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php @@ -26,14 +26,11 @@ use Magento\ConfigurableProduct\Test\Fixture\AddProductToCart as AddConfigurableProductToCartFixture; use Magento\ConfigurableProduct\Test\Fixture\Attribute as AttributeFixture; use Magento\ConfigurableProduct\Test\Fixture\Product as ConfigurableProductFixture; -use Magento\Customer\Test\Fixture\Customer; use Magento\Framework\DataObject; use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Test\Fixture\AddProductToCart; -use Magento\Quote\Test\Fixture\CustomerCart; use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; use Magento\Quote\Test\Fixture\QuoteIdMask as QuoteMaskFixture; -use Magento\SalesRule\Test\Fixture\ProductCondition as ProductConditionFixture; use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\Fixture\DataFixtureStorage; use Magento\TestFramework\Fixture\DataFixtureStorageManager; From 1e6c822076ec8c636d605f2f9b2ae2f4d953e638 Mon Sep 17 00:00:00 2001 From: Sumesh P Date: Wed, 13 Dec 2023 19:36:30 +0530 Subject: [PATCH 06/14] Change function name --- .../Model/Resolver/CheckAvailability.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php index 91a19b91a0de3..5cd0db0cc6e26 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php @@ -22,7 +22,6 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogInventory\Api\Data\StockStatusInterface; use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; use Magento\Quote\Model\Quote\Item; @@ -39,7 +38,7 @@ class CheckAvailability implements ResolverInterface /** * @var StockStatusRepositoryInterface */ - private $stockStatusRepository; + private StockStatusRepositoryInterface $stockStatusRepository; /** * CheckAvailability constructor @@ -72,7 +71,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value * @param Item $cartItem * @return bool */ - private function checkProductQtyStatus($cartItem):bool + private function checkProductQtyStatus(Item $cartItem): bool { $requestedQty = 0; $previousQty = 0; @@ -97,30 +96,28 @@ private function checkProductQtyStatus($cartItem):bool if ($totalRequestedQty) { $requiredItemQty = $requiredItemQty * $totalRequestedQty; } - if ($this->getProductStockStatus($productId, $requiredItemQty)) { + if (!$this->isRequiredStockAvailable($productId, $requiredItemQty)) { return false; } } } else { $requiredItemQty = $requestedQty + $previousQty; $productId = (int) $cartItem->getProduct()->getId(); - if ($this->getProductStockStatus($productId, $requiredItemQty)) { - return false; - } + return $this->isRequiredStockAvailable($productId, $requiredItemQty); } return true; } /** - * Get product qty + * Check if is required product available in stock * * @param int $productId * @param float $requiredQuantity * @return bool */ - private function getProductStockStatus(int $productId, float $requiredQuantity): bool + private function isRequiredStockAvailable(int $productId, float $requiredQuantity): bool { $stock = $this->stockStatusRepository->get($productId); - return ($stock->getQty() < $requiredQuantity) ? true : false; + return ($stock->getQty() >= $requiredQuantity); } } From 4e89e4889cba20a8568113f9438107014db468a6 Mon Sep 17 00:00:00 2001 From: Abhishek Pathak Date: Wed, 13 Dec 2023 21:18:26 +0530 Subject: [PATCH 07/14] Create new ProductStock class to check availability of product --- .../ProductStock.php} | 44 ++++-------- .../CheckProductStockAvailability.php | 62 +++++++++++++++++ .../Magento/QuoteGraphQl/etc/schema.graphqls | 2 +- .../Magento/GraphQl/Quote/StockStatusTest.php | 69 ++++++++++++++----- 4 files changed, 126 insertions(+), 51 deletions(-) rename app/code/Magento/QuoteGraphQl/Model/{Resolver/CheckAvailability.php => CartItem/ProductStock.php} (66%) create mode 100644 app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php similarity index 66% rename from app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php rename to app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php index 5cd0db0cc6e26..49ec2271f4af8 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckAvailability.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php @@ -16,19 +16,15 @@ */ declare(strict_types=1); -namespace Magento\QuoteGraphQl\Model\Resolver; +namespace Magento\QuoteGraphQl\Model\CartItem; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; use Magento\Quote\Model\Quote\Item; /** - * @inheritdoc + * Product Stock class to check availability of product */ -class CheckAvailability implements ResolverInterface +class ProductStock { /** * Product type code @@ -38,10 +34,10 @@ class CheckAvailability implements ResolverInterface /** * @var StockStatusRepositoryInterface */ - private StockStatusRepositoryInterface $stockStatusRepository; + private $stockStatusRepository; /** - * CheckAvailability constructor + * ProductStock constructor * * @param StockStatusRepositoryInterface $stockStatusRepository */ @@ -51,35 +47,17 @@ public function __construct( $this->stockStatusRepository = $stockStatusRepository; } - /** - * @inheritdoc - */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { - if (!isset($value['model'])) { - throw new LocalizedException(__('"model" value should be specified')); - } - /** @var Item $cartItem */ - $cartItem = $value['model']; - - return $this->checkProductQtyStatus($cartItem) ? "available" : "unavailable"; - } - /** * Check item status available or unavailable * * @param Item $cartItem * @return bool */ - private function checkProductQtyStatus(Item $cartItem): bool + public function getProductAvailability($cartItem):bool { $requestedQty = 0; $previousQty = 0; - if (!$cartItem->getQuote()->getItems()) { - return false; - } - foreach ($cartItem->getQuote()->getItems() as $item) { if ($item->getItemId() == $cartItem->getItemId()) { $requestedQty = $item->getQtyToAdd() ?? $item->getQty(); @@ -96,14 +74,16 @@ private function checkProductQtyStatus(Item $cartItem): bool if ($totalRequestedQty) { $requiredItemQty = $requiredItemQty * $totalRequestedQty; } - if (!$this->isRequiredStockAvailable($productId, $requiredItemQty)) { + if ($this->isStockAvailable($productId, $requiredItemQty)) { return false; } } } else { $requiredItemQty = $requestedQty + $previousQty; $productId = (int) $cartItem->getProduct()->getId(); - return $this->isRequiredStockAvailable($productId, $requiredItemQty); + if ($this->isStockAvailable($productId, $requiredItemQty)) { + return false; + } } return true; } @@ -115,9 +95,9 @@ private function checkProductQtyStatus(Item $cartItem): bool * @param float $requiredQuantity * @return bool */ - private function isRequiredStockAvailable(int $productId, float $requiredQuantity): bool + private function isStockAvailable(int $productId, float $requiredQuantity): bool { $stock = $this->stockStatusRepository->get($productId); - return ($stock->getQty() >= $requiredQuantity); + return ($stock->getQty() < $requiredQuantity) ? true : false; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php new file mode 100644 index 0000000000000..ef3f5e73fd056 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php @@ -0,0 +1,62 @@ +productStock = $productStock; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Item $cartItem */ + $cartItem = $value['model']; + + return $this->productStock->getProductAvailability($cartItem) ? "available" : "unavailable"; + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 9581fc6da43c3..4ec48d3414cef 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -360,7 +360,7 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ id: String! @deprecated(reason: "Use `uid` instead.") uid: ID! @doc(description: "The unique ID for a `CartItemInterface` object.") quantity: Float! @doc(description: "The quantity of this item in the cart.") - status: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CheckAvailability") @doc(description: "If qty is more than stock display status as unavailable else available.") + stock_availability: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CheckProductStockAvailability") @doc(description: "If qty is more than stock display status as unavailable else available.") prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") @doc(description: "Contains details about the price of the item, including taxes and discounts.") product: ProductInterface! @doc(description: "Details about an item in the cart.") errors: [CartItemError!] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemErrors") @doc(description: "An array of errors encountered while loading the cart item") diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php index 15189cd23c839..a88f0bdf8e841 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php @@ -86,7 +86,7 @@ public function testStockStatusUnavailableSimpleProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/status')); + self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/stock_availability')); } #[ @@ -102,12 +102,21 @@ public function testStockStatusUnavailableAddSimpleProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals('unavailable', $responseDataObject->getData('addProductsToCart/cart/items/0/status')); + self::assertEquals( + 'unavailable', + $responseDataObject->getData('addProductsToCart/cart/items/0/stock_availability') + ); } #[ DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), - DataFixture(BundleSelectionFixture::class, ['sku' => '$product.sku$', 'price' => 100, 'price_type' => 0], as:'link'), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product.sku$', 'price' => 100, 'price_type' => 0 + ], + as:'link' + ), DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', 'required' => 1,'product_links' => ['$link$']], 'option'), DataFixture( @@ -135,12 +144,21 @@ public function testStockStatusUnavailableBundleProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/status')); + self::assertEquals( + 'unavailable', + $responseDataObject->getData('cart/items/0/stock_availability') + ); } #[ DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), - DataFixture(BundleSelectionFixture::class, ['sku' => '$product.sku$', 'price' => 100, 'price_type' => 0], as:'link'), + DataFixture( + BundleSelectionFixture::class, + [ + 'sku' => '$product.sku$', 'price' => 100, 'price_type' => 0 + ], + as:'link' + ), DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', 'required' => 1,'product_links' => ['$link$']], 'option'), DataFixture( @@ -181,7 +199,10 @@ public function testStockStatusUnavailableAddBundleProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals('unavailable', $responseDataObject->getData('addProductsToCart/cart/items/0/status')); + self::assertEquals( + 'unavailable', + $responseDataObject->getData('addProductsToCart/cart/items/0/stock_availability') + ); } #[ @@ -212,7 +233,10 @@ public function testStockStatusUnavailableConfigurableProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/status')); + self::assertEquals( + 'unavailable', + $responseDataObject->getData('cart/items/0/stock_availability') + ); } #[ @@ -242,7 +266,10 @@ public function testStockStatusUnavailableAddConfigurableProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals('unavailable', $responseDataObject->getData('addProductsToCart/cart/items/0/status')); + self::assertEquals( + 'unavailable', + $responseDataObject->getData('addProductsToCart/cart/items/0/stock_availability') + ); } /** @@ -255,7 +282,7 @@ private function getQuery(string $cartId): string { cart(cart_id:"{$cartId}"){ items{ - status + stock_availability } } } @@ -277,7 +304,7 @@ private function mutationAddSimpleProduct(string $cartId, string $sku, int $qty ) { cart { items { - status + stock_availability } } } @@ -285,8 +312,12 @@ private function mutationAddSimpleProduct(string $cartId, string $sku, int $qty QUERY; } - private function mutationAddBundleProduct(string $cartId, string $sku, string $bundleOptionIdV2, int $qty = 1): string - { + private function mutationAddBundleProduct( + string $cartId, + string $sku, + string $bundleOptionIdV2, + int $qty = 1 + ): string { return << Date: Thu, 14 Dec 2023 07:06:55 +0530 Subject: [PATCH 08/14] Update product stock fixture --- .../Catalog/Test/Fixture/ProductStock.php | 29 +++++++++++++++++-- .../Model/CartItem/ProductStock.php | 8 ++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Fixture/ProductStock.php b/app/code/Magento/Catalog/Test/Fixture/ProductStock.php index ef864bc32f38f..ee3dde3ab4d87 100644 --- a/app/code/Magento/Catalog/Test/Fixture/ProductStock.php +++ b/app/code/Magento/Catalog/Test/Fixture/ProductStock.php @@ -1,9 +1,25 @@ dataObjectFactory = $dataObjectFactory; $this->stockRegistry = $stockRegistry; + $this->dataMerger = $dataMerger; } /** * {@inheritdoc} - * @param array $data Parameters. Same format as ReduceProductStock::DEFAULT_DATA + * @param array $data Parameters. Same format as ProductStock::DEFAULT_DATA */ public function apply(array $data = []): ?DataObject { + $data = $this->dataMerger->merge(self::DEFAULT_DATA, $data); $stockItem = $this->stockRegistry->getStockItem($data['prod_id']); $stockItem->setData('is_in_stock', 1); $stockItem->setData('qty', 90); diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php index 49ec2271f4af8..1f47f1224a473 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php @@ -53,7 +53,7 @@ public function __construct( * @param Item $cartItem * @return bool */ - public function getProductAvailability($cartItem):bool + public function getProductAvailability($cartItem): bool { $requestedQty = 0; $previousQty = 0; @@ -74,14 +74,14 @@ public function getProductAvailability($cartItem):bool if ($totalRequestedQty) { $requiredItemQty = $requiredItemQty * $totalRequestedQty; } - if ($this->isStockAvailable($productId, $requiredItemQty)) { + if (!$this->isStockAvailable($productId, $requiredItemQty)) { return false; } } } else { $requiredItemQty = $requestedQty + $previousQty; $productId = (int) $cartItem->getProduct()->getId(); - if ($this->isStockAvailable($productId, $requiredItemQty)) { + if (!$this->isStockAvailable($productId, $requiredItemQty)) { return false; } } @@ -98,6 +98,6 @@ public function getProductAvailability($cartItem):bool private function isStockAvailable(int $productId, float $requiredQuantity): bool { $stock = $this->stockStatusRepository->get($productId); - return ($stock->getQty() < $requiredQuantity) ? true : false; + return $stock->getQty() >= $requiredQuantity; } } From 4639f5c3d35341bdb85635b2fb83656498a2f0a0 Mon Sep 17 00:00:00 2001 From: Sumesh P Date: Thu, 14 Dec 2023 08:42:08 +0530 Subject: [PATCH 09/14] Updated api functional test file name --- .../Quote/{StockStatusTest.php => StockAvailabilityTest.php} | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/{StockStatusTest.php => StockAvailabilityTest.php} (99%) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php similarity index 99% rename from dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php rename to dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php index a88f0bdf8e841..d7d2f8c393098 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockStatusTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php @@ -40,7 +40,7 @@ /** * Test discount totals calculation model */ -class StockStatusTest extends GraphQlAbstract +class StockAvailabilityTest extends GraphQlAbstract { /** * @var ObjectManagerInterface @@ -379,3 +379,5 @@ private function generateBundleOptionIdV2(int $optionId, int $selectionId, int $ return base64_encode("bundle/$optionId/$selectionId/$quantity"); } } + + From 643b8f0dcd86133147f3f44931bbfb4c2713df2a Mon Sep 17 00:00:00 2001 From: Sumesh P Date: Thu, 14 Dec 2023 11:47:00 +0530 Subject: [PATCH 10/14] static test fix --- .../testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php index d7d2f8c393098..544f91b92cf70 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php @@ -379,5 +379,3 @@ private function generateBundleOptionIdV2(int $optionId, int $selectionId, int $ return base64_encode("bundle/$optionId/$selectionId/$quantity"); } } - - From 2afe92f41390b5adfeb4661b3833ada531f56cca Mon Sep 17 00:00:00 2001 From: Abhishek Pathak Date: Thu, 14 Dec 2023 14:17:58 +0530 Subject: [PATCH 11/14] Fix static test --- app/code/Magento/Quote/Model/Quote/Item.php | 4 +++- app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php | 2 +- .../Model/Resolver/CheckProductStockAvailability.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Quote/Model/Quote/Item.php b/app/code/Magento/Quote/Model/Quote/Item.php index ab3eb8774ed62..d05085d1d3cb4 100644 --- a/app/code/Magento/Quote/Model/Quote/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Item.php @@ -139,7 +139,7 @@ class Item extends \Magento\Quote\Model\Quote\Item\AbstractItem implements \Mage protected $_optionsByCode = []; /** - * Not Represent options + * Not Represent option * * @var array */ @@ -148,6 +148,7 @@ class Item extends \Magento\Quote\Model\Quote\Item\AbstractItem implements \Mage /** * Flag stating that options were successfully saved * + * @var bool */ protected $_flagOptionsSaved; @@ -176,6 +177,7 @@ class Item extends \Magento\Quote\Model\Quote\Item\AbstractItem implements \Mage /** * @var \Magento\CatalogInventory\Api\StockRegistryInterface * @deprecated 101.0.0 + * @see nothing */ protected $stockRegistry; diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php index 1f47f1224a473..e21bfe8b6b69f 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php @@ -53,7 +53,7 @@ public function __construct( * @param Item $cartItem * @return bool */ - public function getProductAvailability($cartItem): bool + public function isProductAvailable($cartItem): bool { $requestedQty = 0; $previousQty = 0; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php index ef3f5e73fd056..31572d28d1307 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php @@ -57,6 +57,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var Item $cartItem */ $cartItem = $value['model']; - return $this->productStock->getProductAvailability($cartItem) ? "available" : "unavailable"; + return $this->productStock->isProductAvailable($cartItem) ? "available" : "unavailable"; } } From a8eec060a1f1b852c2c0a5c09be29fa981f3bfca Mon Sep 17 00:00:00 2001 From: Abhishek Pathak Date: Thu, 14 Dec 2023 17:16:27 +0530 Subject: [PATCH 12/14] Rename stock_availability field to is_available and use property promotion --- .../Model/CartItem/ProductStock.php | 8 +---- .../CheckProductStockAvailability.php | 10 ++----- .../Magento/QuoteGraphQl/etc/schema.graphqls | 2 +- .../GraphQl/Quote/StockAvailabilityTest.php | 30 +++++++++---------- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php index e21bfe8b6b69f..56696cf7d34a3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php @@ -31,20 +31,14 @@ class ProductStock */ private const PRODUCT_TYPE_BUNDLE = "bundle"; - /** - * @var StockStatusRepositoryInterface - */ - private $stockStatusRepository; - /** * ProductStock constructor * * @param StockStatusRepositoryInterface $stockStatusRepository */ public function __construct( - StockStatusRepositoryInterface $stockStatusRepository + private StockStatusRepositoryInterface $stockStatusRepository ) { - $this->stockStatusRepository = $stockStatusRepository; } /** diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php index 31572d28d1307..ed3ef8247390c 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php @@ -30,20 +30,14 @@ */ class CheckProductStockAvailability implements ResolverInterface { - /** - * @var ProductStock - */ - private ProductStock $productStock; - /** * CheckProductStockAvailability constructor * * @param ProductStock $productStock */ public function __construct( - ProductStock $productStock + private ProductStock $productStock ) { - $this->productStock = $productStock; } /** @@ -57,6 +51,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var Item $cartItem */ $cartItem = $value['model']; - return $this->productStock->isProductAvailable($cartItem) ? "available" : "unavailable"; + return $this->productStock->isProductAvailable($cartItem) ? true : false; } } diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 4ec48d3414cef..bf77d2e6e07b5 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -360,7 +360,7 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ id: String! @deprecated(reason: "Use `uid` instead.") uid: ID! @doc(description: "The unique ID for a `CartItemInterface` object.") quantity: Float! @doc(description: "The quantity of this item in the cart.") - stock_availability: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CheckProductStockAvailability") @doc(description: "If qty is more than stock display status as unavailable else available.") + is_available: Boolean! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CheckProductStockAvailability") @doc(description: "If qty is more than stock display status as unavailable else available.") prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") @doc(description: "Contains details about the price of the item, including taxes and discounts.") product: ProductInterface! @doc(description: "Details about an item in the cart.") errors: [CartItemError!] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemErrors") @doc(description: "An array of errors encountered while loading the cart item") diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php index 544f91b92cf70..b8dbc2f5ab7c0 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php @@ -86,7 +86,7 @@ public function testStockStatusUnavailableSimpleProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals('unavailable', $responseDataObject->getData('cart/items/0/stock_availability')); + self::assertEquals(false, $responseDataObject->getData('cart/items/0/is_available')); } #[ @@ -103,8 +103,8 @@ public function testStockStatusUnavailableAddSimpleProduct(): void $responseDataObject = new DataObject($response); self::assertEquals( - 'unavailable', - $responseDataObject->getData('addProductsToCart/cart/items/0/stock_availability') + false, + $responseDataObject->getData('addProductsToCart/cart/items/0/is_available') ); } @@ -145,8 +145,8 @@ public function testStockStatusUnavailableBundleProduct(): void $responseDataObject = new DataObject($response); self::assertEquals( - 'unavailable', - $responseDataObject->getData('cart/items/0/stock_availability') + false, + $responseDataObject->getData('cart/items/0/is_available') ); } @@ -200,8 +200,8 @@ public function testStockStatusUnavailableAddBundleProduct(): void $responseDataObject = new DataObject($response); self::assertEquals( - 'unavailable', - $responseDataObject->getData('addProductsToCart/cart/items/0/stock_availability') + false, + $responseDataObject->getData('addProductsToCart/cart/items/0/is_available') ); } @@ -234,8 +234,8 @@ public function testStockStatusUnavailableConfigurableProduct(): void $responseDataObject = new DataObject($response); self::assertEquals( - 'unavailable', - $responseDataObject->getData('cart/items/0/stock_availability') + false, + $responseDataObject->getData('cart/items/0/is_available') ); } @@ -267,8 +267,8 @@ public function testStockStatusUnavailableAddConfigurableProduct(): void $responseDataObject = new DataObject($response); self::assertEquals( - 'unavailable', - $responseDataObject->getData('addProductsToCart/cart/items/0/stock_availability') + false, + $responseDataObject->getData('addProductsToCart/cart/items/0/is_available') ); } @@ -282,7 +282,7 @@ private function getQuery(string $cartId): string { cart(cart_id:"{$cartId}"){ items{ - stock_availability + is_available } } } @@ -304,7 +304,7 @@ private function mutationAddSimpleProduct(string $cartId, string $sku, int $qty ) { cart { items { - stock_availability + is_available } } } @@ -333,7 +333,7 @@ private function mutationAddBundleProduct( ) { cart { items { - stock_availability + is_available product { sku } @@ -363,7 +363,7 @@ private function mutationAddConfigurableProduct( ) { cart { items { - stock_availability + is_available product { sku } From afd51ceded339a4be0b056f3f87be196c1e06ed7 Mon Sep 17 00:00:00 2001 From: Abhishek Pathak Date: Thu, 14 Dec 2023 19:39:23 +0530 Subject: [PATCH 13/14] Fix review coments --- .../Magento/QuoteGraphQl/etc/schema.graphqls | 2 +- .../GraphQl/Quote/StockAvailabilityTest.php | 69 ++++++++++++++----- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index bf77d2e6e07b5..e8653da785c97 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -360,7 +360,7 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ id: String! @deprecated(reason: "Use `uid` instead.") uid: ID! @doc(description: "The unique ID for a `CartItemInterface` object.") quantity: Float! @doc(description: "The quantity of this item in the cart.") - is_available: Boolean! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CheckProductStockAvailability") @doc(description: "If qty is more than stock display status as unavailable else available.") + is_available: Boolean! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CheckProductStockAvailability") @doc(description: "True if requested quantity is less than available stock, false otherwise.") prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") @doc(description: "Contains details about the price of the item, including taxes and discounts.") product: ProductInterface! @doc(description: "Details about an item in the cart.") errors: [CartItemError!] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemErrors") @doc(description: "An array of errors encountered while loading the cart item") diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php index b8dbc2f5ab7c0..bdf6423e1f8f3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php @@ -86,24 +86,47 @@ public function testStockStatusUnavailableSimpleProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals(false, $responseDataObject->getData('cart/items/0/is_available')); + self::assertFalse( + $responseDataObject->getData('cart/items/0/is_available') + ); } #[ - DataFixture(ProductFixture::class, ['sku' => self::SKU, 'price' => 100.00], as: 'product'), + DataFixture(ProductFixture::class, ['price' => 100.00], as: 'product'), DataFixture(GuestCartFixture::class, as: 'cart'), DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 100]), DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] - public function testStockStatusUnavailableAddSimpleProduct(): void + public function testStockStatusAvailableSimpleProduct(): void { $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); - $query = $this->mutationAddSimpleProduct($maskedQuoteId, self::SKU, 1); + $query = $this->getQuery($maskedQuoteId); $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals( - false, + self::assertTrue( + $responseDataObject->getData('cart/items/0/is_available') + ); + } + + #[ + DataFixture(ProductFixture::class, ['sku' => self::SKU, 'price' => 100.00], as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 99]), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') + ] + public function testStockStatusAddSimpleProduct(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $query = $this->mutationAddSimpleProduct($maskedQuoteId, self::SKU, 1); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + self::assertTrue( + $responseDataObject->getData('addProductsToCart/cart/items/0/is_available') + ); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + self::assertFalse( $responseDataObject->getData('addProductsToCart/cart/items/0/is_available') ); } @@ -144,8 +167,7 @@ public function testStockStatusUnavailableBundleProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals( - false, + self::assertFalse( $responseDataObject->getData('cart/items/0/is_available') ); } @@ -173,12 +195,12 @@ public function testStockStatusUnavailableBundleProduct(): void 'cart_id' => '$cart.id$', 'product_id' => '$bundleProduct.id$', 'selections' => [['$product.id$']], - 'qty' => 100 + 'qty' => 99 ], ), DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask') ] - public function testStockStatusUnavailableAddBundleProduct(): void + public function testStockStatusAddBundleProduct(): void { $product = $this->productRepository->get(self::PARENT_SKU_BUNDLE); @@ -198,9 +220,14 @@ public function testStockStatusUnavailableAddBundleProduct(): void $query = $this->mutationAddBundleProduct($maskedQuoteId, self::PARENT_SKU_BUNDLE, $bundleOptionIdV2); $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); + self::assertTrue( + $responseDataObject->getData('addProductsToCart/cart/items/0/is_available') + ); - self::assertEquals( - false, + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + + self::assertFalse( $responseDataObject->getData('addProductsToCart/cart/items/0/is_available') ); } @@ -233,8 +260,7 @@ public function testStockStatusUnavailableConfigurableProduct(): void $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - self::assertEquals( - false, + self::assertFalse( $responseDataObject->getData('cart/items/0/is_available') ); } @@ -259,15 +285,18 @@ public function testStockStatusUnavailableConfigurableProduct(): void ], ) ] - public function testStockStatusUnavailableAddConfigurableProduct(): void + public function testStockStatusAddConfigurableProduct(): void { $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); $query = $this->mutationAddConfigurableProduct($maskedQuoteId, self::SKU, self::PARENT_SKU_CONFIGURABLE); $response = $this->graphQlMutation($query); $responseDataObject = new DataObject($response); - - self::assertEquals( - false, + self::assertTrue( + $responseDataObject->getData('addProductsToCart/cart/items/1/is_available') + ); + $response = $this->graphQlMutation($query); + $responseDataObject = new DataObject($response); + self::assertFalse( $responseDataObject->getData('addProductsToCart/cart/items/0/is_available') ); } @@ -369,6 +398,10 @@ private function mutationAddConfigurableProduct( } } } + user_errors { + code + message + } } } QUERY; From dbfe81c259e6d4e5c4fb9058bfa1552aa410c699 Mon Sep 17 00:00:00 2001 From: Sumesh P Date: Mon, 18 Dec 2023 16:06:34 +0530 Subject: [PATCH 14/14] Code simplification --- .../Magento/QuoteGraphQl/Model/CartItem/ProductStock.php | 8 +++----- .../Model/Resolver/CheckProductStockAvailability.php | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php index 56696cf7d34a3..a39666131b4e9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php @@ -53,13 +53,13 @@ public function isProductAvailable($cartItem): bool $previousQty = 0; foreach ($cartItem->getQuote()->getItems() as $item) { - if ($item->getItemId() == $cartItem->getItemId()) { + if ($item->getItemId() === $cartItem->getItemId()) { $requestedQty = $item->getQtyToAdd() ?? $item->getQty(); $previousQty = $item->getPreviousQty() ?? 0; } } - if ($cartItem->getProductType() == self::PRODUCT_TYPE_BUNDLE) { + if ($cartItem->getProductType() === self::PRODUCT_TYPE_BUNDLE) { $qtyOptions = $cartItem->getQtyOptions(); $totalRequestedQty = $previousQty + $requestedQty; foreach ($qtyOptions as $qtyOption) { @@ -75,9 +75,7 @@ public function isProductAvailable($cartItem): bool } else { $requiredItemQty = $requestedQty + $previousQty; $productId = (int) $cartItem->getProduct()->getId(); - if (!$this->isStockAvailable($productId, $requiredItemQty)) { - return false; - } + return $this->isStockAvailable($productId, $requiredItemQty); } return true; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php index ed3ef8247390c..1db3f5e3358c2 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CheckProductStockAvailability.php @@ -51,6 +51,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var Item $cartItem */ $cartItem = $value['model']; - return $this->productStock->isProductAvailable($cartItem) ? true : false; + return $this->productStock->isProductAvailable($cartItem); } }