diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php index 367724891026a..cccde2f8e155b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php @@ -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); } diff --git a/app/code/Magento/CatalogInventory/Model/Config/Source/NotAvailableMessage.php b/app/code/Magento/CatalogInventory/Model/Config/Source/NotAvailableMessage.php index 633ecf5ffdd66..3c0cded659150 100644 --- a/app/code/Magento/CatalogInventory/Model/Config/Source/NotAvailableMessage.php +++ b/app/code/Magento/CatalogInventory/Model/Config/Source/NotAvailableMessage.php @@ -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 * @@ -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; } @@ -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') ]; } } diff --git a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php index fef5d63d25089..e8992d381e056 100644 --- a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php +++ b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php @@ -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') @@ -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') ); } } diff --git a/app/code/Magento/CatalogInventory/i18n/en_US.csv b/app/code/Magento/CatalogInventory/i18n/en_US.csv index 394b918aaace5..2b15978aebf85 100644 --- a/app/code/Magento/CatalogInventory/i18n/en_US.csv +++ b/app/code/Magento/CatalogInventory/i18n/en_US.csv @@ -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" diff --git a/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/NotAvailableMessageResolver.php b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/NotAvailableMessageResolver.php index cb8d7722d6d02..7b357f989509d 100644 --- a/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/NotAvailableMessageResolver.php +++ b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/NotAvailableMessageResolver.php @@ -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'; } } diff --git a/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/QuantityResolver.php b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/QuantityResolver.php new file mode 100644 index 0000000000000..d7a3eed18f54c --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/QuantityResolver.php @@ -0,0 +1,87 @@ +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()); + } +} diff --git a/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls index aa4b8051d176c..f2ea0ce29dbe3 100644 --- a/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls @@ -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.") { diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index f2dd6389d2c4a..2e98469e3b1d9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -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', diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php b/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php index 7b5c9a57a7be9..5c09f657372fb 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php @@ -16,9 +16,6 @@ use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Item; -/** - * Update cart item - */ class UpdateCartItem { /** @@ -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)) { diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index a7f0e59035bd4..4d8cdebddc6be 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -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; } /** @@ -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']; @@ -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; + } } diff --git a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml index b203f9df08d4e..973612201de39 100644 --- a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml @@ -79,4 +79,14 @@ + + + + INSUFFICIENT_STOCK + COULD_NOT_FIND_CART_ITEM + REQUIRED_PARAMETER_MISSING + INVALID_PARAMETER_VALUE + + + diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index adc65c3434887..8764b4c2c2d25 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -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.") { @@ -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 { diff --git a/app/code/Magento/QuoteGraphQl/i18n/en_US.csv b/app/code/Magento/QuoteGraphQl/i18n/en_US.csv index 84e4f2946b63d..f6240e944efba 100644 --- a/app/code/Magento/QuoteGraphQl/i18n/en_US.csv +++ b/app/code/Magento/QuoteGraphQl/i18n/en_US.csv @@ -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" diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php index 9dd743f119749..1764418f7f9cb 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php @@ -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; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/StockQuantityTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/StockQuantityTest.php new file mode 100644 index 0000000000000..b776101ad5e46 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/StockQuantityTest.php @@ -0,0 +1,228 @@ +fixtures = DataFixtureStorageManager::getStorage(); + } + + #[ + Config('cataloginventory/options/not_available_message', 1), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 10]) + ] + public function testStockQuantitySimpleProduct(): void + { + $this->assertProductStockQuantity(10); + } + + #[ + Config('cataloginventory/options/not_available_message', 1), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$product.sku$'], + 'link' + ), + DataFixture( + BundleOptionFixture::class, + [ + 'title' => 'Checkbox Options', + 'type' => 'checkbox', + 'required' => 1, + 'product_links' => ['$link$'] + ], + 'option' + ), + DataFixture( + BundleProductFixture::class, + ['_options' => ['$option$']], + 'bundleProduct' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$bundleProduct.id$', + 'selections' => [['$product.id$']], + ], + ), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 10]) + ] + public function testStockQuantityBundleProduct(): void + { + $this->assertProductStockQuantity(10); + $this->assertNoStockQuantity('bundleProduct'); + } + + #[ + Config('cataloginventory/options/not_available_message', 1), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(AttributeFixture::class, as: 'attribute'), + DataFixture( + ConfigurableProductFixture::class, + [ + '_options' => ['$attribute$'], + '_links' => ['$product$'] + ], + 'configurableProduct' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + DataFixture( + AddConfigurableProductToCartFixture::class, + [ + 'cart_id' => '$cart.id$', + 'product_id' => '$configurableProduct.id$', + 'child_product_id' => '$product.id$', + ], + ), + DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 10]), + ] + public function testStockQuantityConfigurableProduct(): void + { + $this->assertProductStockQuantity(10); + $this->assertNoStockQuantity('configurableProduct'); + } + + #[ + Config('cataloginventory/options/not_available_message', 2), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + ] + public function testStockQuantityEmpty(): void + { + $this->assertProductStockQuantity(null); + } + + /** + * Asserts products stock quantity from cart & product query + * + * @param float|null $stockQuantity + * @return void + */ + private function assertProductStockQuantity(?float $stockQuantity): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $cartQuery = $this->getCartQuery($maskedQuoteId); + $cartResponse = $this->graphQlMutation($cartQuery); + $cartResponseDataObject = new DataObject($cartResponse); + self::assertEquals( + $stockQuantity, + $cartResponseDataObject->getData('cart/itemsV2/items/0/product/quantity') + ); + + $productQuery = $this->getProductQuery($this->fixtures->get('product')->getSku()); + $productResponse = $this->graphQlMutation($productQuery); + $productResponseDataObject = new DataObject($productResponse); + self::assertEquals( + $stockQuantity, + $productResponseDataObject->getData('products/items/0/quantity') + ); + } + + /** + * Asserts bundle & conf product stock quantity from product query + * + * @param string $productFixture + * @return void + */ + private function assertNoStockQuantity(string $productFixture): void + { + $productQuery = $this->getProductQuery($this->fixtures->get($productFixture)->getSku()); + $productResponse = $this->graphQlMutation($productQuery); + $productResponseDataObject = new DataObject($productResponse); + self::assertEquals( + 0, + $productResponseDataObject->getData('products/items/0/quantity') + ); + } + + /** + * Return cart query with product.quantity field + * + * @param string $cartId + * @return string + */ + private function getCartQuery(string $cartId): string + { + return <<getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); $quantity = 0.5; - $this->expectExceptionMessage( - "Could not update the product with SKU simple_product: The fewest you may purchase is 1" - ); $query = $this->getQuery($maskedQuoteId, $itemId, $quantity); - $this->graphQlMutation($query); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('errors', $response['updateCartItems']); + + $responseError = $response['updateCartItems']['errors'][0]; + $this->assertEquals( + "Could not update the product with SKU simple_product: The fewest you may purchase is 1.", + $responseError['message'] + ); + $this->assertEquals('INVALID_PARAMETER_VALUE', $responseError['code']); } /** @@ -65,11 +72,18 @@ public function testUpdateCartItemSetUnavailableQuantity() $itemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); $quantity = 100; - $this->expectExceptionMessage( - "Could not update the product with SKU simple_product: The requested qty is not available" - ); $query = $this->getQuery($maskedQuoteId, $itemId, $quantity); - $this->graphQlMutation($query); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('errors', $response['updateCartItems']); + + $responseError = $response['updateCartItems']['errors'][0]; + $this->assertEquals( + "The requested qty. is not available", + $responseError['message'] + ); + $this->assertEquals('INSUFFICIENT_STOCK', $responseError['code']); } /** @@ -97,6 +111,10 @@ private function getQuery(string $maskedQuoteId, int $itemId, float $quantity): quantity } } + errors { + message + code + } } } QUERY; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php index 9ada8d9ab7546..5230e89045ec9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php @@ -206,7 +206,7 @@ public function testAddVariationFromAnotherConfigurableProductWithDifferentSuper public function testAddProductIfQuantityIsNotAvailable() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The requested qty is not available'); + $this->expectExceptionMessage('The requested qty. is not available'); $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); $product = current($searchResponse['products']['items']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartItemCustomAttributeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartItemCustomAttributeTest.php new file mode 100644 index 0000000000000..1ca8bb3be7897 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartItemCustomAttributeTest.php @@ -0,0 +1,384 @@ +fixtures = DataFixtureStorageManager::getStorage(); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => 'product_custom_attribute', + 'is_visible_on_front' => true + ], + 'varchar_custom_attribute' + ), + DataFixture( + MultiselectAttribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'product_custom_attribute_multiselect', + 'is_visible_on_front' => true + ], + 'multiselect_custom_attribute' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'label' => 'red', + 'sort_order' => 20 + ], + 'multiselect_custom_attribute_option_1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'sort_order' => 10, + 'label' => 'white', + 'is_default' => true + ], + 'multiselect_custom_attribute_option_2' + ), + DataFixture( + ProductFixture::class, + [ + 'custom_attributes' => [ + [ + 'attribute_code' => '$varchar_custom_attribute.attribute_code$', + 'value' => '' + ], + [ + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'selected_options' => [], + ], + ], + ], + 'product' + ), + DataFixture(GuestCartFixture::class, ['reserved_order_id' => 'test_quote'], 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + ] + public function testEmptyErrorsOnCartItemCustomAttributeWithEmptyValue(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $productName = $this->fixtures->get('product')->getName(); + $cartQuery = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($cartQuery); + self::assertEquals( + [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + 0 => [ + 'product' => [ + 'name' => $productName, + 'custom_attributesV2' => [ + 'items' => [], + 'errors' => [] + ] + ] + ] + ] + ] + ] + ], + $response + ); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => 'product_custom_attribute', + 'is_comparable' => 1, + 'is_visible_on_front' => 1 + ], + 'varchar_custom_attribute' + ), + DataFixture( + MultiselectAttribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'product_custom_attribute_multiselect', + 'is_visible_on_front' => 1 + ], + 'multiselect_custom_attribute' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'label' => 'red', + 'sort_order' => 20 + ], + 'multiselect_custom_attribute_option_1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'sort_order' => 10, + 'label' => 'white', + 'is_default' => true + ], + 'multiselect_custom_attribute_option_2' + ), + DataFixture( + ProductFixture::class, + [ + 'custom_attributes' => [ + [ + 'attribute_code' => '$varchar_custom_attribute.attribute_code$', + 'value' => 'test value' + ], + [ + 'attribute_code' => '$multiselect_custom_attribute.attribute_code$', + 'selected_options' => [ + ['value' => '$multiselect_custom_attribute_option_1.value$'], + ['value' => '$multiselect_custom_attribute_option_2.value$'] + ], + ], + ], + ], + 'product' + ), + DataFixture(GuestCartFixture::class, ['reserved_order_id' => 'test_quote'], 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + ] + public function testEmptyErrorsOnCartItemCustomAttributeWithNonEmptyValue(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $productName = $this->fixtures->get('product')->getName(); + $multiselectCustomAttrOption1 = $this->fixtures->get('multiselect_custom_attribute_option_1')->getValue(); + $multiselectCustomAttrOption2 = $this->fixtures->get('multiselect_custom_attribute_option_2')->getValue(); + $cartQuery = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($cartQuery); + self::assertEquals( + [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + 0 => [ + 'product' => [ + 'name' => $productName, + 'custom_attributesV2' => [ + 'items' => [ + 0 => [ + 'code' => 'product_custom_attribute', + 'value' => 'test value' + ], + 1 => [ + 'code' => 'product_custom_attribute_multiselect', + 'selected_options' => [ + 0 => [ + 'value' => $multiselectCustomAttrOption2, + 'label' => 'white' + ], + 1 => [ + 'value' => $multiselectCustomAttrOption1, + 'label' => 'red' + ] + ] + ] + ], + 'errors' => [] + ] + ] + ] + ] + ] + ] + ], + $response + ); + } + + #[ + DataFixture( + SelectAttribute::class, + [ + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'source_model' => Table::class, + 'backend_model' => ArrayBackend::class, + 'attribute_code' => 'product_custom_attribute_select', + 'is_visible_on_front' => true + ], + 'select_custom_attribute' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => '$select_custom_attribute.attribute_code$', + 'label' => 'red', + 'sort_order' => 20 + ], + 'select_custom_attribute_option_1' + ), + DataFixture( + AttributeOptionFixture::class, + [ + 'entity_type' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'attribute_code' => '$select_custom_attribute.attribute_code$', + 'sort_order' => 10, + 'label' => 'white', + 'is_default' => true + ], + 'select_custom_attribute_option_2' + ), + DataFixture( + ProductFixture::class, + [ + 'custom_attributes' => [ + [ + 'attribute_code' => '$select_custom_attribute.attribute_code$', + 'selected_options' => [ + ['value' => '0'] + ], + ], + ], + ], + 'product' + ), + DataFixture(GuestCartFixture::class, ['reserved_order_id' => 'test_quote'], 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), + DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'), + ] + public function testEmptyErrorsOnCartItemCustomAttributeWithNoOptionSelected(): void + { + $maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId(); + $productName = $this->fixtures->get('product')->getName(); + $cartQuery = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($cartQuery); + self::assertEquals( + [ + 'cart' => [ + 'itemsV2' => [ + 'items' => [ + 0 => [ + 'product' => [ + 'name' => $productName, + 'custom_attributesV2' => [ + 'items' => [ + 0 => [ + 'code' => 'product_custom_attribute_select', + 'selected_options' => [] + ] + ], + 'errors' => [] + ] + ] + ] + ] + ] + ] + ], + $response + ); + } + + /** + * Returns cart query with product - custom_attributesV2 + * + * @param string $maskedQuoteId + * @return string + */ + private function getCartQuery(string $maskedQuoteId): string + { + return <<quoteIdToMaskedId->execute((int)$quote->getId()); $notExistentItemId = 999; - $this->expectExceptionMessage("Could not find cart item with id: {$notExistentItemId}."); - $query = $this->getQuery($maskedQuoteId, $notExistentItemId, 2); - $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('errors', $response['updateCartItems']); + + $responseError = $response['updateCartItems']['errors'][0]; + $this->assertEquals( + "Could not find cart item with id: {$notExistentItemId}.", + $responseError['message'] + ); + $this->assertEquals('COULD_NOT_FIND_CART_ITEM', $responseError['code']); } /** @@ -150,10 +158,18 @@ public function testUpdateItemIfItemIsNotBelongToCart() ->getItemByProduct($this->productRepository->get('virtual-product')) ->getId(); - $this->expectExceptionMessage("Could not find cart item with id: {$secondQuoteItemId}."); - $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId, 2); - $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('errors', $response['updateCartItems']); + + $responseError = $response['updateCartItems']['errors'][0]; + $this->assertEquals( + "Could not find cart item with id: {$secondQuoteItemId}.", + $responseError['message'] + ); + $this->assertEquals('COULD_NOT_FIND_CART_ITEM', $responseError['code']); } /** @@ -213,10 +229,11 @@ public function testUpdateItemInAnotherCustomerCart() /** * @param string $input * @param string $message + * @param string $errorCode * @dataProvider dataProviderUpdateWithMissedRequiredParameters * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php */ - public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message, string $errorCode) { $quote = $this->quoteFactory->create(); $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); @@ -234,11 +251,21 @@ public function testUpdateWithMissedItemRequiredParameters(string $input, string quantity } } + errors { + message + code + } } } QUERY; - $this->expectExceptionMessage($message); - $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('errors', $response['updateCartItems']); + + $responseError = $response['updateCartItems']['errors'][0]; + $this->assertEquals($message, $responseError['message']); + $this->assertEquals($errorCode, $responseError['code']); } /** @@ -249,7 +276,8 @@ public static function dataProviderUpdateWithMissedRequiredParameters(): array return [ 'missed_cart_item_qty' => [ 'cart_items: [{ cart_item_id: 1 }]', - 'Required parameter "quantity" for "cart_items" is missing.' + 'Required parameter "quantity" for "cart_items" is missing.', + 'REQUIRED_PARAMETER_MISSING' ], ]; } @@ -279,6 +307,10 @@ private function getQuery(string $maskedQuoteId, int $itemId, float $quantity): quantity } } + errors { + message + code + } } } QUERY; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php index 558a323a6457d..7eb0eaf61d787 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php @@ -154,10 +154,18 @@ public function testUpdateNonExistentItem() $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); $notExistentItemId = 999; - $this->expectExceptionMessage("Could not find cart item with id: {$notExistentItemId}."); - $query = $this->getQuery($maskedQuoteId, $notExistentItemId, 2); - $this->graphQlMutation($query); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('errors', $response['updateCartItems']); + + $responseError = $response['updateCartItems']['errors'][0]; + $this->assertEquals( + "Could not find cart item with id: {$notExistentItemId}.", + $responseError['message'] + ); + $this->assertEquals('COULD_NOT_FIND_CART_ITEM', $responseError['code']); } /** @@ -180,10 +188,18 @@ public function testUpdateItemIfItemIsNotBelongToCart() ->getItemByProduct($this->productRepository->get('virtual-product')) ->getId(); - $this->expectExceptionMessage("Could not find cart item with id: {$secondQuoteItemId}."); - $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId, 2); - $this->graphQlMutation($query); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('errors', $response['updateCartItems']); + + $responseError = $response['updateCartItems']['errors'][0]; + $this->assertEquals( + "Could not find cart item with id: {$secondQuoteItemId}.", + $responseError['message'] + ); + $this->assertEquals('COULD_NOT_FIND_CART_ITEM', $responseError['code']); } /** @@ -205,10 +221,11 @@ public function testUpdateItemFromCustomerCart() /** * @param string $input * @param string $message + * @param string $errorCode * @dataProvider dataProviderUpdateWithMissedRequiredParameters * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php */ - public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message, string $errorCode) { $quote = $this->quoteFactory->create(); $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); @@ -226,11 +243,21 @@ public function testUpdateWithMissedItemRequiredParameters(string $input, string quantity } } + errors { + message + code + } } } QUERY; - $this->expectExceptionMessage($message); - $this->graphQlMutation($query); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('errors', $response['updateCartItems']); + + $responseError = $response['updateCartItems']['errors'][0]; + $this->assertEquals($message, $responseError['message']); + $this->assertEquals($errorCode, $responseError['code']); } /** @@ -241,7 +268,8 @@ public static function dataProviderUpdateWithMissedRequiredParameters(): array return [ 'missed_cart_item_qty' => [ 'cart_items: [{ cart_item_id: 1 }]', - 'Required parameter "quantity" for "cart_items" is missing.' + 'Required parameter "quantity" for "cart_items" is missing.', + 'REQUIRED_PARAMETER_MISSING' ], ]; } @@ -271,6 +299,10 @@ private function getQuery(string $maskedQuoteId, int $itemId, float $quantity): quantity } } + errors { + message + code + } } } QUERY; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php index 6c621852204a6..47059d56f7abf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/StockAvailabilityTest.php @@ -79,7 +79,7 @@ public function testStockStatusUnavailableSimpleProduct(): void $responseDataObject->getData('cart/itemsV2/items/0/is_available') ); self::assertEquals( - 'Not enough items for sale. Please adjust the quantity to continue', + 'Not enough items for sale', $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') ); } @@ -128,7 +128,7 @@ public function testStockStatusUnavailableSimpleProductOption1(): void ); self::assertEquals(10, $responseDataObject->getData('cart/itemsV2/items/0/product/only_x_left_in_stock')); self::assertEquals( - 'Only 10 available for sale. Please adjust the quantity to continue', + 'Only 10 of 20 available', $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') ); } @@ -197,7 +197,7 @@ public function testStockStatusUnavailableBundleProduct(): void $responseDataObject->getData('cart/itemsV2/items/0/is_available') ); self::assertEquals( - 'Not enough items for sale. Please adjust the quantity to continue', + 'Not enough items for sale', $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') ); } @@ -246,7 +246,7 @@ public function testStockStatusUnavailableBundleProductOption1(): void $responseDataObject->getData('cart/itemsV2/items/0/is_available') ); self::assertEquals( - 'Only 90 available for sale. Please adjust the quantity to continue', + 'Only 90 of 100 available', $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') ); } @@ -358,7 +358,7 @@ public function testStockStatusUnavailableConfigurableProduct(): void $responseDataObject->getData('cart/itemsV2/items/0/is_available') ); self::assertEquals( - 'Not enough items for sale. Please adjust the quantity to continue', + 'Not enough items for sale', $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') ); } @@ -400,7 +400,7 @@ public function testStockStatusUnavailableConfigurableProductOption1(): void self::assertEquals(90, $responseDataObject->getData('cart/itemsV2/items/0/product/only_x_left_in_stock')); self::assertEquals( - 'Only 90 available for sale. Please adjust the quantity to continue', + 'Only 90 of 100 available', $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') ); } @@ -548,7 +548,7 @@ public function testNotAvailableMessageOption1(): void self::assertEquals(90, $responseDataObject->getData('cart/itemsV2/items/0/product/only_x_left_in_stock')); self::assertEquals( - 'Only 90 available for sale. Please adjust the quantity to continue', + 'Only 90 of 100 available', $responseDataObject->getData('cart/itemsV2/items/0/not_available_message') ); }