diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php
new file mode 100644
index 000000000000..b9fd5c7807d2
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php
@@ -0,0 +1,98 @@
+shippingAddressManagement = $shippingAddressManagement;
+ $this->addressRepository = $addressRepository;
+ $this->addressModel = $addressModel;
+ $this->checkCustomerAccount = $checkCustomerAccount;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddresses): void
+ {
+ if (count($shippingAddresses) > 1) {
+ throw new GraphQlInputException(
+ __('You cannot specify multiple shipping addresses.')
+ );
+ }
+ $shippingAddress = current($shippingAddresses);
+ $customerAddressId = $shippingAddress['customer_address_id'] ?? null;
+ $addressInput = $shippingAddress['address'] ?? null;
+
+ if (null === $customerAddressId && null === $addressInput) {
+ throw new GraphQlInputException(
+ __('The shipping address must contain either "customer_address_id" or "address".')
+ );
+ }
+ if ($customerAddressId && $addressInput) {
+ throw new GraphQlInputException(
+ __('The shipping address cannot contain "customer_address_id" and "address" at the same time.')
+ );
+ }
+ if (null === $customerAddressId) {
+ $shippingAddress = $this->addressModel->addData($addressInput);
+ } else {
+ $this->checkCustomerAccount->execute($context->getUserId(), $context->getUserType());
+
+ /** @var AddressInterface $customerAddress */
+ $customerAddress = $this->addressRepository->getById($customerAddressId);
+ $shippingAddress = $this->addressModel->importCustomerAddressData($customerAddress);
+ }
+
+ $this->shippingAddressManagement->assign($cart->getId(), $shippingAddress);
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php
new file mode 100644
index 000000000000..c5da3db75add
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php
@@ -0,0 +1,32 @@
+maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId;
+ $this->shippingAddressManagement = $shippingAddressManagement;
+ $this->getCartForUser = $getCartForUser;
+ $this->arrayManager = $arrayManager;
+ $this->setShippingAddressesOnCart = $setShippingAddressesOnCart;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
+ {
+ $shippingAddresses = $this->arrayManager->get('input/shipping_addresses', $args);
+ $maskedCartId = $this->arrayManager->get('input/cart_id', $args);
+
+ if (!$maskedCartId) {
+ throw new GraphQlInputException(__('Required parameter "cart_id" is missing'));
+ }
+ if (!$shippingAddresses) {
+ throw new GraphQlInputException(__('Required parameter "shipping_addresses" is missing'));
+ }
+
+ $maskedCartId = $args['input']['cart_id'];
+ $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId());
+
+ $this->setShippingAddressesOnCart->execute($context, $cart, $shippingAddresses);
+
+ return [
+ 'cart' => [
+ 'cart_id' => $maskedCartId,
+ 'model' => $cart,
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json
index f0d2eea9bcaf..1bf4d581a5fe 100644
--- a/app/code/Magento/QuoteGraphQl/composer.json
+++ b/app/code/Magento/QuoteGraphQl/composer.json
@@ -8,7 +8,9 @@
"magento/module-quote": "*",
"magento/module-checkout": "*",
"magento/module-catalog": "*",
- "magento/module-store": "*"
+ "magento/module-store": "*",
+ "magento/module-customer": "*",
+ "magento/module-customer-graph-ql": "*"
},
"suggest": {
"magento/module-graph-ql": "*"
diff --git a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml
new file mode 100644
index 000000000000..86bc954ae4ac
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
index a6c56318d7a9..edc643973ce7 100644
--- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
@@ -6,10 +6,12 @@ type Query {
}
type Mutation {
- createEmptyCart: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Creates empty shopping cart for guest or logged in user")
+ createEmptyCart: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Creates an empty shopping cart for a guest or logged in user")
+ applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Coupon\\ApplyCouponToCart")
+ removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Coupon\\RemoveCouponFromCart")
+ setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingAddressesOnCart")
applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ApplyCouponToCart")
removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveCouponFromCart")
- setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput
setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput
setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingMethodsOnCart")
addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart")
@@ -17,6 +19,10 @@ type Mutation {
input SetShippingAddressesOnCartInput {
cart_id: String!
+ shipping_addresses: [ShippingAddressInput!]!
+}
+
+input ShippingAddressInput {
customer_address_id: Int # Can be provided in one-page checkout and is required for multi-shipping checkout
address: CartAddressInput
cart_items: [CartItemQuantityInput!]
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php
new file mode 100644
index 000000000000..a023d37895c2
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php
@@ -0,0 +1,469 @@
+quoteResource = $objectManager->create(QuoteResource::class);
+ $this->quote = $objectManager->create(Quote::class);
+ $this->quoteIdToMaskedId = $objectManager->create(QuoteIdToMaskedQuoteIdInterface::class);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php
+ */
+ public function testSetNewGuestShippingAddressOnCart()
+ {
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId());
+
+ $query = <<graphQlQuery($query);
+
+ self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']);
+ $cartResponse = $response['setShippingAddressesOnCart']['cart'];
+ self::assertArrayHasKey('addresses', $cartResponse);
+ $shippingAddressResponse = current($cartResponse['addresses']);
+ $this->assertNewShippingAddressFields($shippingAddressResponse);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php
+ */
+ public function testSetSavedShippingAddressOnCartByGuest()
+ {
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId());
+
+ $query = <<graphQlQuery($query);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php
+ */
+ public function testSetMultipleShippingAddressesOnCartByGuest()
+ {
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId());
+
+ $query = <<graphQlQuery($query);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php
+ */
+ public function testSetSavedAndNewShippingAddressOnCartAtTheSameTime()
+ {
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId());
+
+ $query = <<graphQlQuery($query);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php
+ */
+ public function testSetShippingAddressOnCartWithNoAddresses()
+ {
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId());
+
+ $query = <<graphQlQuery($query);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ */
+ public function testSetNewRegisteredCustomerShippingAddressOnCart()
+ {
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId());
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $this->quote->setCustomerId(1);
+ $this->quoteResource->save($this->quote);
+
+ $headerMap = $this->getHeaderMap();
+
+ $query = <<graphQlQuery($query, [], '', $headerMap);
+
+ self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']);
+ $cartResponse = $response['setShippingAddressesOnCart']['cart'];
+ self::assertArrayHasKey('addresses', $cartResponse);
+ $shippingAddressResponse = current($cartResponse['addresses']);
+ $this->assertNewShippingAddressFields($shippingAddressResponse);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php
+ */
+ public function testSetSavedRegisteredCustomerShippingAddressOnCart()
+ {
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId());
+ $this->quoteResource->load(
+ $this->quote,
+ 'test_order_with_simple_product_without_address',
+ 'reserved_order_id'
+ );
+ $this->quote->setCustomerId(1);
+ $this->quoteResource->save($this->quote);
+
+ $headerMap = $this->getHeaderMap();
+
+ $query = <<graphQlQuery($query, [], '', $headerMap);
+
+ self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']);
+ $cartResponse = $response['setShippingAddressesOnCart']['cart'];
+ self::assertArrayHasKey('addresses', $cartResponse);
+ $shippingAddressResponse = current($cartResponse['addresses']);
+ $this->assertSavedShippingAddressFields($shippingAddressResponse);
+ }
+
+ /**
+ * Verify the all the whitelisted fields for a New Address Object
+ *
+ * @param array $shippingAddressResponse
+ */
+ private function assertNewShippingAddressFields(array $shippingAddressResponse): void
+ {
+ $assertionMap = [
+ ['response_field' => 'firstname', 'expected_value' => 'test firstname'],
+ ['response_field' => 'lastname', 'expected_value' => 'test lastname'],
+ ['response_field' => 'company', 'expected_value' => 'test company'],
+ ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']],
+ ['response_field' => 'city', 'expected_value' => 'test city'],
+ ['response_field' => 'postcode', 'expected_value' => '887766'],
+ ['response_field' => 'telephone', 'expected_value' => '88776655']
+ ];
+
+ $this->assertResponseFields($shippingAddressResponse, $assertionMap);
+ }
+
+ /**
+ * Verify the all the whitelisted fields for a Address Object
+ *
+ * @param array $shippingAddressResponse
+ */
+ private function assertSavedShippingAddressFields(array $shippingAddressResponse): void
+ {
+ $assertionMap = [
+ ['response_field' => 'firstname', 'expected_value' => 'John'],
+ ['response_field' => 'lastname', 'expected_value' => 'Smith'],
+ ['response_field' => 'company', 'expected_value' => 'CompanyName'],
+ ['response_field' => 'street', 'expected_value' => [0 => 'Green str, 67']],
+ ['response_field' => 'city', 'expected_value' => 'CityM'],
+ ['response_field' => 'postcode', 'expected_value' => '75477'],
+ ['response_field' => 'telephone', 'expected_value' => '3468676']
+ ];
+
+ $this->assertResponseFields($shippingAddressResponse, $assertionMap);
+ }
+
+ /**
+ * @param string $username
+ * @return array
+ */
+ private function getHeaderMap(string $username = 'customer@example.com'): array
+ {
+ $password = 'password';
+ /** @var CustomerTokenServiceInterface $customerTokenService */
+ $customerTokenService = ObjectManager::getInstance()
+ ->get(CustomerTokenServiceInterface::class);
+ $customerToken = $customerTokenService->createCustomerAccessToken($username, $password);
+ $headerMap = ['Authorization' => 'Bearer ' . $customerToken];
+ return $headerMap;
+ }
+}