Skip to content

Commit 5ebdfb5

Browse files
authored
Merge pull request #9279 from magento-lynx/2.4.8-graphql-api-enhancements
2 parents 1819fe7 + c29a553 commit 5ebdfb5

File tree

21 files changed

+913
-91
lines changed

21 files changed

+913
-91
lines changed

app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ function (AttributeInterface $customAttribute) {
106106
if (!array_key_exists($attributeCode, $productData)) {
107107
continue;
108108
}
109-
$attributeValue = $productData[$attributeCode];
109+
$attributeValue = $productData[$attributeCode] ?? "";
110110
if (is_array($attributeValue)) {
111111
$attributeValue = implode(',', $attributeValue);
112112
}

app/code/Magento/CatalogInventory/Model/Config/Source/NotAvailableMessage.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
class NotAvailableMessage implements OptionSourceInterface
1616
{
17+
/**
18+
* Message config values
19+
*/
20+
public const VALUE_ONLY_X_OF_Y = 1;
21+
public const VALUE_NOT_ENOUGH_ITEMS = 2;
22+
1723
/**
1824
* Options getter
1925
*
@@ -23,12 +29,12 @@ public function toOptionArray(): array
2329
{
2430
$options = [];
2531
$options[] = [
26-
'value' => 1,
27-
'label' => __('Only X available for sale. Please adjust the quantity to continue'),
32+
'value' => self::VALUE_ONLY_X_OF_Y,
33+
'label' => __('Only X of Y available'),
2834
];
2935
$options[] = [
30-
'value' => 2,
31-
'label' => __('Not enough items for sale. Please adjust the quantity to continue'),
36+
'value' => self::VALUE_NOT_ENOUGH_ITEMS,
37+
'label' => __('Not enough items for sale'),
3238
];
3339
return $options;
3440
}
@@ -41,8 +47,8 @@ public function toOptionArray(): array
4147
public function toArray(): array
4248
{
4349
return [
44-
1 => __('Only X available for sale. Please adjust the quantity to continue'),
45-
2 => __('Not enough items for sale. Please adjust the quantity to continue')
50+
self::VALUE_ONLY_X_OF_Y => __('Only X of Y available'),
51+
self::VALUE_NOT_ENOUGH_ITEMS => __('Not enough items for sale')
4652
];
4753
}
4854
}

app/code/Magento/CatalogInventory/Model/StockStateProvider.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,15 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ
167167
}
168168

169169
if (!$this->checkQty($stockItem, $summaryQty) || !$this->checkQty($stockItem, $qty)) {
170-
$message = __('The requested qty is not available');
170+
$message = __('The requested qty. is not available');
171171
if ((int) $this->scopeConfig->getValue('cataloginventory/options/not_available_message') === 1) {
172172
$itemMessage = (__(sprintf(
173-
'Only %s available for sale. Please adjust the quantity to continue',
174-
$stockItem->getQty() - $stockItem->getMinQty()
173+
'Only %s of %s available',
174+
$stockItem->getQty() - $stockItem->getMinQty(),
175+
$this->localeFormat->getNumber($qty)
175176
)));
176177
} else {
177-
$itemMessage = (__('Not enough items for sale. Please adjust the quantity to continue'));
178+
$itemMessage = (__('Not enough items for sale'));
178179
}
179180
$result->setHasError(true)
180181
->setErrorCode('qty_available')
@@ -231,7 +232,7 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ
231232
}
232233
} elseif ($stockItem->getShowDefaultNotificationMessage()) {
233234
$result->setMessage(
234-
__('The requested qty is not available')
235+
__('The requested qty. is not available')
235236
);
236237
}
237238
}

app/code/Magento/CatalogInventory/i18n/en_US.csv

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Stock,Stock
7373
"Done","Done"
7474
"The requested qty exceeds the maximum qty allowed in shopping cart","The requested qty exceeds the maximum qty allowed in shopping cart"
7575
"You cannot use decimal quantity for this product.","You cannot use decimal quantity for this product."
76-
"Not enough items for sale. Please adjust the quantity to continue","Not enough items for sale. Please adjust the quantity to continue"
77-
"Only X available for sale. Please adjust the quantity to continue","Only X available for sale. Please adjust the quantity to continue"
78-
"Only %s available for sale. Please adjust the quantity to continue","Only %s available for sale. Please adjust the quantity to continue"
76+
"Only X of Y available","Only X of Y available"
77+
"Only %s of %s available","Only %s of %s available"
78+
"Not enough items for sale","Not enough items for sale"
79+
"The requested qty. is not available","The requested qty. is not available"

app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/NotAvailableMessageResolver.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
4747
}
4848

4949
if ((int) $this->scopeConfig->getValue('cataloginventory/options/not_available_message') === 1) {
50+
$requiredItemQty = ($cartItem->getQtyToAdd() ?? $cartItem->getQty()) + ($cartItem->getPreviousQty() ?? 0);
5051
return sprintf(
51-
'Only %s available for sale. Please adjust the quantity to continue',
52-
(string) $this->productStock->getProductSaleableQty($cartItem)
52+
'Only %s of %s available',
53+
(string) $this->productStock->getProductSaleableQty($cartItem),
54+
(string) $requiredItemQty
5355
);
5456
}
5557

56-
return 'Not enough items for sale. Please adjust the quantity to continue';
58+
return 'Not enough items for sale';
5759
}
5860
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogInventoryGraphQl\Model\Resolver;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\CatalogInventory\Model\StockState;
13+
use Magento\CatalogInventory\Model\Config\Source\NotAvailableMessage;
14+
use Magento\Framework\App\Config\ScopeConfigInterface;
15+
use Magento\Framework\Exception\LocalizedException;
16+
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
17+
use Magento\Framework\GraphQl\Config\Element\Field;
18+
use Magento\Framework\GraphQl\Query\ResolverInterface;
19+
use Magento\Quote\Model\Quote\Item;
20+
use Magento\QuoteGraphQl\Model\CartItem\ProductStock;
21+
22+
/**
23+
* Resolver for ProductInterface quantity
24+
* Returns the available stock quantity based on cataloginventory/options/not_available_message
25+
*/
26+
class QuantityResolver implements ResolverInterface
27+
{
28+
/**
29+
* Configurable product type code
30+
*/
31+
private const PRODUCT_TYPE_CONFIGURABLE = "configurable";
32+
33+
/**
34+
* Scope config path for not_available_message
35+
*/
36+
private const CONFIG_PATH_NOT_AVAILABLE_MESSAGE = "cataloginventory/options/not_available_message";
37+
38+
/**
39+
* @param ProductRepositoryInterface $productRepositoryInterface
40+
* @param ScopeConfigInterface $scopeConfig
41+
* @param StockState $stockState
42+
* @param ProductStock $productStock
43+
*/
44+
public function __construct(
45+
private readonly ProductRepositoryInterface $productRepositoryInterface,
46+
private readonly ScopeConfigInterface $scopeConfig,
47+
private readonly StockState $stockState,
48+
private readonly ProductStock $productStock,
49+
) {
50+
}
51+
52+
/**
53+
* @inheritdoc
54+
*
55+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
56+
*/
57+
public function resolve(
58+
Field $field,
59+
$context,
60+
ResolveInfo $info,
61+
array $value = null,
62+
array $args = null
63+
): ?float {
64+
65+
if ((int) $this->scopeConfig->getValue(
66+
self::CONFIG_PATH_NOT_AVAILABLE_MESSAGE
67+
) === NotAvailableMessage::VALUE_NOT_ENOUGH_ITEMS) {
68+
return null;
69+
}
70+
71+
if (isset($value['cart_item']) && $value['cart_item'] instanceof Item) {
72+
return $this->productStock->getProductAvailableStock($value['cart_item']);
73+
}
74+
75+
if (!isset($value['model'])) {
76+
throw new LocalizedException(__('"model" value should be specified'));
77+
}
78+
79+
/** @var Product $product */
80+
$product = $value['model'];
81+
82+
if ($product->getTypeId() === self::PRODUCT_TYPE_CONFIGURABLE) {
83+
$product = $this->productRepositoryInterface->get($product->getSku());
84+
}
85+
return $this->stockState->getStockQty($product->getId());
86+
}
87+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
interface ProductInterface {
55
only_x_left_in_stock: Float @doc(description: "Remaining stock if it is below the value assigned to the Only X Left Threshold option in the Admin.") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\OnlyXLeftInStockResolver")
66
stock_status: ProductStockStatus @doc(description: "The stock status of the product.") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\StockStatusProvider")
7+
quantity: Float @doc(description: "Amount of available stock") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\QuantityResolver")
78
}
89

910
enum ProductStockStatus @doc(description: "States whether a product stock status is in stock or out of stock.") {

app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ public function execute(Quote $cart, array $cartItemData): void
6464
try {
6565
$result = $cart->addProduct($product, $this->buyRequestBuilder->build($cartItemData));
6666
} catch (Exception $e) {
67+
68+
if (str_contains($e->getMessage(), 'The requested qty is not available')) {
69+
throw new GraphQlInputException(__('The requested qty. is not available'));
70+
}
71+
6772
throw new GraphQlInputException(
6873
__(
6974
'Could not add the product with SKU %sku to the shopping cart: %message',

app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616
use Magento\Quote\Model\Quote;
1717
use Magento\Quote\Model\Quote\Item;
1818

19-
/**
20-
* Update cart item
21-
*/
2219
class UpdateCartItem
2320
{
2421
/**
@@ -129,6 +126,9 @@ private function validateCartItem(Item $cartItem): void
129126
if ($cartItem->getHasError()) {
130127
$errors = [];
131128
foreach ($cartItem->getMessage(false) as $message) {
129+
if (str_contains($message, 'The requested qty is not available')) {
130+
throw new GraphQlInputException(__('The requested qty. is not available'));
131+
}
132132
$errors[] = $message;
133133
}
134134
if (!empty($errors)) {

app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,41 +25,24 @@
2525
class UpdateCartItems implements ResolverInterface
2626
{
2727
/**
28-
* @var GetCartForUser
28+
* Undefined error code
2929
*/
30-
private $getCartForUser;
31-
32-
/**
33-
* @var CartRepositoryInterface
34-
*/
35-
private $cartRepository;
36-
37-
/**
38-
* @var UpdateCartItemsProvider
39-
*/
40-
private $updateCartItems;
41-
42-
/**
43-
* @var ArgumentsProcessorInterface
44-
*/
45-
private $argsSelection;
30+
private const CODE_UNDEFINED = 'UNDEFINED';
4631

4732
/**
4833
* @param GetCartForUser $getCartForUser
4934
* @param CartRepositoryInterface $cartRepository
5035
* @param UpdateCartItemsProvider $updateCartItems
5136
* @param ArgumentsProcessorInterface $argsSelection
37+
* @param array $messageCodesMapper
5238
*/
5339
public function __construct(
54-
GetCartForUser $getCartForUser,
55-
CartRepositoryInterface $cartRepository,
56-
UpdateCartItemsProvider $updateCartItems,
57-
ArgumentsProcessorInterface $argsSelection
40+
private readonly GetCartForUser $getCartForUser,
41+
private readonly CartRepositoryInterface $cartRepository,
42+
private readonly UpdateCartItemsProvider $updateCartItems,
43+
private readonly ArgumentsProcessorInterface $argsSelection,
44+
private readonly array $messageCodesMapper,
5845
) {
59-
$this->getCartForUser = $getCartForUser;
60-
$this->cartRepository = $cartRepository;
61-
$this->updateCartItems = $updateCartItems;
62-
$this->argsSelection = $argsSelection;
6346
}
6447

6548
/**
@@ -75,10 +58,15 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
7558

7659
$maskedCartId = $processedArgs['input']['cart_id'];
7760

61+
$errors = [];
7862
if (empty($processedArgs['input']['cart_items'])
7963
|| !is_array($processedArgs['input']['cart_items'])
8064
) {
81-
throw new GraphQlInputException(__('Required parameter "cart_items" is missing.'));
65+
$message = 'Required parameter "cart_items" is missing.';
66+
$errors[] = [
67+
'message' => __($message),
68+
'code' => $this->getErrorCode($message)
69+
];
8270
}
8371

8472
$cartItems = $processedArgs['input']['cart_items'];
@@ -87,18 +75,40 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
8775

8876
try {
8977
$this->updateCartItems->processCartItems($cart, $cartItems);
90-
$updatedCart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
91-
$this->cartRepository->save($updatedCart);
92-
} catch (NoSuchEntityException $e) {
93-
throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e);
94-
} catch (LocalizedException $e) {
95-
throw new GraphQlInputException(__($e->getMessage()), $e);
78+
$this->cartRepository->save(
79+
$this->cartRepository->get((int)$cart->getId())
80+
);
81+
} catch (NoSuchEntityException | LocalizedException $e) {
82+
$message = (str_contains($e->getMessage(), 'The requested qty is not available'))
83+
? 'The requested qty. is not available'
84+
: $e->getMessage();
85+
$errors[] = [
86+
'message' => __($message),
87+
'code' => $this->getErrorCode($e->getMessage())
88+
];
9689
}
9790

9891
return [
9992
'cart' => [
100-
'model' => $updatedCart,
93+
'model' => $cart,
10194
],
95+
'errors' => $errors,
10296
];
10397
}
98+
99+
/**
100+
* Returns error code based on error message
101+
*
102+
* @param string $message
103+
* @return string
104+
*/
105+
private function getErrorCode(string $message): string
106+
{
107+
foreach ($this->messageCodesMapper as $key => $code) {
108+
if (str_contains($message, $key)) {
109+
return $code;
110+
}
111+
}
112+
return self::CODE_UNDEFINED;
113+
}
104114
}

app/code/Magento/QuoteGraphQl/etc/graphql/di.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,14 @@
7979
<plugin name="merge_guest_orders_with_customer_after_place"
8080
type="Magento\QuoteGraphQl\Plugin\Model\MergeGuestOrder" />
8181
</type>
82+
<type name="Magento\QuoteGraphQl\Model\Resolver\UpdateCartItems">
83+
<arguments>
84+
<argument name="messageCodesMapper" xsi:type="array">
85+
<item name="The requested qty" xsi:type="string">INSUFFICIENT_STOCK</item>
86+
<item name="Could not find cart item" xsi:type="string">COULD_NOT_FIND_CART_ITEM</item>
87+
<item name="Required parameter" xsi:type="string">REQUIRED_PARAMETER_MISSING</item>
88+
<item name="The fewest you may purchase" xsi:type="string">INVALID_PARAMETER_VALUE</item>
89+
</argument>
90+
</arguments>
91+
</type>
8292
</config>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ type AddVirtualProductsToCartOutput @doc(description: "Contains details about th
396396

397397
type UpdateCartItemsOutput @doc(description: "Contains details about the cart after updating items.") {
398398
cart: Cart! @doc(description: "The cart after updating products.")
399+
errors: [CartUserInputError!]! @doc(description: "Contains errors encountered while updating an item to the cart.")
399400
}
400401

401402
type RemoveItemFromCartOutput @doc(description: "Contains details about the cart after removing an item.") {
@@ -500,6 +501,9 @@ enum CartUserInputErrorType {
500501
PRODUCT_NOT_FOUND
501502
NOT_SALABLE
502503
INSUFFICIENT_STOCK
504+
COULD_NOT_FIND_CART_ITEM
505+
REQUIRED_PARAMETER_MISSING
506+
INVALID_PARAMETER_VALUE
503507
UNDEFINED
504508
}
505509
enum PlaceOrderErrorCodes {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
"""model"" value should be specified","""model"" value should be specified"
2+
"The requested qty. is not available","The requested qty. is not available"

dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected function setUp(): void
3737
public function testAddProductIfQuantityIsNotAvailable()
3838
{
3939
$this->expectException(\Exception::class);
40-
$this->expectExceptionMessage('The requested qty is not available');
40+
$this->expectExceptionMessage('The requested qty. is not available');
4141

4242
$sku = 'simple';
4343
$quantity = 200;

0 commit comments

Comments
 (0)