Skip to content
This repository has been archived by the owner on Dec 19, 2019. It is now read-only.

Mutations coupons #163

Merged
merged 16 commits into from
Sep 20, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions app/code/Magento/QuoteGraphQl/Model/CartMutationsAllowed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\QuoteGraphQl\Model;

use Magento\Authorization\Model\UserContextInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Quote\Api\CartRepositoryInterface;

/**
* {@inheritDoc}
*/
class CartMutationsAllowed implements CartMutationsAllowedInterface
{
/**
* @var CartRepositoryInterface
*/
private $cartRepository;

/**
* @var UserContextInterface
*/
private $userContext;

/**
* @param UserContextInterface $userContext
* @param CartRepositoryInterface $cartRepository
*/
public function __construct(
UserContextInterface $userContext,
CartRepositoryInterface $cartRepository
) {
$this->userContext = $userContext;
$this->cartRepository = $cartRepository;
}

/**
* {@inheritDoc}
*/
public function execute(int $quoteId): bool
{
try {
$quote = $this->cartRepository->get($quoteId);
} catch (NoSuchEntityException $exception) {
throw new GraphQlNoSuchEntityException(__($exception->getMessage()));
}

$customerId = $quote->getCustomerId();

if (!$customerId) {
Copy link
Contributor

@paliarush paliarush Aug 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments to explain allowed use cases. It might be not obvious why we have these checks.

return true;
}

if ($customerId == $this->userContext->getUserId()) {
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\QuoteGraphQl\Model;

use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;

/**
* Service for checking that the shopping cart operations
* are allowed for current user
*/
interface CartMutationsAllowedInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All classes/interfaces should be named using nouns. Please consider making changes across the review. In this specific case something like Authorization\CartMutation::isAllowed sounds better

{
/**
* @param int $quoteId
* @return bool
* @throws GraphQlNoSuchEntityException
*/
public function execute(int $quoteId): bool;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\QuoteGraphQl\Model\Resolver\Coupon;

use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\GraphQl\Query\Resolver\Value;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Quote\Api\CouponManagementInterface;
use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface;
use Magento\QuoteGraphQl\Model\CartMutationsAllowedInterface;

/**
* {@inheritdoc}
*/
class ApplyCouponToCart implements ResolverInterface
{
/**
* @var CouponManagementInterface
*/
private $couponManagement;

/**
* @var ValueFactory
*/
private $valueFactory;

/**
* @var MaskedQuoteIdToQuoteIdInterface
*/
private $maskedQuoteIdToQuoteId;

/**
* @var CartMutationsAllowedInterface
*/
private $cartMutationsAllowed;

/**
* @param ValueFactory $valueFactory
* @param CouponManagementInterface $couponManagement
* @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToId
* @param CartMutationsAllowedInterface $cartMutationsAllowed
*/
public function __construct(
ValueFactory $valueFactory,
CouponManagementInterface $couponManagement,
MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToId,
CartMutationsAllowedInterface $cartMutationsAllowed
) {
$this->valueFactory = $valueFactory;
$this->couponManagement = $couponManagement;
$this->maskedQuoteIdToQuoteId = $maskedQuoteIdToId;
$this->cartMutationsAllowed = $cartMutationsAllowed;
}

/**
* {@inheritDoc}
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value
{
$maskedQuoteId = $args['input']['cart_id'];
$couponCode = $args['input']['coupon_code'];

if (!$maskedQuoteId || !$couponCode) {
throw new GraphQlInputException(__('Required parameter is missing'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to specify which parameter is missing (have two separate exceptions)

}

try {
$cartId = $this->maskedQuoteIdToQuoteId->execute($maskedQuoteId);
} catch (NoSuchEntityException $exception) {
throw new GraphQlNoSuchEntityException(__('No cart with provided ID found'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can include the ID, then change the exception text to

'Could not find a cart with ID <ID_number>'

Otherwise, change exception text to 'Could not find a cart with the provided ID'

}

if (!$this->cartMutationsAllowed->execute($cartId)) {
throw new GraphQlAuthorizationException(
__('Operations with selected cart is not permitted for current user')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'cart is not permitted' => 'cart are not permitted'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can include the cart ID, change exception text to
'The current user cannot perform operations on cart <cart_id>'

Otherwise, change the text to
The current user cannot perform operations on the selected cart'

);
}

/* 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 (\Exception $exception) {
throw new GraphQlInputException(__($exception->getMessage()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must not display exception messages from \Exception to the clients, they may contain sensitive information. Please log the original exception message and display some generic one.

Please make sure to fix similar issues across the PR if any.

}

$data['cart']['applied_coupon'] = [
'code' => $couponCode
];

$result = function () use ($data) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it possible to get the full cart object if needed?

return $data;
};

return $this->valueFactory->create($result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\QuoteGraphQl\Model\Resolver\Coupon;

use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\GraphQl\Query\Resolver\Value;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Quote\Api\CouponManagementInterface;
use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface;
use Magento\QuoteGraphQl\Model\CartMutationsAllowedInterface;

/**
* {@inheritdoc}
*/
class RemoveCouponFromCart implements ResolverInterface
{
/**
* @var MaskedQuoteIdToQuoteIdInterface
*/
private $maskedQuoteIdToId;

/**
* @var CouponManagementInterface
*/
private $couponManagement;
/**
* @var ValueFactory
*/
private $valueFactory;

/**
* @var CartMutationsAllowedInterface
*/
private $cartMutationsAllowed;

/**
* @param ValueFactory $valueFactory
* @param CouponManagementInterface $couponManagement
* @param CartMutationsAllowedInterface $cartMutationsAllowed
* @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToId
*/
public function __construct(
ValueFactory $valueFactory,
CouponManagementInterface $couponManagement,
CartMutationsAllowedInterface $cartMutationsAllowed,
MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToId
) {
$this->valueFactory = $valueFactory;
$this->couponManagement = $couponManagement;
$this->cartMutationsAllowed = $cartMutationsAllowed;
$this->maskedQuoteIdToId = $maskedQuoteIdToId;
}

/**
* {@inheritDoc}
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value
{
$maskedCartId = $args['input']['cart_id'];

if (!$maskedCartId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apply the same edits to the exception text as with ApplyCouponToCart.php

throw new GraphQlInputException(__('Required parameter is missing'));
}

try {
$cartId = $this->maskedQuoteIdToId->execute($maskedCartId);
} catch (NoSuchEntityException $exception) {
throw new GraphQlNoSuchEntityException(__('No cart with provided ID found'));
}

if (!$this->cartMutationsAllowed->execute((int) $cartId)) {
throw new GraphQlAuthorizationException(
__('Operations with selected cart is not permitted for current user')
);
}

try {
$this->couponManagement->remove($cartId);
} catch (\Exception $exception) {
throw new GraphQlInputException(__($exception->getMessage()));
}

$data['cart']['applied_coupon'] = [
'code' => ''
];

$result = function () use ($data) {
return $data;
};

return $this->valueFactory->create($result);
}
}
10 changes: 10 additions & 0 deletions app/code/Magento/QuoteGraphQl/etc/di.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\QuoteGraphQl\Model\CartMutationsAllowedInterface" type="Magento\QuoteGraphQl\Model\CartMutationsAllowed" />
</config>
32 changes: 32 additions & 0 deletions app/code/Magento/QuoteGraphQl/etc/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,36 @@

type Mutation {
createEmptyCart: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Cart\\CreateEmptyCart") @doc(description:"Creates empty shopping cart for guest or logged in user")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change description to

Creates an empty shopping cart for a guest or a 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 {
# Wrapper allows for future extension of coupon info
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need to leave such comments in the schema.

code: String!
}

input RemoveCouponFromCartInput {
cart_id: String!
}

type RemoveCouponFromCartOutput {
cart: Cart
}
Loading