Skip to content

Commit 73f7db5

Browse files
committed
Merge branch '2.4-develop' of https://github.com/adobe-commerce-tier-4/magento2ce into PR-03-12-2024-anna
2 parents 6a11ca4 + ef6d9c8 commit 73f7db5

File tree

5 files changed

+136
-40
lines changed

5 files changed

+136
-40
lines changed

app/code/Magento/CatalogGraphQl/Model/Config/AttributeReader.php

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
use Magento\Catalog\Model\Product;
1010
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
1111
use Magento\CatalogGraphQl\Model\Resolver\Products\Attributes\Collection;
12+
use Magento\CatalogGraphQl\Model\Resolver\Products\Attributes\CollectionFactory;
1213
use Magento\EavGraphQl\Model\Resolver\Query\Type;
1314
use Magento\Framework\App\Config\ScopeConfigInterface;
15+
use Magento\Framework\App\ObjectManager;
1416
use Magento\Framework\Config\ReaderInterface;
1517
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
1618
use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface;
@@ -36,9 +38,9 @@ class AttributeReader implements ReaderInterface
3638
private Type $typeLocator;
3739

3840
/**
39-
* @var Collection
41+
* @var CollectionFactory
4042
*/
41-
private Collection $collection;
43+
private readonly CollectionFactory $collectionFactory;
4244

4345
/**
4446
* @var ScopeConfigInterface
@@ -48,18 +50,21 @@ class AttributeReader implements ReaderInterface
4850
/**
4951
* @param MapperInterface $mapper
5052
* @param Type $typeLocator
51-
* @param Collection $collection
53+
* @param Collection $collection @deprecated @see $collectionFactory
5254
* @param ScopeConfigInterface $config
55+
* @param CollectionFactory|null $collectionFactory
56+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
5357
*/
5458
public function __construct(
5559
MapperInterface $mapper,
5660
Type $typeLocator,
5761
Collection $collection,
58-
ScopeConfigInterface $config
62+
ScopeConfigInterface $config,
63+
CollectionFactory $collectionFactory = null,
5964
) {
6065
$this->mapper = $mapper;
6166
$this->typeLocator = $typeLocator;
62-
$this->collection = $collection;
67+
$this->collectionFactory = $collectionFactory ?? ObjectManager::getInstance()->get(CollectionFactory::class);
6368
$this->config = $config;
6469
}
6570

@@ -74,12 +79,11 @@ public function __construct(
7479
public function read($scope = null) : array
7580
{
7681
$config = [];
77-
7882
if ($this->config->isSetFlag(self::XML_PATH_INCLUDE_DYNAMIC_ATTRIBUTES, ScopeInterface::SCOPE_STORE)) {
7983
$typeNames = $this->mapper->getMappedTypes(Product::ENTITY);
8084

8185
/** @var Attribute $attribute */
82-
foreach ($this->collection->getAttributes() as $attribute) {
86+
foreach ($this->collectionFactory->create()->getAttributes() as $attribute) {
8387
$attributeCode = $attribute->getAttributeCode();
8488
$locatedType = $this->typeLocator->getType($attributeCode, Product::ENTITY) ?: 'String';
8589
$locatedType = TypeProcessor::NORMALIZED_ANY_TYPE === $locatedType ? 'String' : ucfirst($locatedType);

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)