Skip to content

Commit

Permalink
Merge pull request #9279 from magento-lynx/2.4.8-graphql-api-enhancem…
Browse files Browse the repository at this point in the history
…ents
  • Loading branch information
svera authored Oct 1, 2024
2 parents 1819fe7 + c29a553 commit 5ebdfb5
Show file tree
Hide file tree
Showing 21 changed files with 913 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function (AttributeInterface $customAttribute) {
if (!array_key_exists($attributeCode, $productData)) {
continue;
}
$attributeValue = $productData[$attributeCode];
$attributeValue = $productData[$attributeCode] ?? "";
if (is_array($attributeValue)) {
$attributeValue = implode(',', $attributeValue);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

class NotAvailableMessage implements OptionSourceInterface
{
/**
* Message config values
*/
public const VALUE_ONLY_X_OF_Y = 1;
public const VALUE_NOT_ENOUGH_ITEMS = 2;

/**
* Options getter
*
Expand All @@ -23,12 +29,12 @@ public function toOptionArray(): array
{
$options = [];
$options[] = [
'value' => 1,
'label' => __('Only X available for sale. Please adjust the quantity to continue'),
'value' => self::VALUE_ONLY_X_OF_Y,
'label' => __('Only X of Y available'),
];
$options[] = [
'value' => 2,
'label' => __('Not enough items for sale. Please adjust the quantity to continue'),
'value' => self::VALUE_NOT_ENOUGH_ITEMS,
'label' => __('Not enough items for sale'),
];
return $options;
}
Expand All @@ -41,8 +47,8 @@ public function toOptionArray(): array
public function toArray(): array
{
return [
1 => __('Only X available for sale. Please adjust the quantity to continue'),
2 => __('Not enough items for sale. Please adjust the quantity to continue')
self::VALUE_ONLY_X_OF_Y => __('Only X of Y available'),
self::VALUE_NOT_ENOUGH_ITEMS => __('Not enough items for sale')
];
}
}
11 changes: 6 additions & 5 deletions app/code/Magento/CatalogInventory/Model/StockStateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,15 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ
}

if (!$this->checkQty($stockItem, $summaryQty) || !$this->checkQty($stockItem, $qty)) {
$message = __('The requested qty is not available');
$message = __('The requested qty. is not available');
if ((int) $this->scopeConfig->getValue('cataloginventory/options/not_available_message') === 1) {
$itemMessage = (__(sprintf(
'Only %s available for sale. Please adjust the quantity to continue',
$stockItem->getQty() - $stockItem->getMinQty()
'Only %s of %s available',
$stockItem->getQty() - $stockItem->getMinQty(),
$this->localeFormat->getNumber($qty)
)));
} else {
$itemMessage = (__('Not enough items for sale. Please adjust the quantity to continue'));
$itemMessage = (__('Not enough items for sale'));
}
$result->setHasError(true)
->setErrorCode('qty_available')
Expand Down Expand Up @@ -231,7 +232,7 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ
}
} elseif ($stockItem->getShowDefaultNotificationMessage()) {
$result->setMessage(
__('The requested qty is not available')
__('The requested qty. is not available')
);
}
}
Expand Down
7 changes: 4 additions & 3 deletions app/code/Magento/CatalogInventory/i18n/en_US.csv
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Stock,Stock
"Done","Done"
"The requested qty exceeds the maximum qty allowed in shopping cart","The requested qty exceeds the maximum qty allowed in shopping cart"
"You cannot use decimal quantity for this product.","You cannot use decimal quantity for this product."
"Not enough items for sale. Please adjust the quantity to continue","Not enough items for sale. Please adjust the quantity to continue"
"Only X available for sale. Please adjust the quantity to continue","Only X available for sale. Please adjust the quantity to continue"
"Only %s available for sale. Please adjust the quantity to continue","Only %s available for sale. Please adjust the quantity to continue"
"Only X of Y available","Only X of Y available"
"Only %s of %s available","Only %s of %s available"
"Not enough items for sale","Not enough items for sale"
"The requested qty. is not available","The requested qty. is not available"
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
}

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

return 'Not enough items for sale. Please adjust the quantity to continue';
return 'Not enough items for sale';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\CatalogInventoryGraphQl\Model\Resolver;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product;
use Magento\CatalogInventory\Model\StockState;
use Magento\CatalogInventory\Model\Config\Source\NotAvailableMessage;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Quote\Model\Quote\Item;
use Magento\QuoteGraphQl\Model\CartItem\ProductStock;

/**
* Resolver for ProductInterface quantity
* Returns the available stock quantity based on cataloginventory/options/not_available_message
*/
class QuantityResolver implements ResolverInterface
{
/**
* Configurable product type code
*/
private const PRODUCT_TYPE_CONFIGURABLE = "configurable";

/**
* Scope config path for not_available_message
*/
private const CONFIG_PATH_NOT_AVAILABLE_MESSAGE = "cataloginventory/options/not_available_message";

/**
* @param ProductRepositoryInterface $productRepositoryInterface
* @param ScopeConfigInterface $scopeConfig
* @param StockState $stockState
* @param ProductStock $productStock
*/
public function __construct(
private readonly ProductRepositoryInterface $productRepositoryInterface,
private readonly ScopeConfigInterface $scopeConfig,
private readonly StockState $stockState,
private readonly ProductStock $productStock,
) {
}

/**
* @inheritdoc
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
array $value = null,
array $args = null
): ?float {

if ((int) $this->scopeConfig->getValue(
self::CONFIG_PATH_NOT_AVAILABLE_MESSAGE
) === NotAvailableMessage::VALUE_NOT_ENOUGH_ITEMS) {
return null;
}

if (isset($value['cart_item']) && $value['cart_item'] instanceof Item) {
return $this->productStock->getProductAvailableStock($value['cart_item']);
}

if (!isset($value['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}

/** @var Product $product */
$product = $value['model'];

if ($product->getTypeId() === self::PRODUCT_TYPE_CONFIGURABLE) {
$product = $this->productRepositoryInterface->get($product->getSku());
}
return $this->stockState->getStockQty($product->getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
interface ProductInterface {
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")
stock_status: ProductStockStatus @doc(description: "The stock status of the product.") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\StockStatusProvider")
quantity: Float @doc(description: "Amount of available stock") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\QuantityResolver")
}

enum ProductStockStatus @doc(description: "States whether a product stock status is in stock or out of stock.") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public function execute(Quote $cart, array $cartItemData): void
try {
$result = $cart->addProduct($product, $this->buyRequestBuilder->build($cartItemData));
} catch (Exception $e) {

if (str_contains($e->getMessage(), 'The requested qty is not available')) {
throw new GraphQlInputException(__('The requested qty. is not available'));
}

throw new GraphQlInputException(
__(
'Could not add the product with SKU %sku to the shopping cart: %message',
Expand Down
6 changes: 3 additions & 3 deletions app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\Quote\Item;

/**
* Update cart item
*/
class UpdateCartItem
{
/**
Expand Down Expand Up @@ -129,6 +126,9 @@ private function validateCartItem(Item $cartItem): void
if ($cartItem->getHasError()) {
$errors = [];
foreach ($cartItem->getMessage(false) as $message) {
if (str_contains($message, 'The requested qty is not available')) {
throw new GraphQlInputException(__('The requested qty. is not available'));
}
$errors[] = $message;
}
if (!empty($errors)) {
Expand Down
76 changes: 43 additions & 33 deletions app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,24 @@
class UpdateCartItems implements ResolverInterface
{
/**
* @var GetCartForUser
* Undefined error code
*/
private $getCartForUser;

/**
* @var CartRepositoryInterface
*/
private $cartRepository;

/**
* @var UpdateCartItemsProvider
*/
private $updateCartItems;

/**
* @var ArgumentsProcessorInterface
*/
private $argsSelection;
private const CODE_UNDEFINED = 'UNDEFINED';

/**
* @param GetCartForUser $getCartForUser
* @param CartRepositoryInterface $cartRepository
* @param UpdateCartItemsProvider $updateCartItems
* @param ArgumentsProcessorInterface $argsSelection
* @param array $messageCodesMapper
*/
public function __construct(
GetCartForUser $getCartForUser,
CartRepositoryInterface $cartRepository,
UpdateCartItemsProvider $updateCartItems,
ArgumentsProcessorInterface $argsSelection
private readonly GetCartForUser $getCartForUser,
private readonly CartRepositoryInterface $cartRepository,
private readonly UpdateCartItemsProvider $updateCartItems,
private readonly ArgumentsProcessorInterface $argsSelection,
private readonly array $messageCodesMapper,
) {
$this->getCartForUser = $getCartForUser;
$this->cartRepository = $cartRepository;
$this->updateCartItems = $updateCartItems;
$this->argsSelection = $argsSelection;
}

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

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

$errors = [];
if (empty($processedArgs['input']['cart_items'])
|| !is_array($processedArgs['input']['cart_items'])
) {
throw new GraphQlInputException(__('Required parameter "cart_items" is missing.'));
$message = 'Required parameter "cart_items" is missing.';
$errors[] = [
'message' => __($message),
'code' => $this->getErrorCode($message)
];
}

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

try {
$this->updateCartItems->processCartItems($cart, $cartItems);
$updatedCart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
$this->cartRepository->save($updatedCart);
} catch (NoSuchEntityException $e) {
throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e);
} catch (LocalizedException $e) {
throw new GraphQlInputException(__($e->getMessage()), $e);
$this->cartRepository->save(
$this->cartRepository->get((int)$cart->getId())
);
} catch (NoSuchEntityException | LocalizedException $e) {
$message = (str_contains($e->getMessage(), 'The requested qty is not available'))
? 'The requested qty. is not available'
: $e->getMessage();
$errors[] = [
'message' => __($message),
'code' => $this->getErrorCode($e->getMessage())
];
}

return [
'cart' => [
'model' => $updatedCart,
'model' => $cart,
],
'errors' => $errors,
];
}

/**
* Returns error code based on error message
*
* @param string $message
* @return string
*/
private function getErrorCode(string $message): string
{
foreach ($this->messageCodesMapper as $key => $code) {
if (str_contains($message, $key)) {
return $code;
}
}
return self::CODE_UNDEFINED;
}
}
10 changes: 10 additions & 0 deletions app/code/Magento/QuoteGraphQl/etc/graphql/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,14 @@
<plugin name="merge_guest_orders_with_customer_after_place"
type="Magento\QuoteGraphQl\Plugin\Model\MergeGuestOrder" />
</type>
<type name="Magento\QuoteGraphQl\Model\Resolver\UpdateCartItems">
<arguments>
<argument name="messageCodesMapper" xsi:type="array">
<item name="The requested qty" xsi:type="string">INSUFFICIENT_STOCK</item>
<item name="Could not find cart item" xsi:type="string">COULD_NOT_FIND_CART_ITEM</item>
<item name="Required parameter" xsi:type="string">REQUIRED_PARAMETER_MISSING</item>
<item name="The fewest you may purchase" xsi:type="string">INVALID_PARAMETER_VALUE</item>
</argument>
</arguments>
</type>
</config>
4 changes: 4 additions & 0 deletions app/code/Magento/QuoteGraphQl/etc/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ type AddVirtualProductsToCartOutput @doc(description: "Contains details about th

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

type RemoveItemFromCartOutput @doc(description: "Contains details about the cart after removing an item.") {
Expand Down Expand Up @@ -500,6 +501,9 @@ enum CartUserInputErrorType {
PRODUCT_NOT_FOUND
NOT_SALABLE
INSUFFICIENT_STOCK
COULD_NOT_FIND_CART_ITEM
REQUIRED_PARAMETER_MISSING
INVALID_PARAMETER_VALUE
UNDEFINED
}
enum PlaceOrderErrorCodes {
Expand Down
1 change: 1 addition & 0 deletions app/code/Magento/QuoteGraphQl/i18n/en_US.csv
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
"""model"" value should be specified","""model"" value should be specified"
"The requested qty. is not available","The requested qty. is not available"
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected function setUp(): void
public function testAddProductIfQuantityIsNotAvailable()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The requested qty is not available');
$this->expectExceptionMessage('The requested qty. is not available');

$sku = 'simple';
$quantity = 200;
Expand Down
Loading

0 comments on commit 5ebdfb5

Please sign in to comment.