Skip to content

Commit 5b5a08e

Browse files
committed
Merge remote-tracking branch 'origin/MC-22181' into pr-2.3.4
2 parents 6f7733e + 70bb245 commit 5b5a08e

File tree

4 files changed

+469
-0
lines changed

4 files changed

+469
-0
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\QuoteGraphQl\Model\Resolver;
9+
10+
use Magento\Framework\GraphQl\Config\Element\Field;
11+
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
12+
use Magento\Framework\GraphQl\Query\ResolverInterface;
13+
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
14+
use Magento\QuoteGraphQl\Model\Cart\GetCartForUser;
15+
use Magento\Quote\Api\CartRepositoryInterface;
16+
use Magento\GraphQl\Model\Query\ContextInterface;
17+
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
18+
19+
/**
20+
* Merge Carts Resolver
21+
*/
22+
class MergeCarts implements ResolverInterface
23+
{
24+
/**
25+
* @var GetCartForUser
26+
*/
27+
private $getCartForUser;
28+
29+
/**
30+
* @var CartRepositoryInterface
31+
*/
32+
private $cartRepository;
33+
34+
/**
35+
* @param GetCartForUser $getCartForUser
36+
* @param CartRepositoryInterface $cartRepository
37+
*/
38+
public function __construct(
39+
GetCartForUser $getCartForUser,
40+
CartRepositoryInterface $cartRepository
41+
) {
42+
$this->getCartForUser = $getCartForUser;
43+
$this->cartRepository = $cartRepository;
44+
}
45+
46+
/**
47+
* @inheritdoc
48+
*/
49+
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
50+
{
51+
if (empty($args['source_cart_id'])) {
52+
throw new GraphQlInputException(__('Required parameter "source_cart_id" is missing'));
53+
}
54+
55+
if (empty($args['destination_cart_id'])) {
56+
throw new GraphQlInputException(__('Required parameter "destination_cart_id" is missing'));
57+
}
58+
59+
/** @var ContextInterface $context */
60+
if (false === $context->getExtensionAttributes()->getIsCustomer()) {
61+
throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.'));
62+
}
63+
64+
$guestMaskedCartId = $args['source_cart_id'];
65+
$customerMaskedCartId = $args['destination_cart_id'];
66+
67+
$currentUserId = $context->getUserId();
68+
$storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
69+
// passing customerId as null enforces source cart should always be a guestcart
70+
$guestCart = $this->getCartForUser->execute($guestMaskedCartId, null, $storeId);
71+
$customerCart = $this->getCartForUser->execute($customerMaskedCartId, $currentUserId, $storeId);
72+
$customerCart->merge($guestCart);
73+
$guestCart->setIsActive(false);
74+
$this->cartRepository->save($customerCart);
75+
$this->cartRepository->save($guestCart);
76+
return [
77+
'model' => $customerCart,
78+
];
79+
}
80+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Mutation {
2020
setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart")
2121
setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetGuestEmailOnCart")
2222
setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @deprecated(reason: "Should use setPaymentMethodOnCart and placeOrder mutations in single request.") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder")
23+
mergeCarts(source_cart_id: String!, destination_cart_id: String!): Cart! @doc(description:"Merges the source cart into the destination cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\MergeCarts")
2324
placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder")
2425
}
2526

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\GraphQl\Quote\Customer;
9+
10+
use Magento\Quote\Model\QuoteFactory;
11+
use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface;
12+
use Magento\Quote\Model\ResourceModel\Quote as QuoteResource;
13+
use Magento\TestFramework\Helper\Bootstrap;
14+
use Magento\TestFramework\TestCase\GraphQlAbstract;
15+
use Magento\Integration\Api\CustomerTokenServiceInterface;
16+
17+
/**
18+
* Test for merging customer carts
19+
*/
20+
class MergeCartsTest extends GraphQlAbstract
21+
{
22+
/**
23+
* @var QuoteResource
24+
*/
25+
private $quoteResource;
26+
27+
/**
28+
* @var QuoteFactory
29+
*/
30+
private $quoteFactory;
31+
32+
/**
33+
* @var QuoteIdToMaskedQuoteIdInterface
34+
*/
35+
private $quoteIdToMaskedId;
36+
37+
/**
38+
* @var CustomerTokenServiceInterface
39+
*/
40+
private $customerTokenService;
41+
42+
protected function setUp()
43+
{
44+
$objectManager = Bootstrap::getObjectManager();
45+
$this->quoteResource = $objectManager->get(QuoteResource::class);
46+
$this->quoteFactory = $objectManager->get(QuoteFactory::class);
47+
$this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class);
48+
$this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class);
49+
}
50+
51+
protected function tearDown()
52+
{
53+
$quote = $this->quoteFactory->create();
54+
$this->quoteResource->load($quote, '1', 'customer_id');
55+
$this->quoteResource->delete($quote);
56+
}
57+
58+
/**
59+
* @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php
60+
* @magentoApiDataFixture Magento/Customer/_files/customer.php
61+
* @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
62+
* @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php
63+
* @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
64+
*/
65+
public function testMergeGuestWithCustomerCart()
66+
{
67+
$customerQuote = $this->quoteFactory->create();
68+
$this->quoteResource->load($customerQuote, 'test_quote', 'reserved_order_id');
69+
70+
$guestQuote = $this->quoteFactory->create();
71+
$this->quoteResource->load(
72+
$guestQuote,
73+
'test_order_with_virtual_product_without_address',
74+
'reserved_order_id'
75+
);
76+
77+
$customerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$customerQuote->getId());
78+
$guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestQuote->getId());
79+
80+
$query = $this->getCartMergeMutation($guestQuoteMaskedId, $customerQuoteMaskedId);
81+
$mergeResponse = $this->graphQlMutation($query, [], '', $this->getHeaderMap());
82+
self::assertArrayHasKey('mergeCarts', $mergeResponse);
83+
$cartResponse = $mergeResponse['mergeCarts'];
84+
self::assertArrayHasKey('items', $cartResponse);
85+
self::assertCount(2, $cartResponse['items']);
86+
$cartResponse = $this->graphQlMutation(
87+
$this->getCartQuery($customerQuoteMaskedId),
88+
[],
89+
'',
90+
$this->getHeaderMap()
91+
);
92+
93+
self::assertArrayHasKey('cart', $cartResponse);
94+
self::assertArrayHasKey('items', $cartResponse['cart']);
95+
self::assertCount(2, $cartResponse['cart']['items']);
96+
$item1 = $cartResponse['cart']['items'][0];
97+
self::assertArrayHasKey('quantity', $item1);
98+
self::assertEquals(2, $item1['quantity']);
99+
$item2 = $cartResponse['cart']['items'][1];
100+
self::assertArrayHasKey('quantity', $item2);
101+
self::assertEquals(1, $item2['quantity']);
102+
}
103+
104+
/**
105+
* @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php
106+
* @magentoApiDataFixture Magento/Customer/_files/customer.php
107+
* @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
108+
* @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php
109+
* @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
110+
* @expectedException \Exception
111+
* @expectedExceptionMessage Current user does not have an active cart.
112+
*/
113+
public function testGuestCartExpiryAfterMerge()
114+
{
115+
$customerQuote = $this->quoteFactory->create();
116+
$this->quoteResource->load($customerQuote, 'test_quote', 'reserved_order_id');
117+
118+
$guestQuote = $this->quoteFactory->create();
119+
$this->quoteResource->load(
120+
$guestQuote,
121+
'test_order_with_virtual_product_without_address',
122+
'reserved_order_id'
123+
);
124+
125+
$customerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$customerQuote->getId());
126+
$guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestQuote->getId());
127+
128+
$query = $this->getCartMergeMutation($guestQuoteMaskedId, $customerQuoteMaskedId);
129+
$this->graphQlMutation($query, [], '', $this->getHeaderMap());
130+
$cartResponse = $this->graphQlMutation(
131+
$this->getCartQuery($guestQuoteMaskedId),
132+
[],
133+
'',
134+
$this->getHeaderMap()
135+
);
136+
}
137+
138+
/**
139+
* @magentoApiDataFixture Magento/Customer/_files/two_customers.php
140+
* @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
141+
* @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php
142+
* @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
143+
* @expectedException \Exception
144+
* @expectedExceptionMessage The current user cannot perform operations on cart
145+
*/
146+
public function testMergeTwoCustomerCarts()
147+
{
148+
$firstQuote = $this->quoteFactory->create();
149+
$this->quoteResource->load($firstQuote, 'test_quote', 'reserved_order_id');
150+
$firstMaskedId = $this->quoteIdToMaskedId->execute((int)$firstQuote->getId());
151+
152+
$createCartResponse = $this->graphQlMutation(
153+
$this->getCreateEmptyCartMutation(),
154+
[],
155+
'',
156+
$this->getHeaderMap('customer_two@example.com')
157+
);
158+
self::assertArrayHasKey('createEmptyCart', $createCartResponse);
159+
$secondMaskedId = $createCartResponse['createEmptyCart'];
160+
$this->addSimpleProductToCart($secondMaskedId, $this->getHeaderMap());
161+
162+
$query = $this->getCartMergeMutation($firstMaskedId, $secondMaskedId);
163+
$this->graphQlMutation($query, [], '', $this->getHeaderMap());
164+
}
165+
166+
/**
167+
* Add simple product to cart
168+
*
169+
* @param string $maskedId
170+
* @param array $headerMap
171+
* @throws \Exception
172+
*/
173+
private function addSimpleProductToCart(string $maskedId, array $headerMap): void
174+
{
175+
$result = $this->graphQlMutation($this->getAddProductToCartMutation($maskedId), [], '', $headerMap);
176+
self::assertArrayHasKey('addSimpleProductsToCart', $result);
177+
self::assertArrayHasKey('cart', $result['addSimpleProductsToCart']);
178+
self::assertArrayHasKey('items', $result['addSimpleProductsToCart']['cart']);
179+
self::assertArrayHasKey(0, $result['addSimpleProductsToCart']['cart']['items']);
180+
self::assertArrayHasKey('quantity', $result['addSimpleProductsToCart']['cart']['items'][0]);
181+
self::assertEquals(1, $result['addSimpleProductsToCart']['cart']['items'][0]['quantity']);
182+
self::assertArrayHasKey('product', $result['addSimpleProductsToCart']['cart']['items'][0]);
183+
self::assertArrayHasKey('sku', $result['addSimpleProductsToCart']['cart']['items'][0]['product']);
184+
self::assertEquals('simple_product', $result['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']);
185+
}
186+
187+
/**
188+
* Create the mergeCart mutation
189+
*
190+
* @param string $guestQuoteMaskedId
191+
* @param string $customerQuoteMaskedId
192+
* @return string
193+
*/
194+
private function getCartMergeMutation(string $guestQuoteMaskedId, string $customerQuoteMaskedId): string
195+
{
196+
return <<<QUERY
197+
mutation {
198+
mergeCarts(
199+
source_cart_id: "{$guestQuoteMaskedId}"
200+
destination_cart_id: "{$customerQuoteMaskedId}"
201+
){
202+
items {
203+
quantity
204+
product {
205+
sku
206+
}
207+
}
208+
}
209+
}
210+
QUERY;
211+
}
212+
213+
/**
214+
* Get cart query
215+
*
216+
* @param string $maskedId
217+
* @return string
218+
*/
219+
private function getCartQuery(string $maskedId): string
220+
{
221+
return <<<QUERY
222+
{
223+
cart(cart_id: "{$maskedId}") {
224+
items {
225+
quantity
226+
product {
227+
sku
228+
}
229+
}
230+
}
231+
}
232+
QUERY;
233+
}
234+
235+
/**
236+
* Get create empty cart mutation
237+
*
238+
* @return string
239+
*/
240+
private function getCreateEmptyCartMutation(): string
241+
{
242+
return <<<QUERY
243+
mutation {
244+
createEmptyCart
245+
}
246+
QUERY;
247+
}
248+
249+
/**
250+
* Get add product to cart mutation
251+
*
252+
* @param string $maskedId
253+
* @return string
254+
*/
255+
private function getAddProductToCartMutation(string $maskedId): string
256+
{
257+
return <<<QUERY
258+
mutation {
259+
addSimpleProductsToCart(input: {
260+
cart_id: "{$maskedId}"
261+
cart_items: {
262+
data: {
263+
quantity: 1
264+
sku: "simple_product"
265+
}
266+
}
267+
}) {
268+
cart {
269+
items {
270+
quantity
271+
product {
272+
sku
273+
}
274+
}
275+
}
276+
}
277+
}
278+
QUERY;
279+
}
280+
281+
/**
282+
* @param string $username
283+
* @param string $password
284+
* @return array
285+
* @throws \Magento\Framework\Exception\AuthenticationException
286+
*/
287+
private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array
288+
{
289+
$customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password);
290+
$headerMap = ['Authorization' => 'Bearer ' . $customerToken];
291+
return $headerMap;
292+
}
293+
}

0 commit comments

Comments
 (0)