diff --git a/app/code/Magento/Quote/Test/Fixture/MakeCartInactive.php b/app/code/Magento/Quote/Test/Fixture/MakeCartInactive.php new file mode 100644 index 0000000000000..c6e89890f4a9e --- /dev/null +++ b/app/code/Magento/Quote/Test/Fixture/MakeCartInactive.php @@ -0,0 +1,83 @@ +cartRepository = $cartRepository; + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + } + + /** + * @param array $data + * @return void + * @throws InvalidArgumentException + */ + public function apply(array $data = []): ?DataObject + { + if (empty($data[self::FIELD_CART_ID])) { + throw new InvalidArgumentException(__('"%field" is required', ['field' => self::FIELD_CART_ID])); + } + + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $data[self::FIELD_CART_ID]); + $quote->setIsActive(false); + $this->cartRepository->save($quote); + + return $quote; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateMaskedQuoteId.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateMaskedQuoteId.php new file mode 100644 index 0000000000000..054f84d76543a --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateMaskedQuoteId.php @@ -0,0 +1,78 @@ +maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + } + + /** + * Validate masked id + * + * @param string $maskedId + * @throws GraphQlAlreadyExistsException + * @throws GraphQlInputException + */ + public function execute(string $maskedId): void + { + if (mb_strlen($maskedId) != 32) { + throw new GraphQlInputException(__('Cart ID length should to be 32 symbols.')); + } + + if ($this->isQuoteWithSuchMaskedIdAlreadyExists($maskedId)) { + throw new GraphQlAlreadyExistsException(__('Cart with ID "%1" already exists.', $maskedId)); + } + } + + /** + * Check is quote with such maskedId already exists + * + * @param string $maskedId + * @return bool + */ + private function isQuoteWithSuchMaskedIdAlreadyExists(string $maskedId): bool + { + try { + $this->maskedQuoteIdToQuoteId->execute($maskedId); + return true; + } catch (NoSuchEntityException $e) { + return false; + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php index f020527d958e4..b89bce213d7a7 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php @@ -7,15 +7,13 @@ namespace Magento\QuoteGraphQl\Model\Resolver; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer; use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForGuest; +use Magento\QuoteGraphQl\Model\Cart\ValidateMaskedQuoteId; /** * @inheritdoc @@ -37,19 +35,27 @@ class CreateEmptyCart implements ResolverInterface */ private $maskedQuoteIdToQuoteId; + /** + * @var ValidateMaskedQuoteId + */ + private ValidateMaskedQuoteId $validateMaskedQuoteId; + /** * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer * @param CreateEmptyCartForGuest $createEmptyCartForGuest * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + * @param ValidateMaskedQuoteId $validateMaskedQuoteId */ public function __construct( CreateEmptyCartForCustomer $createEmptyCartForCustomer, CreateEmptyCartForGuest $createEmptyCartForGuest, - MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + ValidateMaskedQuoteId $validateMaskedQuoteId ) { $this->createEmptyCartForCustomer = $createEmptyCartForCustomer; $this->createEmptyCartForGuest = $createEmptyCartForGuest; $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->validateMaskedQuoteId = $validateMaskedQuoteId; } /** @@ -62,7 +68,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $predefinedMaskedQuoteId = null; if (isset($args['input']['cart_id'])) { $predefinedMaskedQuoteId = $args['input']['cart_id']; - $this->validateMaskedId($predefinedMaskedQuoteId); + $this->validateMaskedQuoteId->execute($predefinedMaskedQuoteId); } $maskedQuoteId = (0 === $customerId || null === $customerId) @@ -70,38 +76,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value : $this->createEmptyCartForCustomer->execute($customerId, $predefinedMaskedQuoteId); return $maskedQuoteId; } - - /** - * Validate masked id - * - * @param string $maskedId - * @throws GraphQlAlreadyExistsException - * @throws GraphQlInputException - */ - private function validateMaskedId(string $maskedId): void - { - if (mb_strlen($maskedId) != 32) { - throw new GraphQlInputException(__('Cart ID length should to be 32 symbols.')); - } - - if ($this->isQuoteWithSuchMaskedIdAlreadyExists($maskedId)) { - throw new GraphQlAlreadyExistsException(__('Cart with ID "%1" already exists.', $maskedId)); - } - } - - /** - * Check is quote with such maskedId already exists - * - * @param string $maskedId - * @return bool - */ - private function isQuoteWithSuchMaskedIdAlreadyExists(string $maskedId): bool - { - try { - $this->maskedQuoteIdToQuoteId->execute($maskedId); - return true; - } catch (NoSuchEntityException $e) { - return false; - } - } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateGuestCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateGuestCart.php new file mode 100644 index 0000000000000..5889c8eb721fe --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateGuestCart.php @@ -0,0 +1,102 @@ +createEmptyCartForGuest = $createEmptyCartForGuest; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->cartRepository = $cartRepository; + $this->validateMaskedQuoteId = $validateMaskedQuoteId; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $customerId = $context->getUserId(); + + $predefinedMaskedQuoteId = null; + if (isset($args['input']['cart_uid'])) { + $predefinedMaskedQuoteId = $args['input']['cart_uid']; + $this->validateMaskedQuoteId->execute($predefinedMaskedQuoteId); + } + + if ($customerId === 0 || $customerId === null) { + $maskedQuoteId = $this->createEmptyCartForGuest->execute($predefinedMaskedQuoteId); + $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedQuoteId); + $cart = $this->cartRepository->get($cartId); + } else { + throw new GraphQlAlreadyExistsException( + __('Use `Query.customerCart` for logged in customer.') + ); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 1b65bdcf4564c..89473e1554e11 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -8,7 +8,8 @@ type Query { } type Mutation { - createEmptyCart(input: createEmptyCartInput @doc(description: "An optional input object that assigns the specified ID to the cart.")): String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Create an empty shopping cart for a guest or logged in user") + createGuestCart(input: CreateGuestCartInput): CreateGuestCartOutput @doc(description: "Create a new shopping cart") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateGuestCart") + createEmptyCart(input: createEmptyCartInput @doc(description: "An optional input object that assigns the specified ID to the cart.")): String @deprecated(reason: "Use `Mutation.createGuestCart` or `Query.customerCart` for logged in customer") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Create an empty shopping cart for a guest or logged in user") addSimpleProductsToCart(input: AddSimpleProductsToCartInput @doc(description: "An input object that defines which simple products to add to the cart.")): AddSimpleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") @doc(description:"Add one or more simple products to the specified cart. We recommend using `addProductsToCart` instead.") addVirtualProductsToCart(input: AddVirtualProductsToCartInput @doc(description: "An input object that defines which virtual products to add to the cart.")): AddVirtualProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") @doc(description:"Add one or more virtual products to the specified cart. We recommend using `addProductsToCart` instead.") applyCouponToCart(input: ApplyCouponToCartInput @doc(description: "An input object that defines the coupon code to apply to the cart.")): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ApplyCouponToCart") @doc(description:"Apply a pre-defined coupon code to the specified cart.") @@ -29,6 +30,10 @@ type Mutation { addProductsToCart(cartId: String! @doc(description: "The cart ID of the shopper."), cartItems: [CartItemInput!]! @doc(description: "An array that defines the products to add to the cart.")): AddProductsToCartOutput @doc(description:"Add any type of product to the cart.") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddProductsToCart") } +input CreateGuestCartInput { + cart_uid: ID @doc(description: "Optional client-generated ID") +} + input createEmptyCartInput @doc(description: "Assigns a specific `cart_id` to the empty cart.") { cart_id: String @doc(description: "The ID to assign to the cart.") } @@ -186,6 +191,10 @@ type CartDiscount @doc(description: "Contains information about discounts applie label: [String!]! @doc(description: "The description of the discount.") } +type CreateGuestCartOutput { + cart: Cart @doc(description: "The newly created cart.") +} + type SetPaymentMethodOnCartOutput @doc(description: "Contains details about the cart after setting the payment method.") { cart: Cart! @doc(description: "The cart after setting the payment method.") } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateGuestCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateGuestCartTest.php new file mode 100644 index 0000000000000..88d69b90ac8f3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateGuestCartTest.php @@ -0,0 +1,112 @@ +objectManager = Bootstrap::getObjectManager(); + $this->quoteCollectionFactory = $this->objectManager->get(QuoteCollectionFactory::class); + $this->quoteResource = $this->objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $this->objectManager->get(QuoteIdMaskFactory::class); + } + + #[ + DataFixture(Customer::class, as: 'customer') + ] + public function testFailForLoggedInUser() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage("Use `Query.customerCart` for logged in customer."); + + $customer = DataFixtureStorageManager::getStorage()->get('customer'); + + $query = $this->getQuery(); + $this->graphQlMutation( + $query, + [], + '', + $this->objectManager->get(GetCustomerAuthenticationHeader::class)->execute($customer->getEmail()) + ); + } + + /** + * @return string + */ + private function getQuery(): string + { + return <<quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateGuestCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateGuestCartTest.php new file mode 100644 index 0000000000000..5ac8f38067760 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateGuestCartTest.php @@ -0,0 +1,195 @@ +guestCartRepository = $objectManager->get(GuestCartRepositoryInterface::class); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + public function testSuccessfulCreateGuestCart() + { + $query = $this->getQuery(); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('createGuestCart', $response); + self::assertNotEmpty($response['createGuestCart']); + self::assertArrayHasKey('cart', $response['createGuestCart']); + self::assertNotEmpty($response['createGuestCart']['cart']); + self::assertArrayHasKey('id', $response['createGuestCart']['cart']); + self::assertNotEmpty($response['createGuestCart']['cart']['id']); + + $guestCart = $this->guestCartRepository->get($response['createGuestCart']['cart']['id']); + + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + self::assertEquals('default', $guestCart->getStore()->getCode()); + self::assertEquals('1', $guestCart->getCustomerIsGuest()); + } + + #[ + DataFixture(Store::class, as: 'store') + ] + public function testSuccessfulWithNotDefaultStore() + { + $store = DataFixtureStorageManager::getStorage()->get('store'); + $storeCode = $store->getCode(); + + $query = $this->getQuery(); + $headerMap = ['Store' => $storeCode]; + $response = $this->graphQlMutation($query, [], '', $headerMap); + + self::assertArrayHasKey('createGuestCart', $response); + self::assertNotEmpty($response['createGuestCart']); + self::assertArrayHasKey('cart', $response['createGuestCart']); + self::assertNotEmpty($response['createGuestCart']['cart']); + self::assertArrayHasKey('id', $response['createGuestCart']['cart']); + self::assertNotEmpty($response['createGuestCart']['cart']['id']); + + $guestCart = $this->guestCartRepository->get($response['createGuestCart']['cart']['id']); + + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + self::assertSame($storeCode, $guestCart->getStore()->getCode()); + self::assertEquals('1', $guestCart->getCustomerIsGuest()); + } + + public function testSuccessfulWithPredefinedCartId() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = $this->getQueryWithCartId($predefinedCartId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('createGuestCart', $response); + self::assertNotEmpty($response['createGuestCart']); + self::assertArrayHasKey('cart', $response['createGuestCart']); + self::assertNotEmpty($response['createGuestCart']['cart']); + self::assertArrayHasKey('id', $response['createGuestCart']['cart']); + self::assertNotEmpty($response['createGuestCart']['cart']['id']); + self::assertEquals($predefinedCartId, $response['createGuestCart']['cart']['id']); + } + + public function testFailIfPredefinedCartIdAlreadyExists() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage("Cart with ID \"572cda51902b5b517c0e1a2b2fd004b4\" already exists."); + + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = $this->getQueryWithCartId($predefinedCartId); + $this->graphQlMutation($query); + $this->graphQlMutation($query); + } + + public function testFailWithWrongPredefinedCartId() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage("Cart ID length should to be 32 symbols."); + + $predefinedCartId = '1234567890'; + + $query = $this->getQueryWithCartId($predefinedCartId); + $this->graphQlMutation($query); + } + + /** + * @return string + */ + private function getQuery(): string + { + return <<quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + parent::tearDown(); + } +}