diff --git a/app/code/Magento/QuoteGraphQl/Model/Authorization/IsCartMutationAllowedForCurrentUser.php b/app/code/Magento/QuoteGraphQl/Model/Authorization/IsCartMutationAllowedForCurrentUser.php new file mode 100644 index 000000000000..2dec8c278800 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Authorization/IsCartMutationAllowedForCurrentUser.php @@ -0,0 +1,67 @@ +userContext = $userContext; + $this->cartRepository = $cartRepository; + } + + /** + * Check that the shopping cart operations are allowed for current user + * + * @param int $quoteId + * @return bool + * @throws GraphQlNoSuchEntityException + */ + public function execute(int $quoteId): bool + { + try { + $quote = $this->cartRepository->get($quoteId); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException(__($exception->getMessage())); + } + + $customerId = $quote->getCustomerId(); + + /* Guest cart, allow operations */ + if (!$customerId) { + return true; + } + + /* If the quote belongs to the current customer allow operations */ + return $customerId == $this->userContext->getUserId(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Coupon/ApplyCouponToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Coupon/ApplyCouponToCart.php new file mode 100644 index 000000000000..ab57b8ff499c --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Coupon/ApplyCouponToCart.php @@ -0,0 +1,110 @@ +couponManagement = $couponManagement; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToId; + $this->isCartMutationAllowedForCurrentUser = $isCartMutationAllowedForCurrentUser; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['coupon_code'])) { + throw new GraphQlInputException(__('Required parameter "coupon_code" is missing')); + } + $couponCode = $args['input']['coupon_code']; + + try { + $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException( + __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $maskedCartId]) + ); + } + + if (false === $this->isCartMutationAllowedForCurrentUser->execute($cartId)) { + throw new GraphQlAuthorizationException( + __( + 'The current user cannot perform operations on cart "%masked_cart_id"', + ['masked_cart_id' => $maskedCartId] + ) + ); + } + + /* Check current cart does not have coupon code applied */ + $appliedCouponCode = $this->couponManagement->get($cartId); + if (!empty($appliedCouponCode)) { + throw new GraphQlInputException( + __('A coupon is already applied to the cart. Please remove it to apply another') + ); + } + + try { + $this->couponManagement->set($cartId, $couponCode); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException(__($exception->getMessage())); + } catch (CouldNotSaveException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + $data['cart']['applied_coupon'] = [ + 'code' => $couponCode, + ]; + return $data; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Coupon/RemoveCouponFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Coupon/RemoveCouponFromCart.php new file mode 100644 index 000000000000..abb5a0b57519 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Coupon/RemoveCouponFromCart.php @@ -0,0 +1,97 @@ +couponManagement = $couponManagement; + $this->isCartMutationAllowedForCurrentUser = $isCartMutationAllowedForCurrentUser; + $this->maskedQuoteIdToId = $maskedQuoteIdToId; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['input']['cart_id']; + + try { + $cartId = $this->maskedQuoteIdToId->execute($maskedCartId); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException( + __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $maskedCartId]) + ); + } + + if (false === $this->isCartMutationAllowedForCurrentUser->execute($cartId)) { + throw new GraphQlAuthorizationException( + __( + 'The current user cannot perform operations on cart "%masked_cart_id"', + ['masked_cart_id' => $maskedCartId] + ) + ); + } + + try { + $this->couponManagement->remove($cartId); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException(__($exception->getMessage())); + } catch (CouldNotDeleteException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + $data['cart']['applied_coupon'] = [ + 'code' => '', + ]; + return $data; + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 46d1b97d0aea..06b3328b9e05 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -3,4 +3,35 @@ type Mutation { createEmptyCart: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Cart\\CreateEmptyCart") @doc(description:"Creates empty shopping cart for 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") } + +input ApplyCouponToCartInput { + cart_id: String! + coupon_code: String! +} + +type ApplyCouponToCartOutput { + cart: Cart! +} + +type Cart { + applied_coupon: AppliedCoupon +} + +type CartAddress { + applied_coupon: AppliedCoupon +} + +type AppliedCoupon { + code: String! +} + +input RemoveCouponFromCartInput { + cart_id: String! +} + +type RemoveCouponFromCartOutput { + cart: Cart +} \ No newline at end of file diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php new file mode 100644 index 000000000000..1f8ad06a9f8e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php @@ -0,0 +1,225 @@ +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 + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testApplyCouponToGuestCartWithItems() + { + $couponCode = '2?ds5!2d'; + + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey("applyCouponToCart", $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testApplyCouponTwice() + { + $couponCode = '2?ds5!2d'; + + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey("applyCouponToCart", $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + + self::expectExceptionMessage('A coupon is already applied to the cart. Please remove it to apply another'); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testApplyCouponToCartWithNoItems() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/191'); + $couponCode = '2?ds5!2d'; + + $this->quoteResource->load($this->quote, 'test_order_1', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessageRegExp('/Cart doesn\'t contain products/'); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testGuestCustomerAttemptToChangeCustomerCart() + { + $couponCode = '2?ds5!2d'; + + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $this->quote->setCustomerId(1); + $this->quoteResource->save($this->quote); + $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testRemoveCoupon() + { + $couponCode = '2?ds5!2d'; + + /* Apply coupon to the quote */ + $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' + ); + $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); + $this->graphQlQuery($query); + + /* Remove coupon from quote */ + $query = $this->prepareRemoveCouponRequestQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertSame('', $response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testRemoveCouponFromCustomerCartByGuest() + { + $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); + $query = $this->prepareRemoveCouponRequestQuery($maskedQuoteId); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @param string $couponCode + * @return string + */ + private function prepareAddCouponRequestQuery(string $maskedQuoteId, string $couponCode): string + { + return <<get(\Magento\Framework\Registry::class); + +/** @var Magento\SalesRule\Model\Rule $rule */ +$rule = $registry->registry('cart_rule_fixed_discount_coupon'); +if ($rule) { + $rule->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_code_with_wildcard_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_code_with_wildcard_rollback.php index 776c30221035..c9613c371bbe 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_code_with_wildcard_rollback.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_code_with_wildcard_rollback.php @@ -14,8 +14,19 @@ $objectManager = Bootstrap::getObjectManager(); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('name', '5$ fixed discount on whole cart') + ->create(); + +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = Bootstrap::getObjectManager()->get(RuleRepositoryInterface::class); +$items = $ruleRepository->getList($searchCriteria) + ->getItems(); + +$salesRule = array_pop($items); + /** @var Rule $salesRule */ -$salesRule = getSalesRule('5$ fixed discount on whole cart'); if ($salesRule !== null) { /** @var RuleRepositoryInterface $ruleRepository */ $ruleRepository = $objectManager->get(RuleRepositoryInterface::class); @@ -29,18 +40,3 @@ $couponRepository = $objectManager->get(CouponRepositoryInterface::class); $couponRepository->deleteById($coupon->getCouponId()); } - -function getSalesRule(string $name) -{ - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilter('name', $name) - ->create(); - - /** @var RuleRepositoryInterface $ruleRepository */ - $ruleRepository = Bootstrap::getObjectManager()->get(RuleRepositoryInterface::class); - $items = $ruleRepository->getList($searchCriteria) - ->getItems(); - - return array_pop($items); -}