Skip to content

Commit 4f0d43f

Browse files
committed
ACP2E-3885: is_available attribute in CartItemInterface returns false even when salable stock is high
1 parent 147e077 commit 4f0d43f

File tree

2 files changed

+211
-2
lines changed

2 files changed

+211
-2
lines changed

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Magento\Framework\Exception\NoSuchEntityException;
1818
use Magento\Quote\Model\Quote\Item;
1919
use Magento\Store\Model\ScopeInterface;
20+
use Magento\Store\Model\StoreManagerInterface;
2021

2122
/**
2223
* Product Stock class to check availability of product
@@ -41,13 +42,15 @@ class ProductStock
4142
* @param StockConfigurationInterface $stockConfiguration
4243
* @param ScopeConfigInterface $scopeConfig
4344
* @param StockRegistryInterface $stockRegistry
45+
* @param StoreManagerInterface $storeManager
4446
*/
4547
public function __construct(
4648
private readonly ProductRepositoryInterface $productRepositoryInterface,
4749
private readonly StockState $stockState,
4850
private readonly StockConfigurationInterface $stockConfiguration,
4951
private readonly ScopeConfigInterface $scopeConfig,
50-
private readonly StockRegistryInterface $stockRegistry
52+
private readonly StockRegistryInterface $stockRegistry,
53+
private readonly StoreManagerInterface $storeManager
5154
) {
5255
}
5356

@@ -60,6 +63,9 @@ public function __construct(
6063
*/
6164
public function isProductAvailable(Item $cartItem): bool
6265
{
66+
if (!$cartItem->getQtyToAdd()) {
67+
return false;
68+
}
6369
$requestedQty = $cartItem->getQtyToAdd() ?? $cartItem->getQty();
6470
$previousQty = $cartItem->getPreviousQty() ?? 0;
6571

@@ -159,12 +165,15 @@ private function isStockQtyAvailable(
159165
float $requiredQuantity,
160166
float $prevQty
161167
): bool {
168+
$storeId = $this->storeManager->getStore()->getId();
169+
$websiteId = $this->storeManager->getStore($storeId)->getWebsiteId();
170+
$scopeId = $websiteId ?? $this->stockConfiguration->getDefaultScopeId();
162171
$stockStatus = $this->stockState->checkQuoteItemQty(
163172
$product->getId(),
164173
$itemQty,
165174
$requiredQuantity,
166175
$prevQty,
167-
$this->stockConfiguration->getDefaultScopeId()
176+
$scopeId
168177
);
169178

170179
return ((bool) $stockStatus->getHasError()) === false;
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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\StockConfigurationInterface;
14+
use Magento\CatalogInventory\Api\StockRegistryInterface;
15+
use Magento\CatalogInventory\Model\StockState;
16+
use Magento\Framework\App\Config\ScopeConfigInterface;
17+
use Magento\Quote\Model\Quote\Item;
18+
use Magento\QuoteGraphQl\Model\CartItem\ProductStock;
19+
use Magento\Store\Api\Data\StoreInterface;
20+
use Magento\Store\Model\StoreManagerInterface;
21+
use PHPUnit\Framework\MockObject\MockObject;
22+
use PHPUnit\Framework\TestCase;
23+
24+
/**
25+
* Unit test for ProductStock::isProductAvailable()
26+
*/
27+
class ProductStockTest extends TestCase
28+
{
29+
/**
30+
* @var ProductStock
31+
*/
32+
private $productStock;
33+
34+
/**
35+
* @var ProductRepositoryInterface|MockObject
36+
*/
37+
private $productRepositoryMock;
38+
39+
/**
40+
* @var StockState|MockObject
41+
*/
42+
private $stockStateMock;
43+
44+
/**
45+
* @var StockConfigurationInterface|MockObject
46+
*/
47+
private $stockConfigurationMock;
48+
49+
/**
50+
* @var ScopeConfigInterface|MockObject
51+
*/
52+
private $scopeConfigMock;
53+
54+
/**
55+
* @var StockRegistryInterface|MockObject
56+
*/
57+
private $stockRegistryMock;
58+
59+
/**
60+
* @var StoreManagerInterface|MockObject
61+
*/
62+
private $storeManagerMock;
63+
64+
/**
65+
* Set up mocks and initialize the ProductStock class
66+
*/
67+
protected function setUp(): void
68+
{
69+
$this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class);
70+
$this->stockStateMock = $this->createMock(StockState::class);
71+
$this->stockConfigurationMock = $this->createMock(StockConfigurationInterface::class);
72+
$this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
73+
$this->stockRegistryMock = $this->createMock(StockRegistryInterface::class);
74+
$this->storeManagerMock = $this->createMock(StoreManagerInterface::class);
75+
$this->productStock = new ProductStock(
76+
$this->productRepositoryMock,
77+
$this->stockStateMock,
78+
$this->stockConfigurationMock,
79+
$this->scopeConfigMock,
80+
$this->stockRegistryMock,
81+
$this->storeManagerMock
82+
);
83+
}
84+
85+
/**
86+
* Test isProductAvailable() without quantity to add
87+
*/
88+
public function testIsProductAvailableWithoutQtyToAdd(): void
89+
{
90+
$cartItemMock = $this->getMockBuilder(Item::class)
91+
->addMethods(['getQtyToAdd'])
92+
->disableOriginalConstructor()
93+
->getMock();
94+
$cartItemMock->expects($this->once())
95+
->method('getQtyToAdd')
96+
->willReturn(null);
97+
$this->assertFalse($this->productStock->isProductAvailable($cartItemMock));
98+
}
99+
100+
/**
101+
* Test isProductAvailable() for a simple product with sufficient stock
102+
*/
103+
public function testIsProductAvailableForSimpleProductWithStock(): void
104+
{
105+
$cartItemMock = $this->getMockBuilder(Item::class)
106+
->addMethods(['getQtyToAdd', 'getPreviousQty'])
107+
->onlyMethods(['getProductType', 'getProduct'])
108+
->disableOriginalConstructor()
109+
->getMock();
110+
$productMock = $this->createMock(ProductInterface::class);
111+
$storeMock = $this->createMock(StoreInterface::class);
112+
$stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)
113+
->disableOriginalConstructor()
114+
->addMethods(['getHasError'])
115+
->getMockForAbstractClass();
116+
$cartItemMock->expects($this->exactly(2))
117+
->method('getQtyToAdd')
118+
->willReturn(2);
119+
$cartItemMock->expects($this->once())
120+
->method('getPreviousQty')
121+
->willReturn(1);
122+
$cartItemMock->expects($this->exactly(2))
123+
->method('getProductType')
124+
->willReturn('simple');
125+
$cartItemMock->expects($this->once())
126+
->method('getProduct')
127+
->willReturn($productMock);
128+
$productMock->expects($this->once())
129+
->method('getId')
130+
->willReturn(1);
131+
$this->storeManagerMock->expects($this->any())
132+
->method('getStore')
133+
->willReturn($storeMock);
134+
$storeMock->expects($this->once())
135+
->method('getId')
136+
->willReturn(1);
137+
$storeMock->expects($this->once())
138+
->method('getWebsiteId')
139+
->willReturn(1);
140+
$this->stockConfigurationMock->expects($this->never())->method('getDefaultScopeId');
141+
$this->stockStateMock->expects($this->once())
142+
->method('checkQuoteItemQty')
143+
->with(1, 2, 3, 1, 1)
144+
->willReturn($stockStatusMock);
145+
$stockStatusMock->expects($this->once())
146+
->method('getHasError')
147+
->willReturn(false);
148+
$this->assertTrue($this->productStock->isProductAvailable($cartItemMock));
149+
}
150+
151+
/**
152+
* Test isProductAvailable() for a simple product with insufficient stock
153+
*/
154+
public function testIsProductAvailableForSimpleProductWithoutStock(): void
155+
{
156+
$cartItemMock = $this->getMockBuilder(Item::class)
157+
->addMethods(['getQtyToAdd', 'getPreviousQty'])
158+
->onlyMethods(['getProductType', 'getProduct'])
159+
->disableOriginalConstructor()
160+
->getMock();
161+
$productMock = $this->createMock(ProductInterface::class);
162+
$storeMock = $this->createMock(StoreInterface::class);
163+
$stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)
164+
->disableOriginalConstructor()
165+
->addMethods(['getHasError'])
166+
->getMockForAbstractClass();
167+
$cartItemMock->expects($this->exactly(2))
168+
->method('getQtyToAdd')
169+
->willReturn(5);
170+
$cartItemMock->expects($this->once())
171+
->method('getPreviousQty')
172+
->willReturn(0);
173+
$cartItemMock->expects($this->exactly(2))
174+
->method('getProductType')
175+
->willReturn('simple');
176+
$cartItemMock->expects($this->once())
177+
->method('getProduct')
178+
->willReturn($productMock);
179+
$productMock->expects($this->once())
180+
->method('getId')
181+
->willReturn(1);
182+
$this->storeManagerMock->expects($this->exactly(2))
183+
->method('getStore')
184+
->willReturn($storeMock);
185+
$storeMock->expects($this->once())
186+
->method('getId')
187+
->willReturn(1);
188+
$storeMock->expects($this->once())
189+
->method('getWebsiteId')
190+
->willReturn(1);
191+
$this->stockStateMock->expects($this->once())
192+
->method('checkQuoteItemQty')
193+
->with(1, 5, 5, 0, 1)
194+
->willReturn($stockStatusMock);
195+
$stockStatusMock->expects($this->once())
196+
->method('getHasError')
197+
->willReturn(true);
198+
$this->assertFalse($this->productStock->isProductAvailable($cartItemMock));
199+
}
200+
}

0 commit comments

Comments
 (0)