feature #11532 [API] Add simple item to cart (lchrusciel)

This PR was merged into the 1.8-dev branch.

Discussion
----------

| Q               | A
| --------------- | -----
| Branch?         | master
| Bug fix?        | no
| New feature?    | yes
| BC breaks?      | no
| Deprecations?   | no
| Related tickets | Related to #11250, Replaces #11528
| License         | MIT

<!--
 - Bug fixes must be submitted against the 1.6 or 1.7 branches (the lowest possible)
 - Features and deprecations must be submitted against the master branch
 - Make sure that the correct base branch is set
-->


Commits
-------

0c4088959b [API] Put simple item to cart implementation
c804251927 [API] Add to cart when logged in
14d8129be9 [API] Add products with quantity to cart
This commit is contained in:
Grzegorz Sadowski 2020-05-29 12:57:15 +02:00 committed by GitHub
commit 2875e9e254
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 621 additions and 30 deletions

View file

@ -7,7 +7,7 @@ Feature: Adding a simple product to the cart
Background:
Given the store operates on a single channel in "United States"
@ui
@ui @api
Scenario: Adding a simple product to the cart
Given the store has a product "T-shirt banana" priced at "$12.54"
When I add this product to the cart
@ -16,7 +16,7 @@ Feature: Adding a simple product to the cart
And there should be one item in my cart
And this item should have name "T-shirt banana"
@ui
@ui @api
Scenario: Adding a product to the cart as a logged in customer
Given I am a logged in customer
And the store has a product "Oathkeeper" priced at "$99.99"

View file

@ -8,7 +8,7 @@ Feature: Adding a simple product of given quantity to the cart
Given the store operates on a single channel in "United States"
And the store has a product "T-shirt banana" priced at "$12.54"
@ui
@ui @api
Scenario: Adding a product with stated quantity to the cart
Given there are 10 units of product "T-shirt banana" available in the inventory
When I add 5 of them to my cart
@ -16,7 +16,7 @@ Feature: Adding a simple product of given quantity to the cart
And I should be notified that the product has been successfully added
And I should see "T-shirt banana" with quantity 5 in my cart
@ui
@ui @api
Scenario: Adding way too many products sets their quantity to 9999
Given there are 100000 units of product "T-shirt banana" available in the inventory
When I add 20000 of them to my cart

View file

@ -44,6 +44,8 @@ interface ApiClientInterface
public function upload(): Response;
public function executeCustomRequest(RequestInterface $request): Response;
public function buildCreateRequest(): void;
public function buildUpdateRequest(string $id): void;

View file

@ -16,6 +16,7 @@ namespace Sylius\Behat\Client;
use Sylius\Behat\Service\SharedStorageInterface;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
final class ApiPlatformClient implements ApiClientInterface
@ -48,12 +49,18 @@ final class ApiPlatformClient implements ApiClientInterface
public function showByIri(string $iri): Response
{
return $this->request(Request::custom($iri, 'GET', $this->sharedStorage->get('token')));
$request = Request::custom($iri, HttpRequest::METHOD_GET);
$request->authorize($this->sharedStorage->get('token'));
return $this->request($request);
}
public function subResourceIndex(string $subResource, string $id): Response
{
return $this->request(Request::subResourceIndex($this->resource, $id, $subResource, $this->sharedStorage->get('token')));
$request = Request::subResourceIndex($this->resource, $id, $subResource);
$request->authorize($this->sharedStorage->get('token'));
return $this->request($request);
}
public function show(string $id): Response
@ -90,7 +97,8 @@ final class ApiPlatformClient implements ApiClientInterface
public function applyTransition(string $id, string $transition, array $content = []): Response
{
$request = Request::transition($this->resource, $id, $transition, $this->sharedStorage->get('token'));
$request = Request::transition($this->resource, $id, $transition);
$request->authorize($this->sharedStorage->get('token'));
$request->setContent($content);
return $this->request($request);
@ -98,15 +106,19 @@ final class ApiPlatformClient implements ApiClientInterface
public function customItemAction(string $id, string $type, string $action): Response
{
$request = Request::customItemAction($this->resource, $id, $type, $action, $this->sharedStorage->get('token'));
$request = Request::customItemAction($this->resource, $id, $type, $action);
$request->authorize($this->sharedStorage->get('token'));
return $this->request($request);
}
public function customAction(string $url, string $method): Response
{
$token = $this->sharedStorage->has('token') ? $this->sharedStorage->get('token') : null;
$request = Request::custom($url, $method, $token);
$request = Request::custom($url, $method);
if ($this->sharedStorage->has('token')) {
$request->authorize($this->sharedStorage->get('token'));
}
return $this->request($request);
}
@ -116,6 +128,11 @@ final class ApiPlatformClient implements ApiClientInterface
return $this->request($this->request);
}
public function executeCustomRequest(RequestInterface $request): Response
{
return $this->request($request);
}
public function buildCreateRequest(): void
{
$this->request = Request::create($this->resource);

View file

@ -15,6 +15,7 @@ namespace Sylius\Behat\Client;
use Sylius\Behat\Service\SharedStorageInterface;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
final class ApiPlatformIriClient implements ApiIriClientInterface
@ -33,7 +34,10 @@ final class ApiPlatformIriClient implements ApiIriClientInterface
public function showByIri(string $iri): Response
{
return $this->request(Request::custom($iri, 'GET', $this->sharedStorage->get('token')));
$request = Request::custom($iri, HttpRequest::METHOD_GET);
$request->authorize($this->sharedStorage->get('token'));
return $this->request($request);
}
private function request(RequestInterface $request): Response

View file

@ -49,12 +49,11 @@ final class Request implements RequestInterface
return new self('/new-api/' . $resource, HttpRequest::METHOD_GET, $headers);
}
public static function subResourceIndex(string $resource, string $id, string $subResource, string $token): RequestInterface
public static function subResourceIndex(string $resource, string $id, string $subResource): RequestInterface
{
return new self(
sprintf('/new-api/%s/%s/%s', $resource, $id, $subResource),
HttpRequest::METHOD_GET,
['HTTP_Authorization' => 'Bearer ' . $token]
HttpRequest::METHOD_GET
);
}
@ -94,17 +93,17 @@ final class Request implements RequestInterface
);
}
public static function transition(string $resource, string $id, string $transition, string $token): RequestInterface
public static function transition(string $resource, string $id, string $transition): RequestInterface
{
return self::customItemAction($resource, $id, HttpRequest::METHOD_PATCH, $transition, $token);
return self::customItemAction($resource, $id, HttpRequest::METHOD_PATCH, $transition);
}
public static function customItemAction(string $resource, string $id, string $type, string $action, string $token): RequestInterface
public static function customItemAction(string $resource, string $id, string $type, string $action): RequestInterface
{
return new self(
sprintf('/new-api/%s/%s/%s', $resource, $id, $action),
$type,
['CONTENT_TYPE' => 'application/merge-patch+json', 'HTTP_Authorization' => 'Bearer ' . $token]
['CONTENT_TYPE' => 'application/merge-patch+json']
);
}
@ -117,12 +116,12 @@ final class Request implements RequestInterface
);
}
public static function custom(string $url, string $method, ?string $token): RequestInterface
public static function custom(string $url, string $method): RequestInterface
{
return new self(
$url,
$method,
['CONTENT_TYPE' => 'application/merge-patch+json', 'HTTP_Authorization' => 'Bearer ' . $token]
['CONTENT_TYPE' => 'application/merge-patch+json']
);
}

View file

@ -17,7 +17,7 @@ interface RequestInterface
{
public static function index(string $resource, string $token): self;
public static function subResourceIndex(string $resource, string $id, string $subResource, string $token): self;
public static function subResourceIndex(string $resource, string $id, string $subResource): self;
public static function show(string $resource, string $id, string $token): self;
@ -27,11 +27,11 @@ interface RequestInterface
public static function delete(string $resource, string $id, string $token): self;
public static function transition(string $resource, string $id, string $transition, string $token): self;
public static function transition(string $resource, string $id, string $transition): self;
public static function upload(string $resource, string $token): self;
public static function custom(string $url, string $method, ?string $token): self;
public static function custom(string $url, string $method): self;
public function url(): string;

View file

@ -13,6 +13,7 @@ declare(strict_types=1);
namespace Sylius\Behat\Client;
use Sylius\Behat\Service\SprintfResponseEscaper;
use Symfony\Component\HttpFoundation\Response;
use Webmozart\Assert\Assert;
@ -147,6 +148,14 @@ final class ResponseChecker implements ResponseCheckerInterface
{
$content = json_decode($response->getContent(), true);
Assert::isArray(
$content,
SprintfResponseEscaper::provideMessageWithEscapedResponseContent(
'Content could not be parsed to array.',
$response
)
);
Assert::keyExists($content, $key);
return $content[$key];

View file

@ -17,6 +17,11 @@ use Behat\Behat\Context\Context;
use Sylius\Behat\Client\ApiClientInterface;
use Sylius\Behat\Client\Request;
use Sylius\Behat\Client\ResponseCheckerInterface;
use Sylius\Behat\Service\SharedStorageInterface;
use Sylius\Behat\Service\SprintfResponseEscaper;
use Sylius\Component\Core\Model\ProductInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
use Webmozart\Assert\Assert;
final class CartContext implements Context
@ -27,10 +32,17 @@ final class CartContext implements Context
/** @var ResponseCheckerInterface */
private $responseChecker;
public function __construct(ApiClientInterface $cartsClient, ResponseCheckerInterface $responseChecker)
{
/** @var SharedStorageInterface */
private $sharedStorage;
public function __construct(
ApiClientInterface $cartsClient,
ResponseCheckerInterface $responseChecker,
SharedStorageInterface $sharedStorage
) {
$this->cartsClient = $cartsClient;
$this->responseChecker = $responseChecker;
$this->sharedStorage = $sharedStorage;
}
/**
@ -41,6 +53,22 @@ final class CartContext implements Context
$this->cartsClient->create(Request::create('orders'));
}
/**
* @When /^I (?:add|added) (this product) to the cart$/
*/
public function iAddThisProductToTheCart(ProductInterface $product): void
{
$this->putProductToCart($product);
}
/**
* @When /^I add (\d+) of (them) to (?:the|my) cart$/
*/
public function iAddOfThemToMyCart(int $quantity, ProductInterface $product): void
{
$this->putProductToCart($product, $quantity);
}
/**
* @Then my cart should be empty
*/
@ -49,7 +77,109 @@ final class CartContext implements Context
$response = $this->cartsClient->getLastResponse();
Assert::true(
$this->responseChecker->isCreationSuccessful($response),
'Cart has not been created. Reason: ' . $response->getContent()
SprintfResponseEscaper::provideMessageWithEscapedResponseContent('Cart has not been created.', $response)
);
}
/**
* @Then I should be on my cart summary page
*/
public function iShouldBeOnMyCartSummaryPage(): void
{
// Intentionally left blank
}
/**
* @Then I should be notified that the product has been successfully added
*/
public function iShouldBeNotifiedThatTheProductHasBeenSuccessfullyAdded(): void
{
$response = $this->cartsClient->getLastResponse();
Assert::true(
$this->responseChecker->isUpdateSuccessful($response),
SprintfResponseEscaper::provideMessageWithEscapedResponseContent('Item has not been added.', $response)
);
}
/**
* @Then there should be one item in my cart
*/
public function thereShouldBeOneItemInMyCart(): void
{
$response = $this->cartsClient->getLastResponse();
$items = $this->responseChecker->getValue($response, 'items');
Assert::count($items, 1);
$this->sharedStorage->set('item', $items[0]);
}
/**
* @Then /^(this item) should have name "([^"]+)"$/
*/
public function thisItemShouldHaveName(array $item, string $productName): void
{
$response = $this->getProductForItem($item);
Assert::true(
$this->responseChecker->hasTranslation($response, 'en_US', 'name', $productName),
SprintfResponseEscaper::provideMessageWithEscapedResponseContent('Name not found.', $response)
);
}
/**
* @Then I should see :productName with quantity :quantity in my cart
*/
public function iShouldSeeWithQuantityInMyCart(string $productName, int $quantity): void
{
$cartResponse = $this->cartsClient->getLastResponse();
$items = $this->responseChecker->getValue($cartResponse, 'items');
foreach ($items as $item) {
$productResponse = $this->getProductForItem($item);
if ($this->responseChecker->hasTranslation($productResponse, 'en_US', 'name', $productName)) {
Assert::same(
$item['quantity'],
$quantity,
SprintfResponseEscaper::provideMessageWithEscapedResponseContent(
sprintf('Quantity did not match. Expected %s.', $quantity),
$cartResponse
)
);
}
}
}
private function putProductToCart(ProductInterface $product, int $quantity = 1): void
{
$this->cartsClient->create(Request::create('orders'));
$response = $this->cartsClient->getLastResponse();
$tokenValue = $this->responseChecker->getValue($response, 'tokenValue');
$request = Request::customItemAction('orders', $tokenValue, HttpRequest::METHOD_PATCH, 'items');
$request->updateContent([
'productCode' => $product->getCode(),
'quantity' => $quantity
]);
$this->cartsClient->executeCustomRequest($request);
}
private function getProductForItem(array $item): Response
{
if (!isset($item['variant']['product'])) {
throw new \InvalidArgumentException(
'Expected array to have variant key and variant to have product, but one these keys is missing. Current array: ' .
json_encode($item)
);
}
$this->cartsClient->executeCustomRequest(Request::custom(
$item['variant']['product'],
HttpRequest::METHOD_GET)
);
return $this->cartsClient->getLastResponse();
}
}

View file

@ -40,6 +40,13 @@
<service id="sylius.behat.admin_api_security" class="Sylius\Behat\Service\ApiSecurityService" public="true">
<argument type="service" id="test.client" />
<argument type="service" id="sylius.behat.shared_storage" />
<argument>admin-user-authentication-token</argument>
</service>
<service id="sylius.behat.shop_api_security" class="Sylius\Behat\Service\ApiSecurityService" public="true">
<argument type="service" id="test.client" />
<argument type="service" id="sylius.behat.shared_storage" />
<argument>shop-user-authentication-token</argument>
</service>
<service id="sylius.behat.shop_security" class="Sylius\Behat\Service\SecurityService" public="false">

View file

@ -21,6 +21,7 @@
<service id="sylius.behat.context.api.shop.cart" class="Sylius\Behat\Context\Api\Shop\CartContext">
<argument type="service" id="sylius.behat.api_platform_client.cart" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
<argument type="service" id="sylius.behat.shared_storage" />
</service>
<service id="sylius.behat.context.api.shop.channel" class="Sylius\Behat\Context\Api\Shop\ChannelContext">

View file

@ -205,6 +205,13 @@
<argument type="service" id="sylius.repository.shop_user" />
</service>
<service id="sylius.behat.context.setup.shop_api_security" class="Sylius\Behat\Context\Setup\ShopSecurityContext">
<argument type="service" id="sylius.behat.shared_storage" />
<argument type="service" id="sylius.behat.shop_api_security" />
<argument type="service" id="sylius.fixture.example_factory.shop_user" />
<argument type="service" id="sylius.repository.shop_user" />
</service>
<service id="sylius.behat.context.setup.shipping" class="Sylius\Behat\Context\Setup\ShippingContext">
<argument type="service" id="sylius.behat.shared_storage" />
<argument type="service" id="sylius.repository.shipping_method" />

View file

@ -8,6 +8,12 @@ default:
- sylius.behat.context.hook.doctrine_orm
- sylius.behat.context.setup.channel
- sylius.behat.context.setup.product
- sylius.behat.context.setup.shop_api_security
- sylius.behat.context.transform.lexical
- sylius.behat.context.transform.product
- sylius.behat.context.transform.shared_storage
- sylius.behat.context.api.shop.cart

View file

@ -17,6 +17,7 @@ use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserTo
use Sylius\Component\User\Model\UserInterface;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Webmozart\Assert\Assert;
final class ApiSecurityService implements SecurityServiceInterface
{
@ -26,24 +27,38 @@ final class ApiSecurityService implements SecurityServiceInterface
/** @var SharedStorageInterface */
private $sharedStorage;
public function __construct(AbstractBrowser $client, SharedStorageInterface $sharedStorage)
/** @var string */
private $loginEndpoint;
public function __construct(AbstractBrowser $client, SharedStorageInterface $sharedStorage, string $loginEndpoint)
{
$this->client = $client;
$this->sharedStorage = $sharedStorage;
$this->loginEndpoint = $loginEndpoint;
}
public function logIn(UserInterface $user): void
{
$this->client->request(
'POST',
'/new-api/admin-user-authentication-token',
'/new-api/' . $this->loginEndpoint,
[],
[],
['CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'],
json_encode(['email' => $user->getEmail(), 'password' => 'sylius'])
);
$token = json_decode($this->client->getResponse()->getContent(), true)['token'];
$response = $this->client->getResponse();
$content = json_decode($response->getContent(), true);
Assert::keyExists(
$content,
'token',
SprintfResponseEscaper::provideMessageWithEscapedResponseContent('Token not found.', $response)
);
$token = $content['token'];
$this->sharedStorage->set('token', $token);
}

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Behat\Service;
use Symfony\Component\HttpFoundation\Response;
final class SprintfResponseEscaper
{
public static function provideMessageWithEscapedResponseContent(string $message, Response $response): string
{
return sprintf(
'%s Received response: %s',
$message,
str_replace(
'%',
'%%',
$response->getContent()
)
);
}
}

View file

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Command\Cart;
class AddItemToCart
{
/** @var string|null */
public $tokenValue;
/**
* @var string
* @psalm-immutable
*/
public $productCode;
/**
* @var integer
* @psalm-immutable
*/
public $quantity;
public function __construct(string $productCode, int $quantity)
{
$this->productCode = $productCode;
$this->quantity = $quantity;
}
public static function createFromData(string $tokenValue, string $productCode, int $quantity): self
{
$command = new self($productCode, $quantity);
$command->tokenValue = $tokenValue;
return $command;
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Sylius\Bundle\ApiBundle\CommandHandler\Cart;
use Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart;
use Sylius\Component\Core\Factory\CartItemFactoryInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Core\Repository\ProductRepositoryInterface;
use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface;
use Sylius\Component\Order\Modifier\OrderModifierInterface;
use Sylius\Component\Order\Processor\OrderProcessorInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Webmozart\Assert\Assert;
final class AddItemToCartHandler implements MessageHandlerInterface
{
/** @var OrderRepositoryInterface */
private $orderRepository;
/** @var ProductRepositoryInterface */
private $productRepository;
/** @var OrderModifierInterface */
private $orderModifier;
/** @var CartItemFactoryInterface */
private $cartItemFactory;
/** @var OrderItemQuantityModifierInterface */
private $orderItemQuantityModifier;
/** @var OrderProcessorInterface */
private $orderProcessor;
public function __construct(
OrderRepositoryInterface $orderRepository,
ProductRepositoryInterface $productRepository,
OrderModifierInterface $orderModifier,
CartItemFactoryInterface $cartItemFactory,
OrderItemQuantityModifierInterface $orderItemQuantityModifier,
OrderProcessorInterface $orderProcessor
) {
$this->orderRepository = $orderRepository;
$this->productRepository = $productRepository;
$this->orderModifier = $orderModifier;
$this->cartItemFactory = $cartItemFactory;
$this->orderItemQuantityModifier = $orderItemQuantityModifier;
$this->orderProcessor = $orderProcessor;
}
public function __invoke(AddItemToCart $addItemToCart): OrderInterface
{
$product = $this->productRepository->findOneByCode($addItemToCart->productCode);
Assert::notNull($product);
$cart = $this->orderRepository->findOneBy([
'state' => OrderInterface::STATE_CART,
'tokenValue' => $addItemToCart->tokenValue
]);
Assert::notNull($cart);
$cartItem = $this->cartItemFactory->createForProduct($product);
$this->orderItemQuantityModifier->modify($cartItem, $addItemToCart->quantity);
$this->orderModifier->addToOrder($cart, $cartItem);
$this->orderProcessor->process($cart);
return $cart;
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\DataTransformer;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart;
use Sylius\Component\Core\Model\OrderInterface;
use Webmozart\Assert\Assert;
final class AddItemToCartInputDataTransformer implements DataTransformerInterface
{
public function transform($object, string $to, array $context = [])
{
/** @var AddItemToCart|mixed $object */
Assert::isInstanceOf($object, AddItemToCart::class);
/** @var OrderInterface $cart */
$cart = $context['object_to_populate'];
$object->tokenValue = $cart->getTokenValue();
return $object;
}
public function supportsTransformation($data, string $to, array $context = []): bool
{
return AddItemToCart::class === $context['input']['class'];
}
}

View file

@ -56,6 +56,20 @@
<attribute name="groups">order:update</attribute>
</attribute>
</itemOperation>
<itemOperation name="add_item">
<attribute name="security">is_granted('IS_AUTHENTICATED_ANONYMOUSLY')</attribute>
<attribute name="openapi_context">
<attribute name="summary">Cancels Order</attribute>
</attribute>
<attribute name="method">PATCH</attribute>
<attribute name="path">/orders/{id}/items</attribute>
<attribute name="messenger">input</attribute>
<attribute name="input">Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart</attribute>
<attribute name="denormalization_context">
<attribute name="groups">cart:add_item</attribute>
</attribute>
</itemOperation>
</itemOperations>
<property name="id" identifier="false" writable="false" />
@ -69,5 +83,6 @@
<property name="paymentState" writable="false" />
<property name="total" writable="false" />
<property name="orderPromotionTotal" writable="false" />
<property name="items" readable="true" writable="true" />
</resource>
</resources>

View file

@ -46,7 +46,6 @@
<itemOperations>
<itemOperation name="get">
<attribute name="security">is_granted('ROLE_API_ACCESS')</attribute>
<attribute name="path">products/{id}</attribute>
</itemOperation>
<itemOperation name="put">

View file

@ -0,0 +1,26 @@
<?xml version="1.0" ?>
<!--
This file is part of the Sylius package.
(c) Paweł Jędrzejewski
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd"
>
<class name="Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart">
<attribute name="productCode">
<group>cart:add_item</group>
</attribute>
<attribute name="quantity">
<group>cart:add_item</group>
</attribute>
</class>
</serializer>

View file

@ -59,5 +59,8 @@
<attribute name="tokenValue">
<group>order:read</group>
</attribute>
<attribute name="items">
<group>order:read</group>
</attribute>
</class>
</serializer>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" ?>
<!--
This file is part of the Sylius package.
(c) Paweł Jędrzejewski
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd"
>
<class name="Sylius\Component\Core\Model\OrderItem">
<attribute name="id">
<group>order:read</group>
</attribute>
<attribute name="variant">
<group>order:read</group>
</attribute>
<attribute name="quantity">
<group>order:read</group>
</attribute>
</class>
</serializer>

View file

@ -17,9 +17,11 @@
>
<class name="Sylius\Component\Core\Model\ProductVariant">
<attribute name="id">
<group>order:read</group>
<group>product_variant:read</group>
</attribute>
<attribute name="product">
<group>order:read</group>
<group>product_variant:read</group>
</attribute>
</class>

View file

@ -66,5 +66,10 @@
<service id="sylius.api.property_info.extractor.empty_list_extractor" class="Sylius\Bundle\ApiBundle\PropertyInfo\Extractor\EmptyPropertyListExtractor">
<tag name="property_info.list_extractor" priority="-2000" />
</service>
<service class="Sylius\Bundle\ApiBundle\DataTransformer\AddItemToCartInputDataTransformer" id="sylius_bundle_api.data_transformer.add_item_to_cart_input_data_transformer">
<tag name="api_platform.data_transformer"/>
</service>
</services>
</container>

View file

@ -30,5 +30,15 @@
<argument type="service" id="sylius.random_generator" />
<tag name="messenger.message_handler" />
</service>
<service id="Sylius\Bundle\ApiBundle\CommandHandler\Cart\AddItemToCartHandler">
<argument type="service" id="sylius.repository.order" />
<argument type="service" id="sylius.repository.product" />
<argument type="service" id="sylius.order_modifier" />
<argument type="service" id="sylius.factory.order_item" />
<argument type="service" id="sylius.order_item_quantity_modifier" />
<argument type="service" id="sylius.order_processing.order_processor" />
<tag name="messenger.message_handler" />
</service>
</services>
</container>

View file

@ -0,0 +1,121 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Cart;
use Doctrine\Persistence\ObjectManager;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart;
use Sylius\Bundle\ApiBundle\Command\PickupCart;
use Sylius\Bundle\ApiBundle\Command\RegisterShopUser;
use Sylius\Bundle\ApiBundle\Provider\CustomerProviderInterface;
use Sylius\Component\Channel\Context\ChannelContextInterface;
use Sylius\Component\Core\Factory\CartItemFactoryInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\OrderItemInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Core\Repository\ProductRepositoryInterface;
use Sylius\Component\Currency\Model\CurrencyInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface;
use Sylius\Component\Order\Modifier\OrderModifierInterface;
use Sylius\Component\Order\Processor\OrderProcessorInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Sylius\Component\Resource\Generator\RandomnessGeneratorInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
final class AddItemToCartHandlerSpec extends ObjectBehavior
{
function let(
OrderRepositoryInterface $orderRepository,
ProductRepositoryInterface $productRepository,
OrderModifierInterface $orderModifier,
CartItemFactoryInterface $cartItemFactory,
OrderItemQuantityModifierInterface $orderItemQuantityModifier,
OrderProcessorInterface $orderProcessor
): void {
$this->beConstructedWith(
$orderRepository,
$productRepository,
$orderModifier,
$cartItemFactory,
$orderItemQuantityModifier,
$orderProcessor
);
}
function it_is_a_message_handler(): void
{
$this->shouldImplement(MessageHandlerInterface::class);
}
function it_adds_simple_product_to_cart(
OrderRepositoryInterface $orderRepository,
ProductRepositoryInterface $productRepository,
OrderModifierInterface $orderModifier,
CartItemFactoryInterface $cartItemFactory,
OrderItemQuantityModifierInterface $orderItemQuantityModifier,
OrderProcessorInterface $orderProcessor,
OrderInterface $cart,
OrderItemInterface $cartItem,
ProductInterface $product
): void {
$orderRepository->findOneBy(['state' => OrderInterface::STATE_CART, 'tokenValue' => 'TOKEN'])->willReturn($cart);
$productRepository->findOneByCode('PRODUCT_CODE')->willReturn($product);
$cartItemFactory->createForProduct($product)->willReturn($cartItem);
$orderItemQuantityModifier->modify($cartItem, 5)->shouldBeCalled();
$orderModifier->addToOrder($cart, $cartItem)->shouldBeCalled();
$orderProcessor->process($cart)->shouldBeCalled();
$this(AddItemToCart::createFromData('TOKEN', 'PRODUCT_CODE', 5))->shouldReturn($cart);
}
function it_throws_an_exception_if_product_is_not_found(
ProductRepositoryInterface $productRepository,
CartItemFactoryInterface $cartItemFactory
): void {
$productRepository->findOneByCode('PRODUCT_CODE')->willReturn(null);
$cartItemFactory->createForProduct(Argument::any())->shouldNotBeCalled();
$this
->shouldThrow(\InvalidArgumentException::class)
->during('__invoke', [AddItemToCart::createFromData('TOKEN', 'PRODUCT_CODE', 1)])
;
}
function it_throws_an_exception_if_cart_is_not_found(
OrderRepositoryInterface $orderRepository,
ProductRepositoryInterface $productRepository,
CartItemFactoryInterface $cartItemFactory,
ProductInterface $product
): void {
$productRepository->findOneByCode('PRODUCT_CODE')->willReturn($product);
$orderRepository->findOneBy(['state' => OrderInterface::STATE_CART, 'tokenValue' => 'TOKEN'])->willReturn(null);
$cartItemFactory->createForProduct(Argument::any())->shouldNotBeCalled();
$this
->shouldThrow(\InvalidArgumentException::class)
->during('__invoke', [AddItemToCart::createFromData('TOKEN', 'PRODUCT_CODE', 1)])
;
}
}