Skip to content

Commit 3564388

Browse files
committed
ACP2E-3885: is_available attribute in CartItemInterface returns false even when salable stock is high
1 parent 3f0b7ae commit 3564388

File tree

2 files changed

+157
-98
lines changed

2 files changed

+157
-98
lines changed

app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@
99

1010
use Magento\Catalog\Api\Data\ProductInterface;
1111
use Magento\Catalog\Api\ProductRepositoryInterface;
12-
use Magento\CatalogInventory\Api\StockConfigurationInterface;
1312
use Magento\CatalogInventory\Api\StockRegistryInterface;
1413
use Magento\CatalogInventory\Model\Configuration;
1514
use Magento\CatalogInventory\Model\StockState;
1615
use Magento\Framework\App\Config\ScopeConfigInterface;
1716
use Magento\Framework\Exception\NoSuchEntityException;
1817
use Magento\Quote\Model\Quote\Item;
1918
use Magento\Store\Model\ScopeInterface;
20-
use Magento\Store\Model\StoreManagerInterface;
2119

2220
/**
2321
* Product Stock class to check availability of product
@@ -39,18 +37,14 @@ class ProductStock
3937
*
4038
* @param ProductRepositoryInterface $productRepositoryInterface
4139
* @param StockState $stockState
42-
* @param StockConfigurationInterface $stockConfiguration
4340
* @param ScopeConfigInterface $scopeConfig
4441
* @param StockRegistryInterface $stockRegistry
45-
* @param StoreManagerInterface $storeManager
4642
*/
4743
public function __construct(
4844
private readonly ProductRepositoryInterface $productRepositoryInterface,
4945
private readonly StockState $stockState,
50-
private readonly StockConfigurationInterface $stockConfiguration,
5146
private readonly ScopeConfigInterface $scopeConfig,
52-
private readonly StockRegistryInterface $stockRegistry,
53-
private readonly StoreManagerInterface $storeManager
47+
private readonly StockRegistryInterface $stockRegistry
5448
) {
5549
}
5650

@@ -73,9 +67,13 @@ public function isProductAvailable(Item $cartItem): bool
7367
$variantProduct = $this->getVariantProduct($cartItem);
7468
$requiredItemQty = $requestedQty + $previousQty;
7569
if ($variantProduct !== null) {
76-
return $this->isStockQtyAvailable($variantProduct, $requestedQty, $requiredItemQty, $previousQty);
70+
return $this->isStockQtyAvailable(
71+
$cartItem, $variantProduct, $requestedQty, $requiredItemQty, $previousQty
72+
);
7773
}
78-
return $this->isStockQtyAvailable($cartItem->getProduct(), $requestedQty, $requiredItemQty, $previousQty);
74+
return $this->isStockQtyAvailable(
75+
$cartItem, $cartItem->getProduct(), $requestedQty, $requiredItemQty, $previousQty
76+
);
7977
}
8078

8179
/**
@@ -96,7 +94,8 @@ public function isStockAvailableBundle(Item $cartItem, int $previousQty, $reques
9694
if ($totalRequestedQty) {
9795
$requiredItemQty = $requiredItemQty * $totalRequestedQty;
9896
}
99-
if (!$this->isStockQtyAvailable($qtyOption->getProduct(), $requestedQty, $requiredItemQty, $previousQty)) {
97+
if (!$this->isStockQtyAvailable(
98+
$cartItem, $qtyOption->getProduct(), $requestedQty, $requiredItemQty, $previousQty)) {
10099
return false;
101100
}
102101
}
@@ -150,24 +149,21 @@ private function getVariantProduct(Item $cartItem): ?ProductInterface
150149
/**
151150
* Check if product is available in stock
152151
*
152+
* @param Item $cartItem
153153
* @param ProductInterface $product
154154
* @param float $itemQty
155155
* @param float $requiredQuantity
156156
* @param float $prevQty
157157
* @return bool
158158
*/
159159
private function isStockQtyAvailable(
160+
Item $cartItem,
160161
ProductInterface $product,
161162
float $itemQty,
162163
float $requiredQuantity,
163164
float $prevQty
164165
): bool {
165-
try {
166-
$storeId = $this->storeManager->getStore()->getId();
167-
$scopeId = $this->storeManager->getStore($storeId)->getWebsiteId();
168-
} catch (NoSuchEntityException $e) {
169-
$scopeId = $this->stockConfiguration->getDefaultScopeId();
170-
}
166+
$scopeId = $cartItem->getStore()->getId();
171167
$stockStatus = $this->stockState->checkQuoteItemQty(
172168
$product->getId(),
173169
$itemQty,

app/code/Magento/QuoteGraphQl/Test/Unit/Model/CartItem/ProductStockTest.php

Lines changed: 145 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
use Magento\Catalog\Api\Data\ProductInterface;
1111
use Magento\Catalog\Api\ProductRepositoryInterface;
1212
use Magento\CatalogInventory\Api\Data\StockStatusInterface;
13-
use Magento\CatalogInventory\Api\StockConfigurationInterface;
1413
use Magento\CatalogInventory\Api\StockRegistryInterface;
1514
use Magento\CatalogInventory\Model\StockState;
1615
use Magento\Framework\App\Config\ScopeConfigInterface;
1716
use Magento\Quote\Model\Quote\Item;
17+
use Magento\Quote\Model\Quote\Item\Option;
1818
use Magento\QuoteGraphQl\Model\CartItem\ProductStock;
1919
use Magento\Store\Api\Data\StoreInterface;
20-
use Magento\Store\Model\StoreManagerInterface;
2120
use PHPUnit\Framework\MockObject\MockObject;
2221
use PHPUnit\Framework\TestCase;
2322

@@ -41,11 +40,6 @@ class ProductStockTest extends TestCase
4140
*/
4241
private $stockStateMock;
4342

44-
/**
45-
* @var StockConfigurationInterface|MockObject
46-
*/
47-
private $stockConfigurationMock;
48-
4943
/**
5044
* @var ScopeConfigInterface|MockObject
5145
*/
@@ -57,9 +51,24 @@ class ProductStockTest extends TestCase
5751
private $stockRegistryMock;
5852

5953
/**
60-
* @var StoreManagerInterface|MockObject
54+
* @var Item|MockObject
55+
*/
56+
private $cartItemMock;
57+
58+
/**
59+
* @var ProductInterface|MockObject
6160
*/
62-
private $storeManagerMock;
61+
private $productMock;
62+
63+
/**
64+
* @var StoreInterface|MockObject
65+
*/
66+
private $storeMock;
67+
68+
/**
69+
* @var StockStatusInterface|MockObject
70+
*/
71+
private $stockStatusMock;
6372

6473
/**
6574
* Set up mocks and initialize the ProductStock class
@@ -68,118 +77,172 @@ protected function setUp(): void
6877
{
6978
$this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class);
7079
$this->stockStateMock = $this->createMock(StockState::class);
71-
$this->stockConfigurationMock = $this->createMock(StockConfigurationInterface::class);
7280
$this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
7381
$this->stockRegistryMock = $this->createMock(StockRegistryInterface::class);
74-
$this->storeManagerMock = $this->createMock(StoreManagerInterface::class);
7582
$this->productStock = new ProductStock(
7683
$this->productRepositoryMock,
7784
$this->stockStateMock,
78-
$this->stockConfigurationMock,
7985
$this->scopeConfigMock,
80-
$this->stockRegistryMock,
81-
$this->storeManagerMock
86+
$this->stockRegistryMock
8287
);
88+
$this->stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)
89+
->disableOriginalConstructor()
90+
->addMethods(['getHasError'])
91+
->getMockForAbstractClass();
92+
$this->cartItemMock = $this->getMockBuilder(Item::class)
93+
->addMethods(['getQtyToAdd', 'getPreviousQty'])
94+
->onlyMethods(['getStore', 'getProductType', 'getProduct', 'getChildren', 'getQtyOptions'])
95+
->disableOriginalConstructor()
96+
->getMock();
97+
$this->productMock = $this->createMock(ProductInterface::class);
98+
$this->storeMock = $this->createMock(StoreInterface::class);
8399
}
84100

85101
/**
86102
* Test isProductAvailable() for a simple product with sufficient stock
87103
*/
88104
public function testIsProductAvailableForSimpleProductWithStock(): void
89105
{
90-
$cartItemMock = $this->getMockBuilder(Item::class)
91-
->addMethods(['getQtyToAdd', 'getPreviousQty'])
92-
->onlyMethods(['getProductType', 'getProduct'])
93-
->disableOriginalConstructor()
94-
->getMock();
95-
$productMock = $this->createMock(ProductInterface::class);
96-
$storeMock = $this->createMock(StoreInterface::class);
97-
$stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)
98-
->disableOriginalConstructor()
99-
->addMethods(['getHasError'])
100-
->getMockForAbstractClass();
101-
$cartItemMock->expects($this->once())
102-
->method('getQtyToAdd')
103-
->willReturn(2);
104-
$cartItemMock->expects($this->once())
105-
->method('getPreviousQty')
106-
->willReturn(1);
107-
$cartItemMock->expects($this->exactly(2))
106+
$this->cartItemMock->expects($this->exactly(2))
108107
->method('getProductType')
109108
->willReturn('simple');
110-
$cartItemMock->expects($this->once())
109+
$this->cartItemMock->expects($this->once())
110+
->method('getQtyToAdd')
111+
->willReturn(2.0);
112+
$this->cartItemMock->expects($this->once())
113+
->method('getPreviousQty')
114+
->willReturn(1.0);
115+
$this->cartItemMock->expects($this->once())
111116
->method('getProduct')
112-
->willReturn($productMock);
113-
$productMock->expects($this->once())
114-
->method('getId')
115-
->willReturn(1);
116-
$this->storeManagerMock->expects($this->any())
117+
->willReturn($this->productMock);
118+
$this->cartItemMock->expects($this->once())
117119
->method('getStore')
118-
->willReturn($storeMock);
119-
$storeMock->expects($this->once())
120+
->willReturn($this->storeMock);
121+
$this->storeMock->expects($this->once())
120122
->method('getId')
121123
->willReturn(1);
122-
$storeMock->expects($this->once())
123-
->method('getWebsiteId')
124-
->willReturn(1);
125-
$this->stockConfigurationMock->expects($this->never())->method('getDefaultScopeId');
126-
$this->stockStateMock->expects($this->once())
127-
->method('checkQuoteItemQty')
128-
->with(1, 2, 3, 1, 1)
129-
->willReturn($stockStatusMock);
130-
$stockStatusMock->expects($this->once())
124+
$this->productMock->expects($this->once())
125+
->method('getId')
126+
->willReturn(123);
127+
$this->stockStatusMock->expects($this->once())
131128
->method('getHasError')
132129
->willReturn(false);
133-
$this->assertTrue($this->productStock->isProductAvailable($cartItemMock));
130+
$this->stockStateMock->expects($this->once())
131+
->method('checkQuoteItemQty')
132+
->with(123, 2.0, 3.0, 1.0, 1)
133+
->willReturn($this->stockStatusMock);
134+
$this->cartItemMock->expects($this->never())->method('getChildren');
135+
$result = $this->productStock->isProductAvailable($this->cartItemMock);
136+
$this->assertTrue($result);
134137
}
135138

136139
/**
137140
* Test isProductAvailable() for a simple product with insufficient stock
138141
*/
139-
public function testIsProductAvailableForSimpleProductWithoutStock(): void
142+
public function testIsProductAvailableForSimpleProductWithoutStock()
140143
{
141-
$cartItemMock = $this->getMockBuilder(Item::class)
142-
->addMethods(['getQtyToAdd', 'getPreviousQty'])
143-
->onlyMethods(['getProductType', 'getProduct'])
144-
->disableOriginalConstructor()
145-
->getMock();
146-
$productMock = $this->createMock(ProductInterface::class);
147-
$storeMock = $this->createMock(StoreInterface::class);
148-
$stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)
149-
->disableOriginalConstructor()
150-
->addMethods(['getHasError'])
151-
->getMockForAbstractClass();
152-
$cartItemMock->expects($this->once())
153-
->method('getQtyToAdd')
154-
->willReturn(5);
155-
$cartItemMock->expects($this->once())
156-
->method('getPreviousQty')
157-
->willReturn(0);
158-
$cartItemMock->expects($this->exactly(2))
144+
$this->cartItemMock->expects($this->exactly(2))
159145
->method('getProductType')
160146
->willReturn('simple');
161-
$cartItemMock->expects($this->once())
147+
$this->cartItemMock->expects($this->once())
148+
->method('getQtyToAdd')
149+
->willReturn(2.0);
150+
$this->cartItemMock->expects($this->once())
151+
->method('getPreviousQty')
152+
->willReturn(1.0);
153+
$this->cartItemMock->expects($this->once())
162154
->method('getProduct')
163-
->willReturn($productMock);
164-
$productMock->expects($this->once())
155+
->willReturn($this->productMock);
156+
$this->cartItemMock->expects($this->once())
157+
->method('getStore')
158+
->willReturn($this->storeMock);
159+
$this->storeMock->expects($this->once())
165160
->method('getId')
166161
->willReturn(1);
167-
$this->storeManagerMock->expects($this->exactly(2))
162+
$this->productMock->expects($this->once())
163+
->method('getId')
164+
->willReturn(123);
165+
$this->stockStateMock->expects($this->once())
166+
->method('checkQuoteItemQty')
167+
->with(123, 2.0, 3.0, 1.0, 1)
168+
->willReturn($this->stockStatusMock);
169+
$this->stockStatusMock->expects($this->once())
170+
->method('getHasError')
171+
->willReturn(true);
172+
$this->cartItemMock->expects($this->never())->method('getChildren');
173+
$result = $this->productStock->isProductAvailable($this->cartItemMock);
174+
$this->assertFalse($result);
175+
}
176+
177+
/**
178+
* Test isStockAvailableBundle when stock is available
179+
*/
180+
public function testIsStockAvailableBundleStockAvailable()
181+
{
182+
$qtyOptionMock = $this->createMock(Option::class);
183+
$qtyOptionMock->expects($this->once())
184+
->method('getValue')
185+
->willReturn(2.0);
186+
$optionProductMock = $this->createMock(ProductInterface::class);
187+
$qtyOptionMock->expects($this->once())
188+
->method('getProduct')
189+
->willReturn($optionProductMock);
190+
$this->cartItemMock->expects($this->once())
191+
->method('getQtyOptions')
192+
->willReturn([$qtyOptionMock]);
193+
$this->cartItemMock->expects($this->once())
168194
->method('getStore')
169-
->willReturn($storeMock);
170-
$storeMock->expects($this->once())
195+
->willReturn($this->storeMock);
196+
$this->storeMock->expects($this->once())
171197
->method('getId')
172198
->willReturn(1);
173-
$storeMock->expects($this->once())
174-
->method('getWebsiteId')
175-
->willReturn(1);
199+
$optionProductMock->expects($this->once())
200+
->method('getId')
201+
->willReturn(789);
202+
$this->stockStatusMock->expects($this->once())
203+
->method('getHasError')
204+
->willReturn(false);
176205
$this->stockStateMock->expects($this->once())
177206
->method('checkQuoteItemQty')
178-
->with(1, 5, 5, 0, 1)
179-
->willReturn($stockStatusMock);
180-
$stockStatusMock->expects($this->once())
207+
->with(789, 2.0, 6.0, 1.0, 1)
208+
->willReturn($this->stockStatusMock);
209+
$result = $this->productStock->isStockAvailableBundle($this->cartItemMock, 1, 2.0);
210+
$this->assertTrue($result);
211+
}
212+
213+
/**
214+
* Test isStockAvailableBundle when stock is not available
215+
*/
216+
public function testIsStockAvailableBundleStockNotAvailable()
217+
{
218+
$qtyOptionMock = $this->createMock(\Magento\Quote\Model\Quote\Item\Option::class);
219+
$qtyOptionMock->expects($this->once())
220+
->method('getValue')
221+
->willReturn(2.0);
222+
$optionProductMock = $this->createMock(ProductInterface::class);
223+
$qtyOptionMock->expects($this->once())
224+
->method('getProduct')
225+
->willReturn($optionProductMock);
226+
$this->cartItemMock->expects($this->once())
227+
->method('getQtyOptions')
228+
->willReturn([$qtyOptionMock]);
229+
$this->cartItemMock->expects($this->once())
230+
->method('getStore')
231+
->willReturn($this->storeMock);
232+
$this->storeMock->expects($this->once())
233+
->method('getId')
234+
->willReturn(1);
235+
$this->stockStatusMock->expects($this->once())
181236
->method('getHasError')
182237
->willReturn(true);
183-
$this->assertFalse($this->productStock->isProductAvailable($cartItemMock));
238+
$optionProductMock->expects($this->once())
239+
->method('getId')
240+
->willReturn(789);
241+
$this->stockStateMock->expects($this->once())
242+
->method('checkQuoteItemQty')
243+
->with(789, 2.0, 6.0, 1.0, 1)
244+
->willReturn($this->stockStatusMock);
245+
$result = $this->productStock->isStockAvailableBundle($this->cartItemMock, 1, 2.0);
246+
$this->assertFalse($result);
184247
}
185248
}

0 commit comments

Comments
 (0)