Skip to content

Commit 8aa33e3

Browse files
authored
Merge branch '2.4-develop' into Sync-2.4.7-develop
2 parents 58f5b8f + ef6d9c8 commit 8aa33e3

File tree

4 files changed

+125
-33
lines changed

4 files changed

+125
-33
lines changed

app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type AddConfigurableProductsToCartOutput @doc(description: "Contains details abo
5656

5757
input ConfigurableProductCartItemInput {
5858
data: CartItemInput! @doc(description: "The quantity and SKU of the configurable product.")
59-
variant_sku: String @doc(description: "Deprecated. Use `CartItemInput.sku` instead.")
59+
variant_sku: String @deprecated(reason: "Use `CartItemInput.sku` instead.")
6060
parent_sku: String @doc(description: "The SKU of the parent configurable product.")
6161
customizable_options:[CustomizableOptionInput!] @doc(description: "The ID and value of the option.")
6262
}

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

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
namespace Magento\QuoteGraphQl\Model\CartItem;
2020

21+
use Magento\Catalog\Api\Data\ProductInterface;
22+
use Magento\Catalog\Api\ProductRepositoryInterface;
2123
use Magento\CatalogInventory\Api\StockStatusRepositoryInterface;
24+
use Magento\Framework\Exception\NoSuchEntityException;
2225
use Magento\Quote\Model\Quote\Item;
2326

2427
/**
@@ -27,17 +30,29 @@
2730
class ProductStock
2831
{
2932
/**
30-
* Product type code
33+
* Bundle product type code
3134
*/
3235
private const PRODUCT_TYPE_BUNDLE = "bundle";
3336

37+
/**
38+
* Configurable product type code
39+
*/
40+
private const PRODUCT_TYPE_CONFIGURABLE = "configurable";
41+
42+
/**
43+
* Simple product type code
44+
*/
45+
private const PRODUCT_TYPE_SIMPLE = "simple";
46+
3447
/**
3548
* ProductStock constructor
3649
*
3750
* @param StockStatusRepositoryInterface $stockStatusRepository
51+
* @param ProductRepositoryInterface $productRepositoryInterface
3852
*/
3953
public function __construct(
40-
private StockStatusRepositoryInterface $stockStatusRepository
54+
private readonly StockStatusRepositoryInterface $stockStatusRepository,
55+
private readonly ProductRepositoryInterface $productRepositoryInterface
4156
) {
4257
}
4358

@@ -46,38 +61,42 @@ public function __construct(
4661
*
4762
* @param Item $cartItem
4863
* @return bool
64+
* @throws NoSuchEntityException
4965
*/
50-
public function isProductAvailable($cartItem): bool
66+
public function isProductAvailable(Item $cartItem): bool
5167
{
5268
$requestedQty = 0;
5369
$previousQty = 0;
70+
/**
71+
* @var ProductInterface $variantProduct
72+
* Configurable products cannot have stock, only its variants can. If the user adds a configurable product
73+
* using its SKU and the selected options, we need to get the variant it refers to from the quote.
74+
*/
75+
$variantProduct = null;
5476

5577
foreach ($cartItem->getQuote()->getItems() as $item) {
56-
if ($item->getItemId() === $cartItem->getItemId()) {
57-
$requestedQty = $item->getQtyToAdd() ?? $item->getQty();
58-
$previousQty = $item->getPreviousQty() ?? 0;
78+
if ($item->getItemId() !== $cartItem->getItemId()) {
79+
continue;
5980
}
81+
if ($cartItem->getProductType() === self::PRODUCT_TYPE_CONFIGURABLE) {
82+
if ($cartItem->getChildren()[0] !== null) {
83+
$variantProduct = $this->productRepositoryInterface->get($item->getSku());
84+
}
85+
}
86+
$requestedQty = $item->getQtyToAdd() ?? $item->getQty();
87+
$previousQty = $item->getPreviousQty() ?? 0;
6088
}
6189

6290
if ($cartItem->getProductType() === self::PRODUCT_TYPE_BUNDLE) {
63-
$qtyOptions = $cartItem->getQtyOptions();
64-
$totalRequestedQty = $previousQty + $requestedQty;
65-
foreach ($qtyOptions as $qtyOption) {
66-
$productId = (int) $qtyOption->getProductId();
67-
$requiredItemQty = (float) $qtyOption->getValue();
68-
if ($totalRequestedQty) {
69-
$requiredItemQty = $requiredItemQty * $totalRequestedQty;
70-
}
71-
if (!$this->isStockAvailable($productId, $requiredItemQty)) {
72-
return false;
73-
}
74-
}
75-
} else {
76-
$requiredItemQty = $requestedQty + $previousQty;
77-
$productId = (int) $cartItem->getProduct()->getId();
78-
return $this->isStockAvailable($productId, $requiredItemQty);
91+
return $this->isStockAvailableBundle($cartItem, $previousQty, $requestedQty);
7992
}
80-
return true;
93+
94+
$requiredItemQty = $requestedQty + $previousQty;
95+
$productId = (int) $cartItem->getProduct()->getId();
96+
if ($variantProduct !== null) {
97+
$productId = (int)$variantProduct->getId();
98+
}
99+
return $this->isStockAvailable($productId, $requiredItemQty);
81100
}
82101

83102
/**
@@ -92,4 +111,29 @@ private function isStockAvailable(int $productId, float $requiredQuantity): bool
92111
$stock = $this->stockStatusRepository->get($productId);
93112
return $stock->getQty() >= $requiredQuantity;
94113
}
114+
115+
/**
116+
* Calculate available stock of a bundle
117+
*
118+
* @param Item $cartItem
119+
* @param int $previousQty
120+
* @param int|float $requestedQty
121+
* @return bool
122+
*/
123+
public function isStockAvailableBundle(Item $cartItem, int $previousQty, $requestedQty): bool
124+
{
125+
$qtyOptions = $cartItem->getQtyOptions();
126+
$totalRequestedQty = $previousQty + $requestedQty;
127+
foreach ($qtyOptions as $qtyOption) {
128+
$productId = (int)$qtyOption->getProductId();
129+
$requiredItemQty = $qtyOption->getValue();
130+
if ($totalRequestedQty) {
131+
$requiredItemQty = $requiredItemQty * $totalRequestedQty;
132+
}
133+
if (!$this->isStockAvailable($productId, $requiredItemQty)) {
134+
return false;
135+
}
136+
}
137+
return true;
138+
}
95139
}

dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use Magento\ConfigurableProduct\Test\Fixture\AddProductToCart as AddConfigurableProductToCartFixture;
2727
use Magento\ConfigurableProduct\Test\Fixture\Attribute as AttributeFixture;
2828
use Magento\ConfigurableProduct\Test\Fixture\Product as ConfigurableProductFixture;
29+
use Magento\Eav\Api\Data\AttributeInterface;
30+
use Magento\Eav\Api\Data\AttributeOptionInterface;
2931
use Magento\Framework\DataObject;
3032
use Magento\Framework\ObjectManagerInterface;
3133
use Magento\Quote\Test\Fixture\AddProductToCart;
@@ -266,33 +268,71 @@ public function testStockStatusUnavailableConfigurableProduct(): void
266268
}
267269

268270
#[
269-
DataFixture(ProductFixture::class, ['sku' => self::SKU], as: 'product'),
271+
DataFixture(ProductFixture::class, as: 'product'),
270272
DataFixture(AttributeFixture::class, as: 'attribute'),
271273
DataFixture(
272274
ConfigurableProductFixture::class,
273-
['sku' => self::PARENT_SKU_CONFIGURABLE, '_options' => ['$attribute$'], '_links' => ['$product$']],
275+
['_options' => ['$attribute$'], '_links' => ['$product$']],
274276
'configurable_product'
275277
),
276278
DataFixture(GuestCartFixture::class, as: 'cart'),
277279
DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'),
280+
DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 100], 'prodStock'),
278281
DataFixture(
279282
AddConfigurableProductToCartFixture::class,
280283
[
281284
'cart_id' => '$cart.id$',
282285
'product_id' => '$configurable_product.id$',
283286
'child_product_id' => '$product.id$',
284-
'qty' => 100
287+
'qty' => 90
288+
],
289+
),
290+
]
291+
public function testStockStatusAvailableConfigurableProduct(): void
292+
{
293+
$maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId();
294+
$query = $this->getQuery($maskedQuoteId);
295+
$response = $this->graphQlMutation($query);
296+
$responseDataObject = new DataObject($response);
297+
298+
self::assertTrue(
299+
$responseDataObject->getData('cart/items/0/is_available')
300+
);
301+
}
302+
303+
#[
304+
DataFixture(ProductFixture::class, ['sku' => self::SKU], as: 'product'),
305+
DataFixture(AttributeFixture::class, as: 'attribute'),
306+
DataFixture(
307+
ConfigurableProductFixture::class,
308+
[
309+
'sku' => self::PARENT_SKU_CONFIGURABLE,
310+
'_options' => ['$attribute$'],
311+
'_links' => ['$product$'],
285312
],
286-
)
313+
'configurable_product'
314+
),
315+
DataFixture(GuestCartFixture::class, as: 'cart'),
316+
DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'),
287317
]
288318
public function testStockStatusAddConfigurableProduct(): void
289319
{
290320
$maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId();
291-
$query = $this->mutationAddConfigurableProduct($maskedQuoteId, self::SKU, self::PARENT_SKU_CONFIGURABLE);
321+
/** @var AttributeInterface $attribute */
322+
$attribute = $this->fixtures->get('attribute');
323+
/** @var AttributeOptionInterface $option */
324+
$option = $attribute->getOptions()[1];
325+
$selectedOption = base64_encode("configurable/{$attribute->getAttributeId()}/{$option->getValue()}");
326+
$query = $this->mutationAddConfigurableProduct(
327+
$maskedQuoteId,
328+
self::PARENT_SKU_CONFIGURABLE,
329+
$selectedOption,
330+
100
331+
);
292332
$response = $this->graphQlMutation($query);
293333
$responseDataObject = new DataObject($response);
294334
self::assertTrue(
295-
$responseDataObject->getData('addProductsToCart/cart/items/1/is_available')
335+
$responseDataObject->getData('addProductsToCart/cart/items/0/is_available')
296336
);
297337
$response = $this->graphQlMutation($query);
298338
$responseDataObject = new DataObject($response);
@@ -376,7 +416,7 @@ private function mutationAddBundleProduct(
376416
private function mutationAddConfigurableProduct(
377417
string $cartId,
378418
string $sku,
379-
string $parentSku,
419+
string $selectedOption,
380420
int $qty = 1
381421
): string {
382422
return <<<QUERY
@@ -387,7 +427,9 @@ private function mutationAddConfigurableProduct(
387427
{
388428
sku: "{$sku}"
389429
quantity: $qty
390-
parent_sku: "{$parentSku}"
430+
selected_options: [
431+
"$selectedOption"
432+
]
391433
}]
392434
) {
393435
cart {

dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ class DependencyTest extends \PHPUnit\Framework\TestCase
5959
*/
6060
public const MAP_TYPE_REDUNDANT = 'redundant';
6161

62+
/**
63+
* Redundant dependencies error message
64+
*/
65+
public const UNUSED_DEPENDENCY_ERROR_MSG =
66+
'Some dependencies required by composer.json are not used in the module and must be removed:';
67+
6268
/**
6369
* Count of directories in path
6470
*/
@@ -869,7 +875,7 @@ public function testRedundant()
869875
}
870876
}
871877
if (!empty($output)) {
872-
$this->fail("Redundant dependencies found!\r\n" . implode(' ', $output));
878+
$this->fail(self::UNUSED_DEPENDENCY_ERROR_MSG . "\r\n" . implode(' ', $output));
873879
}
874880
}
875881

0 commit comments

Comments
 (0)