diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php
index c317221fb6ef7..2ce51c8bbf19d 100644
--- a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php
+++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php
@@ -66,7 +66,7 @@ public function resolve(
array $args = null
) {
if (!isset($value['model'])) {
- throw new GraphQlInputException(__('"model" value should be specified'));
+ throw new GraphQlInputException(__('"model" value must be specified'));
}
$cart = $value['model'];
diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php
new file mode 100644
index 0000000000000..a9a8e682612cc
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php
@@ -0,0 +1,97 @@
+itemRepository = $itemRepository;
+ $this->giftMessageHelper = $giftMessageHelper;
+ }
+
+ /**
+ * Return information about Gift message for item cart
+ *
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ *
+ * @return array|Value|mixed
+ * @throws GraphQlInputException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new GraphQlInputException(__('"model" value must be specified'));
+ }
+
+ $quoteItem = $value['model'];
+
+ if (!$this->giftMessageHelper->isMessagesAllowed('items', $quoteItem)) {
+ return null;
+ }
+
+ if (!$this->giftMessageHelper->isMessagesAllowed('item', $quoteItem)) {
+ return null;
+ }
+
+ try {
+ $giftItemMessage = $this->itemRepository->get($quoteItem->getQuoteId(), $quoteItem->getItemId());
+ } catch (LocalizedException $e) {
+ throw new GraphQlInputException(__('Can\'t load cart item'));
+ }
+
+ if (!isset($giftItemMessage)) {
+ return null;
+ }
+
+ return [
+ 'to' => $giftItemMessage->getRecipient() ?? '',
+ 'from' => $giftItemMessage->getSender() ?? '',
+ 'message'=> $giftItemMessage->getMessage() ?? ''
+ ];
+ }
+}
diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md
index fa2e02116b66c..d73a058e0db9c 100644
--- a/app/code/Magento/GiftMessageGraphQl/README.md
+++ b/app/code/Magento/GiftMessageGraphQl/README.md
@@ -1,3 +1,3 @@
# GiftMessageGraphQl
-**GiftMessageGraphQl** provides information about gift messages for cart, cart items, order and order items.
+**GiftMessageGraphQl** provides information about gift messages for carts, cart items, orders and order items.
diff --git a/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml
new file mode 100644
index 0000000000000..bce5b7063e6b9
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ - sales/gift_options/allow_order
+ - sales/gift_options/allow_items
+
+
+
+
diff --git a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls
index f14c812a9a5f3..ad18054abca13 100644
--- a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls
@@ -1,20 +1,47 @@
# Copyright © Magento, Inc. All rights reserved.
# See COPYING.txt for license details.
+type StoreConfig {
+ allow_order : String @doc(description: "The value of the Allow Gift Messages on Order Level option")
+ allow_items : String @doc(description: "The value of the Allow Gift Messages for Order Items option")
+}
+
type Cart {
gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\GiftMessage") @doc(description: "The entered gift message for the cart")
}
-type SalesItemInterface {
- gift_message: GiftMessage @doc(description: "The entered gift message for the order item")
+type SimpleCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
}
-type CustomerOrder {
- gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order")
+type ConfigurableCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
+}
+
+type BundleCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
+}
+
+type GiftMessage @doc(description: "Contains the text of a gift message, its sender, and recipient") {
+ to: String! @doc(description: "Recipient name")
+ from: String! @doc(description: "Sender name")
+ message: String! @doc(description: "Gift message text")
+}
+
+input CartItemUpdateInput {
+ gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item")
}
-type GiftMessage {
- to: String! @doc(description: "Recepient name")
+input GiftMessageInput @doc(description: "Contains the text of a gift message, its sender, and recipient") {
+ to: String! @doc(description: "Recipient name")
from: String! @doc(description: "Sender name")
message: String! @doc(description: "Gift message text")
}
+
+type SalesItemInterface {
+ gift_message: GiftMessage @doc(description: "The entered gift message for the order item")
+}
+
+type CustomerOrder {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order")
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php
new file mode 100644
index 0000000000000..c2e94b215956e
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php
@@ -0,0 +1,157 @@
+cartItemRepository = $cartItemRepository;
+ $this->updateCartItem = $updateCartItem;
+ $this->itemRepository = $itemRepository;
+ $this->giftMessageHelper = $giftMessageHelper;
+ $this->giftMessageFactory = $giftMessageFactory;
+ }
+
+ /**
+ * Process cart items
+ *
+ * @param Quote $cart
+ * @param array $items
+ *
+ * @throws GraphQlInputException
+ * @throws LocalizedException
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ */
+ public function processCartItems(Quote $cart, array $items): void
+ {
+ foreach ($items as $item) {
+ if (empty($item['cart_item_id'])) {
+ throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.'));
+ }
+
+ $itemId = (int)$item['cart_item_id'];
+ $customizableOptions = $item['customizable_options'] ?? [];
+ $cartItem = $cart->getItemById($itemId);
+
+ if ($cartItem && $cartItem->getParentItemId()) {
+ throw new GraphQlInputException(__('Child items may not be updated.'));
+ }
+
+ if (count($customizableOptions) === 0 && !isset($item['quantity'])) {
+ throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.'));
+ }
+
+ $quantity = (float)$item['quantity'];
+
+ if ($quantity <= 0.0) {
+ $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId);
+ } else {
+ $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions);
+ }
+
+ if (!empty($item['gift_message'])) {
+ try {
+ if (!$this->giftMessageHelper->isMessagesAllowed('items', $cartItem)) {
+ continue;
+ }
+ if (!$this->giftMessageHelper->isMessagesAllowed('item', $cartItem)) {
+ continue;
+ }
+
+ /** @var MessageInterface $giftItemMessage */
+ $giftItemMessage = $this->itemRepository->get($cart->getEntityId(), $itemId);
+
+ if (empty($giftItemMessage)) {
+ /** @var MessageInterface $giftMessage */
+ $giftMessage = $this->giftMessageFactory->create();
+ $this->updateGiftMessageForItem($cart, $giftMessage, $item, $itemId);
+ continue;
+ }
+ } catch (LocalizedException $exception) {
+ throw new GraphQlInputException(__('Gift Message cannot be updated.'));
+ }
+
+ $this->updateGiftMessageForItem($cart, $giftItemMessage, $item, $itemId);
+ }
+ }
+ }
+
+ /**
+ * Update Gift Message for Quote item
+ *
+ * @param Quote $cart
+ * @param MessageInterface $giftItemMessage
+ * @param array $item
+ * @param int $itemId
+ *
+ * @throws GraphQlInputException
+ */
+ private function updateGiftMessageForItem(Quote $cart, MessageInterface $giftItemMessage, array $item, int $itemId)
+ {
+ try {
+ $giftItemMessage->setRecipient($item['gift_message']['to']);
+ $giftItemMessage->setSender($item['gift_message']['from']);
+ $giftItemMessage->setMessage($item['gift_message']['message']);
+ $this->itemRepository->save($cart->getEntityId(), $giftItemMessage, $itemId);
+ } catch (LocalizedException $exception) {
+ throw new GraphQlInputException(__('Gift Message cannot be updated'));
+ }
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
index fa90f08e4b553..005baaad0e1e5 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
@@ -14,53 +14,43 @@
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\Quote\Api\CartItemRepositoryInterface;
use Magento\Quote\Api\CartRepositoryInterface;
-use Magento\Quote\Model\Quote;
use Magento\QuoteGraphQl\Model\Cart\GetCartForUser;
-use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem;
+use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider;
/**
* @inheritdoc
*/
class UpdateCartItems implements ResolverInterface
{
- /**
- * @var UpdateCartItem
- */
- private $updateCartItem;
-
/**
* @var GetCartForUser
*/
private $getCartForUser;
/**
- * @var CartItemRepositoryInterface
+ * @var CartRepositoryInterface
*/
- private $cartItemRepository;
+ private $cartRepository;
/**
- * @var CartRepositoryInterface
+ * @var UpdateCartItemsProvider
*/
- private $cartRepository;
+ private $updateCartItems;
/**
- * @param GetCartForUser $getCartForUser
- * @param CartItemRepositoryInterface $cartItemRepository
- * @param UpdateCartItem $updateCartItem
+ * @param GetCartForUser $getCartForUser
* @param CartRepositoryInterface $cartRepository
+ * @param UpdateCartItemsProvider $updateCartItems
*/
public function __construct(
GetCartForUser $getCartForUser,
- CartItemRepositoryInterface $cartItemRepository,
- UpdateCartItem $updateCartItem,
- CartRepositoryInterface $cartRepository
+ CartRepositoryInterface $cartRepository,
+ UpdateCartItemsProvider $updateCartItems
) {
$this->getCartForUser = $getCartForUser;
- $this->cartItemRepository = $cartItemRepository;
- $this->updateCartItem = $updateCartItem;
$this->cartRepository = $cartRepository;
+ $this->updateCartItems = $updateCartItems;
}
/**
@@ -71,6 +61,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
if (empty($args['input']['cart_id'])) {
throw new GraphQlInputException(__('Required parameter "cart_id" is missing.'));
}
+
$maskedCartId = $args['input']['cart_id'];
if (empty($args['input']['cart_items'])
@@ -78,13 +69,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
) {
throw new GraphQlInputException(__('Required parameter "cart_items" is missing.'));
}
- $cartItems = $args['input']['cart_items'];
+ $cartItems = $args['input']['cart_items'];
$storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
$cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
try {
- $this->processCartItems($cart, $cartItems);
+ $this->updateCartItems->processCartItems($cart, $cartItems);
$this->cartRepository->save($cart);
} catch (NoSuchEntityException $e) {
throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e);
@@ -98,39 +89,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
],
];
}
-
- /**
- * Process cart items
- *
- * @param Quote $cart
- * @param array $items
- * @throws GraphQlInputException
- * @throws LocalizedException
- */
- private function processCartItems(Quote $cart, array $items): void
- {
- foreach ($items as $item) {
- if (empty($item['cart_item_id'])) {
- throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.'));
- }
- $itemId = (int)$item['cart_item_id'];
- $customizableOptions = $item['customizable_options'] ?? [];
-
- $cartItem = $cart->getItemById($itemId);
- if ($cartItem && $cartItem->getParentItemId()) {
- throw new GraphQlInputException(__('Child items may not be updated.'));
- }
-
- if (count($customizableOptions) === 0 && !isset($item['quantity'])) {
- throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.'));
- }
- $quantity = (float)$item['quantity'];
-
- if ($quantity <= 0.0) {
- $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId);
- } else {
- $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions);
- }
- }
- }
}
diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json
index 0652d39b5f426..25f089cf75a62 100644
--- a/app/code/Magento/QuoteGraphQl/composer.json
+++ b/app/code/Magento/QuoteGraphQl/composer.json
@@ -13,7 +13,8 @@
"magento/module-customer-graph-ql": "*",
"magento/module-sales": "*",
"magento/module-directory": "*",
- "magento/module-graph-ql": "*"
+ "magento/module-graph-ql": "*",
+ "magento/module-gift-message": "*"
},
"suggest": {
"magento/module-graph-ql-cache": "*"
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php
new file mode 100644
index 0000000000000..7acfb503a5ad0
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php
@@ -0,0 +1,164 @@
+productRepository = $productRepository;
+ $this->buyRequestBuilder = $buyRequestBuilder;
+ $this->wishlistResource = $wishlistResource;
+ }
+
+ /**
+ * Adding products to wishlist
+ *
+ * @param Wishlist $wishlist
+ * @param array $wishlistItems
+ *
+ * @return WishlistOutput
+ *
+ * @throws AlreadyExistsException
+ */
+ public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput
+ {
+ foreach ($wishlistItems as $wishlistItem) {
+ $this->addItemToWishlist($wishlist, $wishlistItem);
+ }
+
+ $wishlistOutput = $this->prepareOutput($wishlist);
+
+ if ($wishlist->isObjectNew() || count($wishlistOutput->getErrors()) !== count($wishlistItems)) {
+ $this->wishlistResource->save($wishlist);
+ }
+
+ return $wishlistOutput;
+ }
+
+ /**
+ * Add product item to wishlist
+ *
+ * @param Wishlist $wishlist
+ * @param WishlistItem $wishlistItem
+ *
+ * @return void
+ */
+ private function addItemToWishlist(Wishlist $wishlist, WishlistItem $wishlistItem): void
+ {
+ $sku = $wishlistItem->getParentSku() ?? $wishlistItem->getSku();
+
+ try {
+ $product = $this->productRepository->get($sku, false, null, true);
+ } catch (NoSuchEntityException $e) {
+ $this->addError(
+ __('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(),
+ self::ERROR_PRODUCT_NOT_FOUND
+ );
+
+ return;
+ }
+
+ try {
+ $options = $this->buyRequestBuilder->build($wishlistItem, (int) $product->getId());
+ $result = $wishlist->addNewItem($product, $options);
+
+ if (is_string($result)) {
+ $this->addError($result);
+ }
+ } catch (LocalizedException $exception) {
+ $this->addError($exception->getMessage());
+ } catch (\Throwable $e) {
+ $this->addError(
+ __(
+ 'Could not add the product with SKU "%sku" to the wishlist:: %message',
+ ['sku' => $sku, 'message' => $e->getMessage()]
+ )->render()
+ );
+ }
+ }
+
+ /**
+ * Add wishlist line item error
+ *
+ * @param string $message
+ * @param string|null $code
+ *
+ * @return void
+ */
+ private function addError(string $message, string $code = null): void
+ {
+ $this->errors[] = new Data\Error(
+ $message,
+ $code ?? self::ERROR_UNDEFINED
+ );
+ }
+
+ /**
+ * Prepare output
+ *
+ * @param Wishlist $wishlist
+ *
+ * @return WishlistOutput
+ */
+ private function prepareOutput(Wishlist $wishlist): WishlistOutput
+ {
+ $output = new WishlistOutput($wishlist, $this->errors);
+ $this->errors = [];
+
+ return $output;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php
new file mode 100644
index 0000000000000..1cfa316c3cd01
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php
@@ -0,0 +1,55 @@
+getSelectedOptions() as $optionData) {
+ $optionData = \explode('/', base64_decode($optionData->getId()));
+
+ if ($this->isProviderApplicable($optionData) === false) {
+ continue;
+ }
+
+ [, $optionId, $optionValueId, $optionQuantity] = $optionData;
+
+ $bundleOptionsData['bundle_option'][$optionId] = $optionValueId;
+ $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity;
+ }
+
+ return $bundleOptionsData;
+ }
+
+ /**
+ * Checks whether this provider is applicable for the current option
+ *
+ * @param array $optionData
+ *
+ * @return bool
+ */
+ private function isProviderApplicable(array $optionData): bool
+ {
+ return $optionData[0] === self::PROVIDER_OPTION_TYPE;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php
new file mode 100644
index 0000000000000..1f7ddce345b1c
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php
@@ -0,0 +1,63 @@
+dataObjectFactory = $dataObjectFactory;
+ $this->providers = $providers;
+ }
+
+ /**
+ * Build product buy request for adding to wishlist
+ *
+ * @param WishlistItem $wishlistItemData
+ * @param int|null $productId
+ *
+ * @return DataObject
+ */
+ public function build(WishlistItem $wishlistItemData, ?int $productId = null): DataObject
+ {
+ $requestData = [
+ [
+ 'qty' => $wishlistItemData->getQuantity(),
+ ]
+ ];
+
+ foreach ($this->providers as $provider) {
+ $requestData[] = $provider->execute($wishlistItemData, $productId);
+ }
+
+ return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]);
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php
new file mode 100644
index 0000000000000..fac45d7f86c7c
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php
@@ -0,0 +1,26 @@
+getSelectedOptions() as $optionData) {
+ $optionData = \explode('/', base64_decode($optionData->getId()));
+
+ if ($this->isProviderApplicable($optionData) === false) {
+ continue;
+ }
+
+ [, $optionId, $optionValue] = $optionData;
+
+ $customizableOptionsData[$optionId][] = $optionValue;
+ }
+
+ foreach ($wishlistItemData->getEnteredOptions() as $option) {
+ $optionData = \explode('/', base64_decode($option->getId()));
+
+ if ($this->isProviderApplicable($optionData) === false) {
+ continue;
+ }
+
+ [, $optionId] = $optionData;
+
+ $customizableOptionsData[$optionId][] = $option->getValue();
+ }
+
+ if (empty($customizableOptionsData)) {
+ return $customizableOptionsData;
+ }
+
+ $result = ['options' => $this->flattenOptionValues($customizableOptionsData)];
+
+ if ($productId) {
+ $result += ['product' => $productId];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Flatten option values for non-multiselect customizable options
+ *
+ * @param array $customizableOptionsData
+ *
+ * @return array
+ */
+ private function flattenOptionValues(array $customizableOptionsData): array
+ {
+ foreach ($customizableOptionsData as $optionId => $optionValue) {
+ if (count($optionValue) === 1) {
+ $customizableOptionsData[$optionId] = $optionValue[0];
+ }
+ }
+
+ return $customizableOptionsData;
+ }
+
+ /**
+ * Checks whether this provider is applicable for the current option
+ *
+ * @param array $optionData
+ * @return bool
+ */
+ private function isProviderApplicable(array $optionData): bool
+ {
+ return $optionData[0] === self::PROVIDER_OPTION_TYPE;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php
new file mode 100644
index 0000000000000..1ad1a0b64706a
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php
@@ -0,0 +1,54 @@
+getSelectedOptions() as $optionData) {
+ $optionData = \explode('/', base64_decode($optionData->getId()));
+
+ if ($this->isProviderApplicable($optionData) === false) {
+ continue;
+ }
+
+ [, $linkId] = $optionData;
+
+ $linksData[] = $linkId;
+ }
+
+ return $linksData ? ['links' => $linksData] : [];
+ }
+
+ /**
+ * Checks whether this provider is applicable for the current option
+ *
+ * @param array $optionData
+ *
+ * @return bool
+ */
+ private function isProviderApplicable(array $optionData): bool
+ {
+ return $optionData[0] === self::PROVIDER_OPTION_TYPE;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php
new file mode 100644
index 0000000000000..01e29bcf80c0b
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php
@@ -0,0 +1,64 @@
+getSelectedOptions() as $optionData) {
+ $optionData = \explode('/', base64_decode($optionData->getId()));
+
+ if ($this->isProviderApplicable($optionData) === false) {
+ continue;
+ }
+
+ [, $attributeId, $valueIndex] = $optionData;
+
+ $configurableData[$attributeId] = $valueIndex;
+ }
+
+ if (empty($configurableData)) {
+ return $configurableData;
+ }
+
+ $result = ['super_attribute' => $configurableData];
+
+ if ($productId) {
+ $result += ['product' => $productId];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Checks whether this provider is applicable for the current option
+ *
+ * @param array $optionData
+ *
+ * @return bool
+ */
+ private function isProviderApplicable(array $optionData): bool
+ {
+ return $optionData[0] === self::PROVIDER_OPTION_TYPE;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php
new file mode 100644
index 0000000000000..a11f631f1f5aa
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php
@@ -0,0 +1,64 @@
+getSelectedOptions() as $optionData) {
+ $optionData = \explode('/', base64_decode($optionData->getId()));
+
+ if ($this->isProviderApplicable($optionData) === false) {
+ continue;
+ }
+
+ [, $simpleProductId, $quantity] = $optionData;
+
+ $groupedData[$simpleProductId] = $quantity;
+ }
+
+ if (empty($groupedData)) {
+ return $groupedData;
+ }
+
+ $result = ['super_group' => $groupedData];
+
+ if ($productId) {
+ $result += ['product' => $productId];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Checks whether this provider is applicable for the current option
+ *
+ * @param array $optionData
+ *
+ * @return bool
+ */
+ private function isProviderApplicable(array $optionData): bool
+ {
+ return $optionData[0] === self::PROVIDER_OPTION_TYPE;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Config.php b/app/code/Magento/Wishlist/Model/Wishlist/Config.php
new file mode 100644
index 0000000000000..041e9f1ca0a21
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/Config.php
@@ -0,0 +1,46 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * Check whether the wishlist is enabled or not
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return $this->scopeConfig->isSetFlag(
+ self::XML_PATH_WISHLIST_ACTIVE,
+ ScopeInterface::SCOPE_STORES
+ );
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php
new file mode 100644
index 0000000000000..0d6b2a2302540
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php
@@ -0,0 +1,54 @@
+id = $id;
+ $this->value = $value;
+ }
+
+ /**
+ * Get entered option id
+ *
+ * @return string
+ */
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ /**
+ * Get entered option value
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php
new file mode 100644
index 0000000000000..cb8420169fa8a
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php
@@ -0,0 +1,54 @@
+message = $message;
+ $this->code = $code;
+ }
+
+ /**
+ * Get error message
+ *
+ * @return string
+ */
+ public function getMessage(): string
+ {
+ return $this->message;
+ }
+
+ /**
+ * Get error code
+ *
+ * @return string
+ */
+ public function getCode(): string
+ {
+ return $this->code;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php
new file mode 100644
index 0000000000000..129a61c0a2a6c
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php
@@ -0,0 +1,37 @@
+id = $id;
+ }
+
+ /**
+ * Get selected option id
+ *
+ * @return string
+ */
+ public function getId(): string
+ {
+ return $this->id;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php
new file mode 100644
index 0000000000000..236b7f1eee72d
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php
@@ -0,0 +1,146 @@
+quantity = $quantity;
+ $this->sku = $sku;
+ $this->parentSku = $parentSku;
+ $this->id = $id;
+ $this->description = $description;
+ $this->selectedOptions = $selectedOptions;
+ $this->enteredOptions = $enteredOptions;
+ }
+
+ /**
+ * Get wishlist item id
+ *
+ * @return int|null
+ */
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ /**
+ * Get wishlist item description
+ *
+ * @return string|null
+ */
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ /**
+ * Get sku
+ *
+ * @return string|null
+ */
+ public function getSku(): ?string
+ {
+ return $this->sku;
+ }
+
+ /**
+ * Get quantity
+ *
+ * @return float
+ */
+ public function getQuantity(): float
+ {
+ return $this->quantity;
+ }
+
+ /**
+ * Get parent sku
+ *
+ * @return string|null
+ */
+ public function getParentSku(): ?string
+ {
+ return $this->parentSku;
+ }
+
+ /**
+ * Get selected options
+ *
+ * @return SelectedOption[]|null
+ */
+ public function getSelectedOptions(): ?array
+ {
+ return $this->selectedOptions;
+ }
+
+ /**
+ * Get entered options
+ *
+ * @return EnteredOption[]|null
+ */
+ public function getEnteredOptions(): ?array
+ {
+ return $this->enteredOptions;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php
new file mode 100644
index 0000000000000..153e8451bae31
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php
@@ -0,0 +1,75 @@
+createSelectedOptions($data['selected_options']) : [],
+ isset($data['entered_options']) ? $this->createEnteredOptions($data['entered_options']) : []
+ );
+ }
+
+ /**
+ * Create array of Entered Options
+ *
+ * @param array $options
+ *
+ * @return EnteredOption[]
+ */
+ private function createEnteredOptions(array $options): array
+ {
+ return \array_map(
+ function (array $option) {
+ if (!isset($option['id'], $option['value'])) {
+ throw new InputException(
+ __('Required fields are not present EnteredOption.id, EnteredOption.value')
+ );
+ }
+ return new EnteredOption($option['id'], $option['value']);
+ },
+ $options
+ );
+ }
+
+ /**
+ * Create array of Selected Options
+ *
+ * @param string[] $options
+ *
+ * @return SelectedOption[]
+ */
+ private function createSelectedOptions(array $options): array
+ {
+ return \array_map(
+ function ($option) {
+ return new SelectedOption($option);
+ },
+ $options
+ );
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php
new file mode 100644
index 0000000000000..fc7db9ec910fb
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php
@@ -0,0 +1,56 @@
+wishlist = $wishlist;
+ $this->errors = $errors;
+ }
+
+ /**
+ * Get Wishlist
+ *
+ * @return Wishlist
+ */
+ public function getWishlist(): Wishlist
+ {
+ return $this->wishlist;
+ }
+
+ /**
+ * Get errors happened during adding products to wishlist
+ *
+ * @return Error[]
+ */
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php
new file mode 100644
index 0000000000000..d143830064752
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php
@@ -0,0 +1,133 @@
+wishlistItemFactory = $wishlistItemFactory;
+ $this->wishlistItemResource = $wishlistItemResource;
+ }
+
+ /**
+ * Removing items from wishlist
+ *
+ * @param Wishlist $wishlist
+ * @param array $wishlistItemsIds
+ *
+ * @return WishlistOutput
+ */
+ public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOutput
+ {
+ foreach ($wishlistItemsIds as $wishlistItemId) {
+ $this->removeItemFromWishlist((int) $wishlistItemId);
+ }
+
+ return $this->prepareOutput($wishlist);
+ }
+
+ /**
+ * Remove product item from wishlist
+ *
+ * @param int $wishlistItemId
+ *
+ * @return void
+ */
+ private function removeItemFromWishlist(int $wishlistItemId): void
+ {
+ try {
+ /** @var WishlistItem $wishlistItem */
+ $wishlistItem = $this->wishlistItemFactory->create();
+ $this->wishlistItemResource->load($wishlistItem, $wishlistItemId);
+ if (!$wishlistItem->getId()) {
+ $this->addError(
+ __('Could not find a wishlist item with ID "%id"', ['id' => $wishlistItemId])->render(),
+ self::ERROR_PRODUCT_NOT_FOUND
+ );
+ }
+
+ $this->wishlistItemResource->delete($wishlistItem);
+ } catch (\Exception $e) {
+ $this->addError(
+ __(
+ 'We can\'t delete the item with ID "%id" from the Wish List right now.',
+ ['id' => $wishlistItemId]
+ )->render()
+ );
+ }
+ }
+
+ /**
+ * Add wishlist line item error
+ *
+ * @param string $message
+ * @param string|null $code
+ *
+ * @return void
+ */
+ private function addError(string $message, string $code = null): void
+ {
+ $this->errors[] = new Data\Error(
+ $message,
+ $code ?? self::ERROR_UNDEFINED
+ );
+ }
+
+ /**
+ * Prepare output
+ *
+ * @param Wishlist $wishlist
+ *
+ * @return WishlistOutput
+ */
+ private function prepareOutput(Wishlist $wishlist): WishlistOutput
+ {
+ $output = new WishlistOutput($wishlist, $this->errors);
+ $this->errors = [];
+
+ return $output;
+ }
+}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php
new file mode 100644
index 0000000000000..4abcada138362
--- /dev/null
+++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php
@@ -0,0 +1,138 @@
+buyRequestBuilder = $buyRequestBuilder;
+ $this->wishlistItemFactory = $wishlistItemFactory;
+ $this->wishlistItemResource = $wishlistItemResource;
+ }
+
+ /**
+ * Adding products to wishlist
+ *
+ * @param Wishlist $wishlist
+ * @param array $wishlistItems
+ *
+ * @return WishlistOutput
+ */
+ public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput
+ {
+ foreach ($wishlistItems as $wishlistItem) {
+ $this->updateItemInWishlist($wishlist, $wishlistItem);
+ }
+
+ return $this->prepareOutput($wishlist);
+ }
+
+ /**
+ * Update product item in wishlist
+ *
+ * @param Wishlist $wishlist
+ * @param WishlistItemData $wishlistItemData
+ *
+ * @return void
+ */
+ private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wishlistItemData): void
+ {
+ try {
+ $options = $this->buyRequestBuilder->build($wishlistItemData);
+ /** @var WishlistItem $wishlistItem */
+ $wishlistItem = $this->wishlistItemFactory->create();
+ $this->wishlistItemResource->load($wishlistItem, $wishlistItemData->getId());
+ $wishlistItem->setDescription($wishlistItemData->getDescription());
+ $resultItem = $wishlist->updateItem($wishlistItem, $options);
+
+ if (is_string($resultItem)) {
+ $this->addError($resultItem);
+ }
+ } catch (LocalizedException $exception) {
+ $this->addError($exception->getMessage());
+ }
+ }
+
+ /**
+ * Add wishlist line item error
+ *
+ * @param string $message
+ * @param string|null $code
+ *
+ * @return void
+ */
+ private function addError(string $message, string $code = null): void
+ {
+ $this->errors[] = new Data\Error(
+ $message,
+ $code ?? self::ERROR_UNDEFINED
+ );
+ }
+
+ /**
+ * Prepare output
+ *
+ * @param Wishlist $wishlist
+ *
+ * @return WishlistOutput
+ */
+ private function prepareOutput(Wishlist $wishlist): WishlistOutput
+ {
+ $output = new WishlistOutput($wishlist, $this->errors);
+ $this->errors = [];
+
+ return $output;
+ }
+}
diff --git a/app/code/Magento/Wishlist/etc/graphql/di.xml b/app/code/Magento/Wishlist/etc/graphql/di.xml
new file mode 100644
index 0000000000000..9726376bf30be
--- /dev/null
+++ b/app/code/Magento/Wishlist/etc/graphql/di.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ - Magento\Wishlist\Model\Wishlist\BuyRequest\SuperAttributeDataProvider
+ - Magento\Wishlist\Model\Wishlist\BuyRequest\CustomizableOptionDataProvider
+ - Magento\Wishlist\Model\Wishlist\BuyRequest\BundleDataProvider
+ - Magento\Wishlist\Model\Wishlist\BuyRequest\DownloadableLinkDataProvider
+ - Magento\Wishlist\Model\Wishlist\BuyRequest\SuperGroupDataProvider
+
+
+
+
diff --git a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php
new file mode 100644
index 0000000000000..9cc1404613e41
--- /dev/null
+++ b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php
@@ -0,0 +1,35 @@
+ $wishlist->getId(),
+ 'sharing_code' => $wishlist->getSharingCode(),
+ 'updated_at' => $wishlist->getUpdatedAt(),
+ 'items_count' => $wishlist->getItemsCount(),
+ 'name' => $wishlist->getName(),
+ 'model' => $wishlist,
+ ];
+ }
+}
diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php
new file mode 100644
index 0000000000000..11c8446a72a9d
--- /dev/null
+++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php
@@ -0,0 +1,158 @@
+wishlistResource = $wishlistResource;
+ $this->wishlistFactory = $wishlistFactory;
+ $this->wishlistConfig = $wishlistConfig;
+ $this->addProductsToWishlist = $addProductsToWishlist;
+ $this->wishlistDataMapper = $wishlistDataMapper;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!$this->wishlistConfig->isEnabled()) {
+ throw new GraphQlInputException(__('The wishlist is not currently available.'));
+ }
+
+ $customerId = $context->getUserId();
+
+ /* Guest checking */
+ if (null === $customerId || 0 === $customerId) {
+ throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist'));
+ }
+
+ $wishlistId = ((int) $args['wishlistId']) ?: null;
+ $wishlist = $this->getWishlist($wishlistId, $customerId);
+
+ if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) {
+ throw new GraphQlInputException(__('The wishlist was not found.'));
+ }
+
+ $wishlistItems = $this->getWishlistItems($args['wishlistItems']);
+ $wishlistOutput = $this->addProductsToWishlist->execute($wishlist, $wishlistItems);
+
+ return [
+ 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()),
+ 'userInputErrors' => array_map(
+ function (Error $error) {
+ return [
+ 'code' => $error->getCode(),
+ 'message' => $error->getMessage(),
+ ];
+ },
+ $wishlistOutput->getErrors()
+ )
+ ];
+ }
+
+ /**
+ * Get wishlist items
+ *
+ * @param array $wishlistItemsData
+ *
+ * @return array
+ */
+ private function getWishlistItems(array $wishlistItemsData): array
+ {
+ $wishlistItems = [];
+
+ foreach ($wishlistItemsData as $wishlistItemData) {
+ $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData);
+ }
+
+ return $wishlistItems;
+ }
+
+ /**
+ * Get customer wishlist
+ *
+ * @param int|null $wishlistId
+ * @param int|null $customerId
+ *
+ * @return Wishlist
+ */
+ private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist
+ {
+ $wishlist = $this->wishlistFactory->create();
+
+ if ($wishlistId !== null && $wishlistId > 0) {
+ $this->wishlistResource->load($wishlist, $wishlistId);
+ } elseif ($customerId !== null) {
+ $wishlist->loadByCustomerId($customerId, true);
+ }
+
+ return $wishlist;
+ }
+}
diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php
index a84ce0e965b6d..cad574ef56ed2 100644
--- a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php
+++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php
@@ -9,9 +9,11 @@
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
+use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Wishlist\Model\Wishlist;
+use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig;
use Magento\Wishlist\Model\WishlistFactory;
/**
@@ -24,12 +26,21 @@ class CustomerWishlistResolver implements ResolverInterface
*/
private $wishlistFactory;
+ /**
+ * @var WishlistConfig
+ */
+ private $wishlistConfig;
+
/**
* @param WishlistFactory $wishlistFactory
+ * @param WishlistConfig $wishlistConfig
*/
- public function __construct(WishlistFactory $wishlistFactory)
- {
+ public function __construct(
+ WishlistFactory $wishlistFactory,
+ WishlistConfig $wishlistConfig
+ ) {
$this->wishlistFactory = $wishlistFactory;
+ $this->wishlistConfig = $wishlistConfig;
}
/**
@@ -42,6 +53,10 @@ public function resolve(
array $value = null,
array $args = null
) {
+ if (!$this->wishlistConfig->isEnabled()) {
+ throw new GraphQlInputException(__('The wishlist is not currently available.'));
+ }
+
if (false === $context->getExtensionAttributes()->getIsCustomer()) {
throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.'));
}
diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php
new file mode 100644
index 0000000000000..1c741361ea7b7
--- /dev/null
+++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php
@@ -0,0 +1,144 @@
+wishlistResource = $wishlistResource;
+ $this->wishlistConfig = $wishlistConfig;
+ $this->wishlistFactory = $wishlistFactory;
+ $this->wishlistDataMapper = $wishlistDataMapper;
+ $this->removeProductsFromWishlist = $removeProductsFromWishlist;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!$this->wishlistConfig->isEnabled()) {
+ throw new GraphQlInputException(__('The wishlist is not currently available.'));
+ }
+
+ $customerId = $context->getUserId();
+
+ /* Guest checking */
+ if ($customerId === null || 0 === $customerId) {
+ throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist'));
+ }
+
+ $wishlistId = ((int) $args['wishlistId']) ?: null;
+ $wishlist = $this->getWishlist($wishlistId, $customerId);
+
+ if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) {
+ throw new GraphQlInputException(__('The wishlist was not found.'));
+ }
+
+ $wishlistItemsIds = $args['wishlistItemsIds'];
+ $wishlistOutput = $this->removeProductsFromWishlist->execute($wishlist, $wishlistItemsIds);
+
+ if (!empty($wishlistItemsIds)) {
+ $this->wishlistResource->save($wishlist);
+ }
+
+ return [
+ 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()),
+ 'userInputErrors' => \array_map(
+ function (Error $error) {
+ return [
+ 'code' => $error->getCode(),
+ 'message' => $error->getMessage(),
+ ];
+ },
+ $wishlistOutput->getErrors()
+ )
+ ];
+ }
+
+ /**
+ * Get customer wishlist
+ *
+ * @param int|null $wishlistId
+ * @param int|null $customerId
+ *
+ * @return Wishlist
+ */
+ private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist
+ {
+ $wishlist = $this->wishlistFactory->create();
+
+ if ($wishlistId !== null && $wishlistId > 0) {
+ $this->wishlistResource->load($wishlist, $wishlistId);
+ } elseif ($customerId !== null) {
+ $wishlist->loadByCustomerId($customerId, true);
+ }
+
+ return $wishlist;
+ }
+}
diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php
new file mode 100644
index 0000000000000..50a56863596c0
--- /dev/null
+++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php
@@ -0,0 +1,163 @@
+wishlistResource = $wishlistResource;
+ $this->wishlistFactory = $wishlistFactory;
+ $this->wishlistConfig = $wishlistConfig;
+ $this->updateProductsInWishlist = $updateProductsInWishlist;
+ $this->wishlistDataMapper = $wishlistDataMapper;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!$this->wishlistConfig->isEnabled()) {
+ throw new GraphQlInputException(__('The wishlist is not currently available.'));
+ }
+
+ $customerId = $context->getUserId();
+
+ /* Guest checking */
+ if (null === $customerId || $customerId === 0) {
+ throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist'));
+ }
+
+ $wishlistId = ((int) $args['wishlistId']) ?: null;
+ $wishlist = $this->getWishlist($wishlistId, $customerId);
+
+ if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) {
+ throw new GraphQlInputException(__('The wishlist was not found.'));
+ }
+
+ $wishlistItems = $args['wishlistItems'];
+ $wishlistItems = $this->getWishlistItems($wishlistItems);
+ $wishlistOutput = $this->updateProductsInWishlist->execute($wishlist, $wishlistItems);
+
+ if (count($wishlistOutput->getErrors()) !== count($wishlistItems)) {
+ $this->wishlistResource->save($wishlist);
+ }
+
+ return [
+ 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()),
+ 'userInputErrors' => \array_map(
+ function (Error $error) {
+ return [
+ 'code' => $error->getCode(),
+ 'message' => $error->getMessage(),
+ ];
+ },
+ $wishlistOutput->getErrors()
+ )
+ ];
+ }
+
+ /**
+ * Get DTO wishlist items
+ *
+ * @param array $wishlistItemsData
+ *
+ * @return array
+ */
+ private function getWishlistItems(array $wishlistItemsData): array
+ {
+ $wishlistItems = [];
+
+ foreach ($wishlistItemsData as $wishlistItemData) {
+ $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData);
+ }
+
+ return $wishlistItems;
+ }
+
+ /**
+ * Get customer wishlist
+ *
+ * @param int|null $wishlistId
+ * @param int|null $customerId
+ *
+ * @return Wishlist
+ */
+ private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist
+ {
+ $wishlist = $this->wishlistFactory->create();
+
+ if (null !== $wishlistId && 0 < $wishlistId) {
+ $this->wishlistResource->load($wishlist, $wishlistId);
+ } elseif ($customerId !== null) {
+ $wishlist->loadByCustomerId($customerId, true);
+ }
+
+ return $wishlist;
+ }
+}
diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php
index 792928ab61aaf..09c0a8a935a6c 100644
--- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php
+++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php
@@ -8,10 +8,12 @@
namespace Magento\WishlistGraphQl\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
+use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel;
use Magento\Wishlist\Model\Wishlist;
+use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig;
use Magento\Wishlist\Model\WishlistFactory;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
@@ -30,14 +32,24 @@ class WishlistResolver implements ResolverInterface
*/
private $wishlistFactory;
+ /**
+ * @var WishlistConfig
+ */
+ private $wishlistConfig;
+
/**
* @param WishlistResourceModel $wishlistResource
* @param WishlistFactory $wishlistFactory
+ * @param WishlistConfig $wishlistConfig
*/
- public function __construct(WishlistResourceModel $wishlistResource, WishlistFactory $wishlistFactory)
- {
+ public function __construct(
+ WishlistResourceModel $wishlistResource,
+ WishlistFactory $wishlistFactory,
+ WishlistConfig $wishlistConfig
+ ) {
$this->wishlistResource = $wishlistResource;
$this->wishlistFactory = $wishlistFactory;
+ $this->wishlistConfig = $wishlistConfig;
}
/**
@@ -50,6 +62,10 @@ public function resolve(
array $value = null,
array $args = null
) {
+ if (!$this->wishlistConfig->isEnabled()) {
+ throw new GraphQlInputException(__('The wishlist is not currently available.'));
+ }
+
$customerId = $context->getUserId();
/* Guest checking */
diff --git a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php
index 8385d3ca852a4..017462b4c94c6 100644
--- a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php
+++ b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php
@@ -14,6 +14,7 @@
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\GraphQl\Model\Query\ContextExtensionInterface;
use Magento\Wishlist\Model\Wishlist;
+use Magento\Wishlist\Model\Wishlist\Config;
use Magento\Wishlist\Model\WishlistFactory;
use Magento\WishlistGraphQl\Model\Resolver\CustomerWishlistResolver;
use PHPUnit\Framework\MockObject\MockObject;
@@ -48,6 +49,11 @@ class CustomerWishlistResolverTest extends TestCase
*/
private $resolver;
+ /**
+ * @var Config|MockObject
+ */
+ private $wishlistConfigMock;
+
/**
* Build the Testing Environment
*/
@@ -74,9 +80,12 @@ protected function setUp(): void
->setMethods(['loadByCustomerId', 'getId', 'getSharingCode', 'getUpdatedAt', 'getItemsCount'])
->getMock();
+ $this->wishlistConfigMock = $this->createMock(Config::class);
+
$objectManager = new ObjectManager($this);
$this->resolver = $objectManager->getObject(CustomerWishlistResolver::class, [
- 'wishlistFactory' => $this->wishlistFactoryMock
+ 'wishlistFactory' => $this->wishlistFactoryMock,
+ 'wishlistConfig' => $this->wishlistConfigMock
]);
}
@@ -85,6 +94,8 @@ protected function setUp(): void
*/
public function testThrowExceptionWhenUserNotAuthorized(): void
{
+ $this->wishlistConfigMock->method('isEnabled')->willReturn(true);
+
// Given
$this->extensionAttributesMock->method('getIsCustomer')
->willReturn(false);
@@ -107,6 +118,8 @@ public function testThrowExceptionWhenUserNotAuthorized(): void
*/
public function testFactoryCreatesWishlistByAuthorizedCustomerId(): void
{
+ $this->wishlistConfigMock->method('isEnabled')->willReturn(true);
+
// Given
$this->extensionAttributesMock->method('getIsCustomer')
->willReturn(true);
diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls
index deaa66921ba7c..794e90ed9f9a9 100644
--- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls
@@ -6,7 +6,7 @@ type Query {
}
type Customer {
- wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "The wishlist query returns the contents of a customer's wish lists") @cache(cacheable: false)
+ wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "Contains the contents of a customer's wish lists") @cache(cacheable: false)
}
type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be used instead") {
@@ -32,3 +32,45 @@ type WishlistItem {
added_at: String @doc(description: "The time when the customer added the item to the wish list"),
product: ProductInterface @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\ProductResolver")
}
+
+type Mutation {
+ addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput @doc(description: "Adds one or more products to the specified wish list. This mutation supports all product types") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\AddProductsToWishlist")
+ removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput @doc(description: "Removes one or more products from the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\RemoveProductsFromWishlist")
+ updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput @doc(description: "Updates one or more products in the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\UpdateProductsInWishlist")
+}
+
+input WishlistItemInput @doc(description: "Defines the items to add to a wish list") {
+ sku: String @doc(description: "The SKU of the product to add. For complex product types, specify the child product SKU")
+ quantity: Float @doc(description: "The amount or number of items to add")
+ parent_sku: String @doc(description: "For complex product types, the SKU of the parent product")
+ selected_options: [String!] @doc(description: "An array of strings corresponding to options the customer selected")
+ entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered")
+}
+
+type AddProductsToWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") {
+ wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully added")
+ userInputErrors:[CheckoutUserInputError]! @doc(description: "An array of errors encountered while adding products to a wish list")
+}
+
+input EnteredOptionInput @doc(description: "Defines a customer-entered option") {
+ id: String! @doc(description: "A base64 encoded ID")
+ value: String! @doc(description: "Text the customer entered")
+}
+
+type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") {
+ wishlist: Wishlist! @doc(description: "Contains the wish list with after items were successfully deleted")
+ userInputErrors:[CheckoutUserInputError]! @doc(description:"An array of errors encountered while deleting products from a wish list")
+}
+
+input WishlistItemUpdateInput @doc(description: "Defines updates to items in a wish list") {
+ wishlist_item_id: ID @doc(description: "The ID of the wishlist item to update")
+ quantity: Float @doc(description: "The new amount or number of this item")
+ description: String @doc(description: "Describes the update")
+ selected_options: [String!] @doc(description: "An array of strings corresponding to options the customer selected")
+ entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered")
+}
+
+type UpdateProductsInWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") {
+ wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully updated")
+ userInputErrors:[CheckoutUserInputError]! @doc(description:"An array of errors encountered while updating products in a wish list")
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php
new file mode 100644
index 0000000000000..fa0909d556b3a
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php
@@ -0,0 +1,89 @@
+getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store sales/gift_options/allow_items 0
+ * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php
+ * @throws NoSuchEntityException
+ * @throws Exception
+ */
+ public function testGiftMessageCartForItemNotAllow()
+ {
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message');
+ foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) {
+ self::assertArrayHasKey('gift_message', $item);
+ self::assertNull($item['gift_message']);
+ }
+ }
+
+ /**
+ * @magentoConfigFixture default_store sales/gift_options/allow_items 1
+ * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php
+ * @throws NoSuchEntityException
+ * @throws Exception
+ */
+ public function testGiftMessageCartForItem()
+ {
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message');
+ foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) {
+ self::assertArrayHasKey('gift_message', $item);
+ self::assertArrayHasKey('to', $item['gift_message']);
+ self::assertArrayHasKey('from', $item['gift_message']);
+ self::assertArrayHasKey('message', $item['gift_message']);
+ }
+ }
+
+ /**
+ * @param string $quoteId
+ *
+ * @return array|bool|float|int|string
+ * @throws Exception
+ */
+ private function requestCartResult(string $quoteId)
+ {
+ $query = <<graphQlQuery($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 a17bc1aa3821a..0a22f3ca9721c 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
@@ -7,8 +7,9 @@
namespace Magento\GraphQl\Quote\Guest;
-use Magento\Quote\Model\QuoteFactory;
+use Exception;
use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Quote\Model\QuoteFactory;
use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface;
use Magento\Quote\Model\ResourceModel\Quote as QuoteResource;
use Magento\TestFramework\Helper\Bootstrap;
@@ -273,6 +274,81 @@ private function getCartQuery(string $maskedQuoteId)
}
}
}
+QUERY;
+ }
+
+ /**
+ * @magentoConfigFixture default_store sales/gift_options/allow_items 0
+ * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php
+ * @throws Exception
+ */
+ public function testUpdateGiftMessageCartForItemNotAllow()
+ {
+ $query = $this->getUpdateGiftMessageQuery();
+ foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) {
+ self::assertNull($item['gift_message']);
+ }
+ }
+
+ /**
+ * @magentoConfigFixture default_store sales/gift_options/allow_items 1
+ * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php
+ * @throws Exception
+ */
+ public function testUpdateGiftMessageCartForItem()
+ {
+ $query = $this->getUpdateGiftMessageQuery();
+ foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) {
+ self::assertArrayHasKey('gift_message', $item);
+ self::assertSame('Alex', $item['gift_message']['to']);
+ self::assertSame('Mike', $item['gift_message']['from']);
+ self::assertSame('Best regards.', $item['gift_message']['message']);
+ }
+ }
+
+ private function getUpdateGiftMessageQuery()
+ {
+ $quote = $this->quoteFactory->create();
+ $this->quoteResource->load($quote, 'test_guest_order_with_gift_message', 'reserved_order_id');
+ $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId());
+ $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId();
+
+ return <<customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class);
+ $this->wishlistFactory = $objectManager->get(WishlistFactory::class);
+ $this->productRepository = $objectManager->get(ProductRepositoryInterface::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store wishlist/general/active 1
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/Bundle/_files/product_1.php
+ *
+ * @throws Exception
+ */
+ public function testAddBundleProductWithOptions(): void
+ {
+ $sku = 'bundle-product';
+ $product = $this->productRepository->get($sku);
+ $customerId = 1;
+ $qty = 2;
+ $optionQty = 1;
+
+ /** @var Type $typeInstance */
+ $typeInstance = $product->getTypeInstance();
+ $typeInstance->setStoreFilter($product->getStoreId(), $product);
+ /** @var Option $option */
+ $option = $typeInstance->getOptionsCollection($product)->getFirstItem();
+ /** @var Product $selection */
+ $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem();
+ $optionId = $option->getId();
+ $selectionId = $selection->getSelectionId();
+ $bundleOptions = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, $optionQty);
+
+ $query = $this->getQuery($sku, $qty, $bundleOptions);
+ $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap());
+ $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true);
+ /** @var Item $item */
+ $item = $wishlist->getItemCollection()->getFirstItem();
+
+ $this->assertArrayHasKey('addProductsToWishlist', $response);
+ $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']);
+ $response = $response['addProductsToWishlist']['wishlist'];
+ $this->assertEquals($wishlist->getItemsCount(), $response['items_count']);
+ $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']);
+ $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']);
+ $this->assertEquals($item->getData('qty'), $response['items'][0]['qty']);
+ $this->assertEquals($item->getDescription(), $response['items'][0]['description']);
+ $this->assertEquals($item->getAddedAt(), $response['items'][0]['added_at']);
+ }
+
+ /**
+ * Authentication header map
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return array
+ *
+ * @throws AuthenticationException
+ */
+ private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array
+ {
+ $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password);
+
+ return ['Authorization' => 'Bearer ' . $customerToken];
+ }
+
+ /**
+ * Returns GraphQl mutation string
+ *
+ * @param string $sku
+ * @param int $qty
+ * @param string $bundleOptions
+ * @param int $wishlistId
+ *
+ * @return string
+ */
+ private function getQuery(
+ string $sku,
+ int $qty,
+ string $bundleOptions,
+ int $wishlistId = 0
+ ): string {
+ return <<customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class);
+ $this->wishlistFactory = $objectManager->get(WishlistFactory::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store wishlist/general/active 1
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ *
+ * @throws Exception
+ */
+ public function testAddDownloadableProductWithOptions(): void
+ {
+ $product = $this->getConfigurableProductInfo();
+ $customerId = 1;
+ $qty = 2;
+ $attributeId = (int) $product['configurable_options'][0]['attribute_id'];
+ $valueIndex = $product['configurable_options'][0]['values'][0]['value_index'];
+ $childSku = $product['variants'][0]['product']['sku'];
+ $parentSku = $product['sku'];
+ $selectedConfigurableOptionsQuery = $this->generateSuperAttributesIdV2Query($attributeId, $valueIndex);
+
+ $query = $this->getQuery($parentSku, $childSku, $qty, $selectedConfigurableOptionsQuery);
+
+ $response = $this->graphQlMutation($query, [], '', $this->getHeadersMap());
+ $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true);
+ /** @var Item $wishlistItem */
+ $wishlistItem = $wishlist->getItemCollection()->getFirstItem();
+
+ self::assertArrayHasKey('addProductsToWishlist', $response);
+ self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']);
+ $wishlistResponse = $response['addProductsToWishlist']['wishlist'];
+ self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']);
+ self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']);
+ self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']);
+ self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']);
+ self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']);
+ self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']);
+ self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']);
+ }
+
+ /**
+ * Authentication header map
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return array
+ *
+ * @throws AuthenticationException
+ */
+ private function getHeadersMap(string $username = 'customer@example.com', string $password = 'password'): array
+ {
+ $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password);
+
+ return ['Authorization' => 'Bearer ' . $customerToken];
+ }
+
+ /**
+ * Returns GraphQl mutation string
+ *
+ * @param string $parentSku
+ * @param string $childSku
+ * @param int $qty
+ * @param string $customizableOptions
+ * @param int $wishlistId
+ *
+ * @return string
+ */
+ private function getQuery(
+ string $parentSku,
+ string $childSku,
+ int $qty,
+ string $customizableOptions,
+ int $wishlistId = 0
+ ): string {
+ return <<graphQlQuery($this->getFetchProductQuery('configurable'));
+
+ return current($searchResponse['products']['items']);
+ }
+
+ /**
+ * Returns GraphQl query for fetching configurable product information
+ *
+ * @param string $term
+ *
+ * @return string
+ */
+ private function getFetchProductQuery(string $term): string
+ {
+ return <<objectManager = Bootstrap::getObjectManager();
+ $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class);
+ $this->wishlistFactory = $this->objectManager->get(WishlistFactory::class);
+ $this->getCustomOptionsWithIDV2ForQueryBySku =
+ $this->objectManager->get(GetCustomOptionsWithIDV2ForQueryBySku::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store wishlist/general/active 1
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php
+ */
+ public function testAddDownloadableProductWithOptions(): void
+ {
+ $customerId = 1;
+ $sku = 'downloadable-product-with-purchased-separately-links';
+ $qty = 2;
+ $links = $this->getProductsLinks($sku);
+ $linkId = key($links);
+ $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku);
+ $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId);
+ $productOptionsQuery = preg_replace(
+ '/"([^"]+)"\s*:\s*/',
+ '$1:',
+ json_encode($itemOptions)
+ );
+ $query = $this->getQuery($qty, $sku, trim($productOptionsQuery, '{}'));
+ $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap());
+ $wishlist = $this->wishlistFactory->create();
+ $wishlist->loadByCustomerId($customerId, true);
+ /** @var Item $wishlistItem */
+ $wishlistItem = $wishlist->getItemCollection()->getFirstItem();
+
+ self::assertArrayHasKey('addProductsToWishlist', $response);
+ self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']);
+ $wishlistResponse = $response['addProductsToWishlist']['wishlist'];
+ self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']);
+ self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']);
+ self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']);
+ self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']);
+ self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']);
+ self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']);
+ self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']);
+ }
+
+ /**
+ * @magentoConfigFixture default_store wishlist/general/active 0
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php
+ */
+ public function testAddDownloadableProductOnDisabledWishlist(): void
+ {
+ $qty = 2;
+ $sku = 'downloadable-product-with-purchased-separately-links';
+ $links = $this->getProductsLinks($sku);
+ $linkId = key($links);
+ $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku);
+ $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId);
+ $productOptionsQuery = trim(preg_replace(
+ '/"([^"]+)"\s*:\s*/',
+ '$1:',
+ json_encode($itemOptions)
+ ), '{}');
+ $query = $this->getQuery($qty, $sku, $productOptionsQuery);
+ $this->expectExceptionMessage('The wishlist is not currently available.');
+ $this->graphQlMutation($query, [], '', $this->getHeaderMap());
+ }
+
+ /**
+ * Function returns array of all product's links
+ *
+ * @param string $sku
+ *
+ * @return array
+ */
+ private function getProductsLinks(string $sku): array
+ {
+ $result = [];
+ /** @var ProductRepositoryInterface $productRepository */
+ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
+ $product = $productRepository->get($sku, false, null, true);
+
+ foreach ($product->getDownloadableLinks() as $linkObject) {
+ $result[$linkObject->getLinkId()] = [
+ 'title' => $linkObject->getTitle(),
+ 'price' => $linkObject->getPrice(),
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Authentication header map
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return array
+ *
+ * @throws AuthenticationException
+ */
+ private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array
+ {
+ $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password);
+
+ return ['Authorization' => 'Bearer ' . $customerToken];
+ }
+
+ /**
+ * Returns GraphQl mutation string
+ *
+ * @param int $qty
+ * @param string $sku
+ * @param string $customizableOptions
+ *
+ * @return string
+ */
+ private function getQuery(
+ int $qty,
+ string $sku,
+ string $customizableOptions
+ ): string {
+ return <<graphQlQuery($query);
}
+ /**
+ * @magentoConfigFixture default_store wishlist/general/active 0
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ */
+ public function testCustomerCannotGetWishlistWhenDisabled()
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('The wishlist is not currently available.');
+
+ $query =
+ <<graphQlQuery(
+ $query,
+ [],
+ '',
+ $this->getCustomerAuthHeaders('customer@example.com', 'password')
+ );
+ }
+
/**
* @param string $email
* @param string $password
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php
new file mode 100644
index 0000000000000..2e203e3ff4228
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php
@@ -0,0 +1,147 @@
+customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store wishlist/general/active 1
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php
+ */
+ public function testDeleteWishlistItemFromWishlist(): void
+ {
+ $wishlist = $this->getWishlist();
+ $wishlistId = $wishlist['customer']['wishlist']['id'];
+ $wishlist = $wishlist['customer']['wishlist'];
+ $wishlistItems = $wishlist['items'];
+ self::assertEquals(1, $wishlist['items_count']);
+
+ $query = $this->getQuery((int) $wishlistId, (int) $wishlistItems[0]['id']);
+ $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap());
+
+ self::assertArrayHasKey('removeProductsFromWishlist', $response);
+ self::assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']);
+ $wishlistResponse = $response['removeProductsFromWishlist']['wishlist'];
+ self::assertEquals(0, $wishlistResponse['items_count']);
+ self::assertEmpty($wishlistResponse['items']);
+ }
+
+ /**
+ * Authentication header map
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return array
+ *
+ * @throws AuthenticationException
+ */
+ private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array
+ {
+ $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password);
+
+ return ['Authorization' => 'Bearer ' . $customerToken];
+ }
+
+ /**
+ * Returns GraphQl mutation string
+ *
+ * @param int $wishlistId
+ * @param int $wishlistItemId
+ *
+ * @return string
+ */
+ private function getQuery(
+ int $wishlistId,
+ int $wishlistItemId
+ ): string {
+ return <<graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap());
+ }
+
+ /**
+ * Get customer wishlist query
+ *
+ * @return string
+ */
+ private function getCustomerWishlistQuery(): string
+ {
+ return <<productCustomOptionRepository = $productCustomOptionRepository;
+ }
+
+ /**
+ * Returns array of custom options for the product
+ *
+ * @param string $sku
+ *
+ * @return array
+ */
+ public function execute(string $sku): array
+ {
+ $customOptions = $this->productCustomOptionRepository->getList($sku);
+ $selectedOptions = [];
+ $enteredOptions = [];
+
+ foreach ($customOptions as $customOption) {
+ $optionType = $customOption->getType();
+
+ if ($optionType === 'field' || $optionType === 'area' || $optionType === 'date') {
+ $enteredOptions[] = [
+ 'id' => $this->encodeEnteredOption((int)$customOption->getOptionId()),
+ 'value' => '2012-12-12'
+ ];
+ } elseif ($optionType === 'drop_down') {
+ $optionSelectValues = $customOption->getValues();
+ $selectedOptions[] = $this->encodeSelectedOption(
+ (int)$customOption->getOptionId(),
+ (int)reset($optionSelectValues)->getOptionTypeId()
+ );
+ } elseif ($optionType === 'multiple') {
+ foreach ($customOption->getValues() as $optionValue) {
+ $selectedOptions[] = $this->encodeSelectedOption(
+ (int)$customOption->getOptionId(),
+ (int)$optionValue->getOptionTypeId()
+ );
+ }
+ }
+ }
+
+ return [
+ 'selected_options' => $selectedOptions,
+ 'entered_options' => $enteredOptions
+ ];
+ }
+
+ /**
+ * Returns id_v2 of the selected custom option
+ *
+ * @param int $optionId
+ * @param int $optionValueId
+ *
+ * @return string
+ */
+ private function encodeSelectedOption(int $optionId, int $optionValueId): string
+ {
+ return base64_encode("custom-option/$optionId/$optionValueId");
+ }
+
+ /**
+ * Returns id_v2 of the entered custom option
+ *
+ * @param int $optionId
+ *
+ * @return string
+ */
+ private function encodeEnteredOption(int $optionId): string
+ {
+ return base64_encode("custom-option/$optionId");
+ }
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php
new file mode 100644
index 0000000000000..9e96bdc5d7079
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php
@@ -0,0 +1,159 @@
+customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store wishlist/general/active 1
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php
+ */
+ public function testUpdateSimpleProductFromWishlist(): void
+ {
+ $wishlist = $this->getWishlist();
+ $qty = 5;
+ $description = 'New Description';
+ $wishlistId = $wishlist['customer']['wishlist']['id'];
+ $wishlistItem = $wishlist['customer']['wishlist']['items'][0];
+ self::assertNotEquals($description, $wishlistItem['description']);
+ self::assertNotEquals($qty, $wishlistItem['qty']);
+
+ $query = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description);
+ $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap());
+
+ self::assertArrayHasKey('updateProductsInWishlist', $response);
+ self::assertArrayHasKey('wishlist', $response['updateProductsInWishlist']);
+ $wishlistResponse = $response['updateProductsInWishlist']['wishlist'];
+ self::assertEquals($qty, $wishlistResponse['items'][0]['qty']);
+ self::assertEquals($description, $wishlistResponse['items'][0]['description']);
+ }
+
+ /**
+ * Authentication header map
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return array
+ *
+ * @throws AuthenticationException
+ */
+ private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array
+ {
+ $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password);
+
+ return ['Authorization' => 'Bearer ' . $customerToken];
+ }
+
+ /**
+ * Returns GraphQl mutation string
+ *
+ * @param int $wishlistId
+ * @param int $wishlistItemId
+ * @param int $qty
+ * @param string $description
+ *
+ * @return string
+ */
+ private function getQuery(
+ int $wishlistId,
+ int $wishlistItemId,
+ int $qty,
+ string $description
+ ): string {
+ return <<graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap());
+ }
+
+ /**
+ * Get customer wishlist query
+ *
+ * @return string
+ */
+ private function getCustomerWishlistQuery(): string
+ {
+ return <<requireDataFixture('Magento/Catalog/_files/products.php');
+
+/** @var ObjectManagerInterface $objectManager */
+$objectManager = Bootstrap::getObjectManager();
+
+/** @var QuoteResource $quote */
+$quote = $objectManager->create(QuoteResource::class);
+
+/** @var Quote $quoteModel */
+$quoteModel = $objectManager->create(Quote::class);
+$quoteModel->setData(['store_id' => 1, 'is_active' => 1, 'is_multi_shipping' => 0]);
+$quote->save($quoteModel);
+
+/** @var ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->create(ProductRepositoryInterface::class);
+$product = $productRepository->get('simple');
+
+$quoteModel->setReservedOrderId('test_guest_order_with_gift_message')
+ ->addProduct($product, 1);
+$quoteModel->collectTotals();
+$quote->save($quoteModel);
+
+/** @var MessageResource $message */
+$message = $objectManager->create(MessageResource::class);
+
+/** @var Message $message */
+$messageModel = $objectManager->create(Message::class);
+
+$messageModel->setSender('John Doe');
+$messageModel->setRecipient('Jane Roe');
+$messageModel->setMessage('Gift Message Text');
+$message->save($messageModel);
+
+$quoteModel->getItemByProduct($product)->setGiftMessageId($messageModel->getId());
+$quote->save($quoteModel);
+
+/** @var QuoteIdMaskResource $quoteIdMask */
+$quoteIdMask = Bootstrap::getObjectManager()
+ ->create(QuoteIdMaskFactory::class)
+ ->create();
+
+/** @var QuoteIdMask $quoteIdMaskModel */
+$quoteIdMaskModel = $objectManager->create(QuoteIdMask::class);
+
+$quoteIdMaskModel->setQuoteId($quoteModel->getId());
+$quoteIdMaskModel->setDataChanges(true);
+$quoteIdMask->save($quoteIdMaskModel);
diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php
new file mode 100644
index 0000000000000..9c215cb432b45
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php
@@ -0,0 +1,32 @@
+get(Registry::class);
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+$objectManager = Bootstrap::getObjectManager();
+$quote = $objectManager->create(Quote::class);
+$quote->load('test_guest_order_with_gift_message', 'reserved_order_id');
+$message = $objectManager->create(Message::class);
+$product = $objectManager->create(Product::class);
+foreach ($quote->getAllItems() as $item) {
+ $message->load($item->getGiftMessageId());
+ $message->delete();
+ $sku = $item->getSku();
+ $product->load($product->getIdBySku($sku));
+ if ($product->getId()) {
+ $product->delete();
+ }
+}
+$quote->delete();
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);