Skip to content

Commit e6a51cb

Browse files
committed
Merge remote-tracking branch 'origin/ACP2E-3885' into PR_2025_05_16
2 parents 68974e9 + b67a078 commit e6a51cb

File tree

2 files changed

+273
-7
lines changed

2 files changed

+273
-7
lines changed

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
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;
@@ -38,14 +37,12 @@ class ProductStock
3837
*
3938
* @param ProductRepositoryInterface $productRepositoryInterface
4039
* @param StockState $stockState
41-
* @param StockConfigurationInterface $stockConfiguration
4240
* @param ScopeConfigInterface $scopeConfig
4341
* @param StockRegistryInterface $stockRegistry
4442
*/
4543
public function __construct(
4644
private readonly ProductRepositoryInterface $productRepositoryInterface,
4745
private readonly StockState $stockState,
48-
private readonly StockConfigurationInterface $stockConfiguration,
4946
private readonly ScopeConfigInterface $scopeConfig,
5047
private readonly StockRegistryInterface $stockRegistry
5148
) {
@@ -70,9 +67,21 @@ public function isProductAvailable(Item $cartItem): bool
7067
$variantProduct = $this->getVariantProduct($cartItem);
7168
$requiredItemQty = $requestedQty + $previousQty;
7269
if ($variantProduct !== null) {
73-
return $this->isStockQtyAvailable($variantProduct, $requestedQty, $requiredItemQty, $previousQty);
70+
return $this->isStockQtyAvailable(
71+
$cartItem,
72+
$variantProduct,
73+
$requestedQty,
74+
$requiredItemQty,
75+
$previousQty
76+
);
7477
}
75-
return $this->isStockQtyAvailable($cartItem->getProduct(), $requestedQty, $requiredItemQty, $previousQty);
78+
return $this->isStockQtyAvailable(
79+
$cartItem,
80+
$cartItem->getProduct(),
81+
$requestedQty,
82+
$requiredItemQty,
83+
$previousQty
84+
);
7685
}
7786

7887
/**
@@ -93,7 +102,13 @@ public function isStockAvailableBundle(Item $cartItem, int $previousQty, $reques
93102
if ($totalRequestedQty) {
94103
$requiredItemQty = $requiredItemQty * $totalRequestedQty;
95104
}
96-
if (!$this->isStockQtyAvailable($qtyOption->getProduct(), $requestedQty, $requiredItemQty, $previousQty)) {
105+
if (!$this->isStockQtyAvailable(
106+
$cartItem,
107+
$qtyOption->getProduct(),
108+
$requestedQty,
109+
$requiredItemQty,
110+
$previousQty
111+
)) {
97112
return false;
98113
}
99114
}
@@ -147,24 +162,27 @@ private function getVariantProduct(Item $cartItem): ?ProductInterface
147162
/**
148163
* Check if product is available in stock
149164
*
165+
* @param Item $cartItem
150166
* @param ProductInterface $product
151167
* @param float $itemQty
152168
* @param float $requiredQuantity
153169
* @param float $prevQty
154170
* @return bool
155171
*/
156172
private function isStockQtyAvailable(
173+
Item $cartItem,
157174
ProductInterface $product,
158175
float $itemQty,
159176
float $requiredQuantity,
160177
float $prevQty
161178
): bool {
179+
$scopeId = $cartItem->getStore()->getId();
162180
$stockStatus = $this->stockState->checkQuoteItemQty(
163181
$product->getId(),
164182
$itemQty,
165183
$requiredQuantity,
166184
$prevQty,
167-
$this->stockConfiguration->getDefaultScopeId()
185+
$scopeId
168186
);
169187

170188
return ((bool) $stockStatus->getHasError()) === false;
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\QuoteGraphQl\Test\Unit\Model\CartItem;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\CatalogInventory\Api\Data\StockStatusInterface;
13+
use Magento\CatalogInventory\Api\StockRegistryInterface;
14+
use Magento\CatalogInventory\Model\StockState;
15+
use Magento\Framework\App\Config\ScopeConfigInterface;
16+
use Magento\Quote\Model\Quote\Item;
17+
use Magento\Quote\Model\Quote\Item\Option;
18+
use Magento\QuoteGraphQl\Model\CartItem\ProductStock;
19+
use Magento\Store\Api\Data\StoreInterface;
20+
use PHPUnit\Framework\MockObject\MockObject;
21+
use PHPUnit\Framework\TestCase;
22+
23+
/**
24+
* Unit test for ProductStock::isProductAvailable()
25+
*/
26+
class ProductStockTest extends TestCase
27+
{
28+
/**
29+
* @var ProductStock
30+
*/
31+
private $productStock;
32+
33+
/**
34+
* @var ProductRepositoryInterface|MockObject
35+
*/
36+
private $productRepositoryMock;
37+
38+
/**
39+
* @var StockState|MockObject
40+
*/
41+
private $stockStateMock;
42+
43+
/**
44+
* @var ScopeConfigInterface|MockObject
45+
*/
46+
private $scopeConfigMock;
47+
48+
/**
49+
* @var StockRegistryInterface|MockObject
50+
*/
51+
private $stockRegistryMock;
52+
53+
/**
54+
* @var Item|MockObject
55+
*/
56+
private $cartItemMock;
57+
58+
/**
59+
* @var ProductInterface|MockObject
60+
*/
61+
private $productMock;
62+
63+
/**
64+
* @var StoreInterface|MockObject
65+
*/
66+
private $storeMock;
67+
68+
/**
69+
* @var StockStatusInterface|MockObject
70+
*/
71+
private $stockStatusMock;
72+
73+
/**
74+
* Set up mocks and initialize the ProductStock class
75+
*/
76+
protected function setUp(): void
77+
{
78+
$this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class);
79+
$this->stockStateMock = $this->createMock(StockState::class);
80+
$this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
81+
$this->stockRegistryMock = $this->createMock(StockRegistryInterface::class);
82+
$this->productStock = new ProductStock(
83+
$this->productRepositoryMock,
84+
$this->stockStateMock,
85+
$this->scopeConfigMock,
86+
$this->stockRegistryMock
87+
);
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);
99+
}
100+
101+
/**
102+
* Test isProductAvailable() for a simple product with sufficient stock
103+
*/
104+
public function testIsProductAvailableForSimpleProductWithStock(): void
105+
{
106+
$this->cartItemMock->expects($this->exactly(2))
107+
->method('getProductType')
108+
->willReturn('simple');
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())
116+
->method('getProduct')
117+
->willReturn($this->productMock);
118+
$this->cartItemMock->expects($this->once())
119+
->method('getStore')
120+
->willReturn($this->storeMock);
121+
$this->storeMock->expects($this->once())
122+
->method('getId')
123+
->willReturn(1);
124+
$this->productMock->expects($this->once())
125+
->method('getId')
126+
->willReturn(123);
127+
$this->stockStatusMock->expects($this->once())
128+
->method('getHasError')
129+
->willReturn(false);
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);
137+
}
138+
139+
/**
140+
* Test isProductAvailable() for a simple product with insufficient stock
141+
*/
142+
public function testIsProductAvailableForSimpleProductWithoutStock()
143+
{
144+
$this->cartItemMock->expects($this->exactly(2))
145+
->method('getProductType')
146+
->willReturn('simple');
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())
154+
->method('getProduct')
155+
->willReturn($this->productMock);
156+
$this->cartItemMock->expects($this->once())
157+
->method('getStore')
158+
->willReturn($this->storeMock);
159+
$this->storeMock->expects($this->once())
160+
->method('getId')
161+
->willReturn(1);
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())
194+
->method('getStore')
195+
->willReturn($this->storeMock);
196+
$this->storeMock->expects($this->once())
197+
->method('getId')
198+
->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);
205+
$this->stockStateMock->expects($this->once())
206+
->method('checkQuoteItemQty')
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())
236+
->method('getHasError')
237+
->willReturn(true);
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);
247+
}
248+
}

0 commit comments

Comments
 (0)