From 25dc2772858ae5ced85ef379fcd5c3b1bbafbf4b Mon Sep 17 00:00:00 2001
From: Jakub 'Wolen' Tobiasz <80641364+jakubtobiasz@users.noreply.github.com>
Date: Thu, 30 Dec 2021 12:27:28 +0100
Subject: [PATCH] [OPSRC-417] Add API Platform support (#28)
* Enable sylius_api in the test application
---
.github/workflows/build.yml | 7 +-
.github/workflows/coding_standard.yml | 8 +-
.gitignore | 2 +
README.md | 2 +-
composer.json | 10 +-
phpstan.neon | 4 +
phpunit.xml.dist | 25 +
src/BitBagSyliusProductBundlePlugin.php | 9 +
src/Command/AddProductBundleToCartCommand.php | 56 +--
src/Command/OrderIdentityAwareInterface.php | 16 +
src/Command/ProductCodeAwareInterface.php | 16 +
src/Controller/OrderItemController.php | 41 +-
...dProductBundleToCartDtoDataTransformer.php | 50 ++
.../AuthenticationManagerPolyfillPass.php | 28 ++
src/Dto/AddProductBundleToCartDto.php | 88 ++++
.../AddProductBundleToCartDtoInterface.php | 33 ++
src/Dto/Api/AddProductBundleToCartDto.php | 51 ++
...dProductBundleItemToCartCommandFactory.php | 22 +
...undleItemToCartCommandFactoryInterface.php | 19 +
.../AddProductBundleToCartCommandFactory.php | 34 ++
...uctBundleToCartCommandFactoryInterface.php | 25 +
.../AddProductBundleToCartDtoFactory.php | 56 +++
...ProductBundleToCartDtoFactoryInterface.php | 25 +
src/Factory/OrderItemFactory.php | 54 +++
src/Factory/OrderItemFactoryInterface.php | 21 +
src/Factory/ProductBundleOrderItemFactory.php | 46 ++
...ProductBundleOrderItemFactoryInterface.php | 20 +
src/Form/Type/AddProductBundleToCartType.php | 4 +-
src/Handler/AddProductBundleToCartHandler.php | 69 +--
.../CartProcessor.php | 72 +++
.../CartProcessorInterface.php | 23 +
src/Resources/config/api_resources/Order.xml | 446 ++++++++++++++++++
.../config/api_resources/Product.xml | 131 +++++
.../config/api_resources/ProductBundle.xml | 99 ++++
.../api_resources/ProductBundleItem.xml | 28 ++
.../AddProductBundleToCartCommand.xml | 21 +
.../AddProductBundleToCartDto.xml | 21 +
.../config/serialization/OrderItem.xml | 21 +
.../config/serialization/Product.xml | 21 +
.../config/serialization/ProductBundle.xml | 46 ++
.../serialization/ProductBundleItem.xml | 27 ++
.../serialization/ProductBundleOrderItem.xml | 31 ++
.../config/serialization/ProductVariant.xml | 32 ++
.../ProductVariantTranslation.xml | 19 +
src/Resources/config/services.xml | 3 +
src/Resources/config/services/controller.xml | 3 +
src/Resources/config/services/factory.xml | 30 ++
src/Resources/config/services/handler.xml | 7 +-
src/Resources/config/services/processor.xml | 15 +
src/Resources/config/services/transformer.xml | 12 +
src/Resources/config/services/validator.xml | 32 ++
.../AddProductBundleToCartCommand.xml | 25 +
.../validation/AddProductBundleToCartDto.xml | 17 +
.../config/validation/ProductBundle.xml | 7 +
.../config/validation/ProductBundleItem.xml | 7 +
src/Resources/translations/validators.en.yml | 12 +
src/Validator/HasAvailableProductBundle.php | 34 ++
.../HasAvailableProductBundleValidator.php | 154 ++++++
src/Validator/HasExistingCart.php | 28 ++
src/Validator/HasExistingCartValidator.php | 50 ++
src/Validator/HasProductBundle.php | 30 ++
src/Validator/HasProductBundleValidator.php | 58 +++
src/Validator/Sequentially.php | 52 ++
src/Validator/SequentiallyValidator.php | 37 ++
tests/Api/Admin/OrderTest.php | 57 +++
tests/Api/Admin/ProductBundleTest.php | 184 ++++++++
tests/Api/AdminJsonApiTestCase.php | 42 ++
.../ORM/general/authentication.yml | 8 +
.../Api/DataFixtures/ORM/general/channels.yml | 20 +
tests/Api/DataFixtures/ORM/shop/orders.yml | 85 ++++
.../DataFixtures/ORM/shop/product_bundles.yml | 120 +++++
tests/Api/JsonApiTestCase.php | 31 ++
.../admin/get_order_with_bundle_response.json | 52 ++
...et_product_bundle_collection_response.json | 21 +
.../admin/get_product_bundle_response.json | 14 +
.../admin/post_product_bundle_response.json | 14 +
.../admin/put_product_bundle_response.json | 14 +
.../shop/get_bundled_product_response.json | 67 +++
.../get_not_bundled_product_response.json | 21 +
.../shop/get_order_with_bundle_response.json | 67 +++
.../shop/get_product_bundle_response.json | 51 ++
tests/Api/Shop/OrderTest.php | 91 ++++
tests/Api/Shop/ProductTest.php | 72 +++
tests/Api/Utils/CartHelperTrait.php | 38 ++
tests/Application/config/bundles.php | 4 +-
.../config/sylius/1.10/packages/_sylius.yaml | 2 +
.../config/sylius/1.10/packages/security.yaml | 42 +-
.../Application/config/sylius/1.8/bundles.php | 20 -
.../config/sylius/1.8/packages/_sylius.yaml | 2 -
.../config/sylius/1.8/packages/security.yaml | 159 -------
.../sylius/1.8/routes/sylius_admin_api.yaml | 3 -
.../Application/config/sylius/1.9/bundles.php | 20 -
.../config/sylius/1.9/packages/_sylius.yaml | 2 -
.../config/sylius/1.9/packages/security.yaml | 159 -------
.../sylius/1.9/routes/sylius_admin_api.yaml | 3 -
...ductBundleToCartDtoDataTransformerTest.php | 69 +++
...ductBundleItemToCartCommandFactoryTest.php | 29 ++
...dProductBundleToCartCommandFactoryTest.php | 49 ++
.../AddProductBundleToCartDtoFactoryTest.php | 64 +++
tests/Unit/Factory/OrderItemFactoryTest.php | 37 ++
.../ProductBundleOrderItemFactoryTest.php | 55 +++
.../CartProcessorTest.php | 241 ++++++++++
.../AddProductBundleToCartHandlerTest.php | 163 +++++++
...ddProductBundleItemToCartCommandMother.php | 22 +
.../AddProductBundleToCartDtoMother.php | 36 ++
.../Api/AddProductBundleToCartDtoMother.php | 21 +
tests/Unit/MotherObject/ChannelMother.php | 31 ++
tests/Unit/MotherObject/OrderItemMother.php | 22 +
tests/Unit/MotherObject/OrderMother.php | 45 ++
.../MotherObject/ProductBundleItemMother.php | 22 +
.../Unit/MotherObject/ProductBundleMother.php | 37 ++
tests/Unit/MotherObject/ProductMother.php | 76 +++
.../MotherObject/ProductVariantMother.php | 40 ++
tests/Unit/TypeExceptionMessage.php | 18 +
...HasAvailableProductBundleValidatorTest.php | 166 +++++++
.../HasExistingCartValidatorTest.php | 90 ++++
tests/Unit/Validator/HasProductBundleTest.php | 87 ++++
117 files changed, 4913 insertions(+), 484 deletions(-)
create mode 100644 phpunit.xml.dist
create mode 100644 src/Command/OrderIdentityAwareInterface.php
create mode 100644 src/Command/ProductCodeAwareInterface.php
create mode 100644 src/DataTransformer/AddProductBundleToCartDtoDataTransformer.php
create mode 100644 src/DependencyInjection/CompilerPass/AuthenticationManagerPolyfillPass.php
create mode 100644 src/Dto/AddProductBundleToCartDto.php
create mode 100644 src/Dto/AddProductBundleToCartDtoInterface.php
create mode 100644 src/Dto/Api/AddProductBundleToCartDto.php
create mode 100644 src/Factory/AddProductBundleItemToCartCommandFactory.php
create mode 100644 src/Factory/AddProductBundleItemToCartCommandFactoryInterface.php
create mode 100644 src/Factory/AddProductBundleToCartCommandFactory.php
create mode 100644 src/Factory/AddProductBundleToCartCommandFactoryInterface.php
create mode 100644 src/Factory/AddProductBundleToCartDtoFactory.php
create mode 100644 src/Factory/AddProductBundleToCartDtoFactoryInterface.php
create mode 100644 src/Factory/OrderItemFactory.php
create mode 100644 src/Factory/OrderItemFactoryInterface.php
create mode 100644 src/Factory/ProductBundleOrderItemFactory.php
create mode 100644 src/Factory/ProductBundleOrderItemFactoryInterface.php
create mode 100644 src/Handler/AddProductBundleToCartHandler/CartProcessor.php
create mode 100644 src/Handler/AddProductBundleToCartHandler/CartProcessorInterface.php
create mode 100644 src/Resources/config/api_resources/Order.xml
create mode 100644 src/Resources/config/api_resources/Product.xml
create mode 100644 src/Resources/config/api_resources/ProductBundle.xml
create mode 100644 src/Resources/config/api_resources/ProductBundleItem.xml
create mode 100644 src/Resources/config/serialization/AddProductBundleToCartCommand.xml
create mode 100644 src/Resources/config/serialization/AddProductBundleToCartDto.xml
create mode 100644 src/Resources/config/serialization/OrderItem.xml
create mode 100644 src/Resources/config/serialization/Product.xml
create mode 100644 src/Resources/config/serialization/ProductBundle.xml
create mode 100644 src/Resources/config/serialization/ProductBundleItem.xml
create mode 100644 src/Resources/config/serialization/ProductBundleOrderItem.xml
create mode 100644 src/Resources/config/serialization/ProductVariant.xml
create mode 100644 src/Resources/config/serialization/ProductVariantTranslation.xml
create mode 100644 src/Resources/config/services/processor.xml
create mode 100644 src/Resources/config/services/transformer.xml
create mode 100644 src/Resources/config/services/validator.xml
create mode 100644 src/Resources/config/validation/AddProductBundleToCartCommand.xml
create mode 100644 src/Resources/config/validation/AddProductBundleToCartDto.xml
create mode 100644 src/Validator/HasAvailableProductBundle.php
create mode 100644 src/Validator/HasAvailableProductBundleValidator.php
create mode 100644 src/Validator/HasExistingCart.php
create mode 100644 src/Validator/HasExistingCartValidator.php
create mode 100644 src/Validator/HasProductBundle.php
create mode 100644 src/Validator/HasProductBundleValidator.php
create mode 100644 src/Validator/Sequentially.php
create mode 100644 src/Validator/SequentiallyValidator.php
create mode 100644 tests/Api/Admin/OrderTest.php
create mode 100644 tests/Api/Admin/ProductBundleTest.php
create mode 100644 tests/Api/AdminJsonApiTestCase.php
create mode 100644 tests/Api/DataFixtures/ORM/general/authentication.yml
create mode 100644 tests/Api/DataFixtures/ORM/general/channels.yml
create mode 100644 tests/Api/DataFixtures/ORM/shop/orders.yml
create mode 100644 tests/Api/DataFixtures/ORM/shop/product_bundles.yml
create mode 100644 tests/Api/JsonApiTestCase.php
create mode 100644 tests/Api/Responses/admin/get_order_with_bundle_response.json
create mode 100644 tests/Api/Responses/admin/get_product_bundle_collection_response.json
create mode 100644 tests/Api/Responses/admin/get_product_bundle_response.json
create mode 100644 tests/Api/Responses/admin/post_product_bundle_response.json
create mode 100644 tests/Api/Responses/admin/put_product_bundle_response.json
create mode 100644 tests/Api/Responses/shop/get_bundled_product_response.json
create mode 100644 tests/Api/Responses/shop/get_not_bundled_product_response.json
create mode 100644 tests/Api/Responses/shop/get_order_with_bundle_response.json
create mode 100644 tests/Api/Responses/shop/get_product_bundle_response.json
create mode 100644 tests/Api/Shop/OrderTest.php
create mode 100644 tests/Api/Shop/ProductTest.php
create mode 100644 tests/Api/Utils/CartHelperTrait.php
create mode 100644 tests/Application/config/sylius/1.10/packages/_sylius.yaml
delete mode 100644 tests/Application/config/sylius/1.8/bundles.php
delete mode 100644 tests/Application/config/sylius/1.8/packages/_sylius.yaml
delete mode 100644 tests/Application/config/sylius/1.8/packages/security.yaml
delete mode 100644 tests/Application/config/sylius/1.8/routes/sylius_admin_api.yaml
delete mode 100644 tests/Application/config/sylius/1.9/bundles.php
delete mode 100644 tests/Application/config/sylius/1.9/packages/_sylius.yaml
delete mode 100644 tests/Application/config/sylius/1.9/packages/security.yaml
delete mode 100644 tests/Application/config/sylius/1.9/routes/sylius_admin_api.yaml
create mode 100644 tests/Unit/DataTransformer/AddProductBundleToCartDtoDataTransformerTest.php
create mode 100644 tests/Unit/Factory/AddProductBundleItemToCartCommandFactoryTest.php
create mode 100644 tests/Unit/Factory/AddProductBundleToCartCommandFactoryTest.php
create mode 100644 tests/Unit/Factory/AddProductBundleToCartDtoFactoryTest.php
create mode 100644 tests/Unit/Factory/OrderItemFactoryTest.php
create mode 100644 tests/Unit/Factory/ProductBundleOrderItemFactoryTest.php
create mode 100644 tests/Unit/Handler/AddProductBundleToCartHandler/CartProcessorTest.php
create mode 100644 tests/Unit/Handler/AddProductBundleToCartHandlerTest.php
create mode 100644 tests/Unit/MotherObject/AddProductBundleItemToCartCommandMother.php
create mode 100644 tests/Unit/MotherObject/AddProductBundleToCartDtoMother.php
create mode 100644 tests/Unit/MotherObject/Api/AddProductBundleToCartDtoMother.php
create mode 100644 tests/Unit/MotherObject/ChannelMother.php
create mode 100644 tests/Unit/MotherObject/OrderItemMother.php
create mode 100644 tests/Unit/MotherObject/OrderMother.php
create mode 100644 tests/Unit/MotherObject/ProductBundleItemMother.php
create mode 100644 tests/Unit/MotherObject/ProductBundleMother.php
create mode 100644 tests/Unit/MotherObject/ProductMother.php
create mode 100644 tests/Unit/MotherObject/ProductVariantMother.php
create mode 100644 tests/Unit/TypeExceptionMessage.php
create mode 100644 tests/Unit/Validator/HasAvailableProductBundleValidatorTest.php
create mode 100644 tests/Unit/Validator/HasExistingCartValidatorTest.php
create mode 100644 tests/Unit/Validator/HasProductBundleTest.php
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 49108a6e..3efa59d7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -23,7 +23,7 @@ jobs:
matrix:
php: [7.4, 7.3, 8.0]
symfony: [^4.4, ^5.2]
- sylius: [~1.8.0, ~1.9.0, ~1.10.0]
+ sylius: [~1.10.0]
node: [10.x]
mysql: [5.7]
@@ -173,11 +173,14 @@ jobs:
name: Run PHPSpec
run: vendor/bin/phpspec run --ansi -f progress --no-interaction
-
-
name: Run Behat
run: vendor/bin/behat --colors --strict -vvv --no-interaction || vendor/bin/behat --colors --strict -vvv --no-interaction --rerun
+ -
+ name: Run PHPUnit
+ run: ./vendor/bin/phpunit -c ./phpunit.xml.dist
+
-
name: Upload Behat logs
uses: actions/upload-artifact@v2
diff --git a/.github/workflows/coding_standard.yml b/.github/workflows/coding_standard.yml
index 7983f145..bae2ffea 100644
--- a/.github/workflows/coding_standard.yml
+++ b/.github/workflows/coding_standard.yml
@@ -23,7 +23,7 @@ jobs:
matrix:
php: [7.4, 7.3, 8.0]
symfony: [^4.4, ^5.2]
- sylius: [~1.8.0, ~1.9.0, ~1.10.0]
+ sylius: [~1.10.0]
node: [10.x]
mysql: [5.7]
@@ -82,11 +82,11 @@ jobs:
name: Restrict Sylius version
if: matrix.sylius != ''
run: composer require "sylius/sylius:${{ matrix.sylius }}" --no-update --no-scripts --no-interaction
-
+
-
name: Install PHP dependencies
run: composer install --no-interaction
-
+
- name: Run ECS
run: vendor/bin/ecs
@@ -98,4 +98,4 @@ jobs:
name: Run PHPStan in /test directory
run: ./vendor/bin/phpstan analyze --configuration=vendor/bitbag/coding-standard/phpstan.neon tests --level=5
-
+
diff --git a/.gitignore b/.gitignore
index 07229a68..5f8e7464 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,5 @@
/behat.yml
/phpspec.yml
+/phpunit.xml
+/.phpunit.result.cache
diff --git a/README.md b/README.md
index c0051ed9..2b8eb44d 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ This **open-source plugin was developed to help the Sylius community**. If you h
composer require bitbag/product-bundle-plugin
```
-2. Add plugin dependencies to your `config/bundles.php` file:
+2. Add plugin dependencies to your `config/bundles.php` file after `Sylius\Bundle\ApiBundle\SyliusApiBundle`.
```php
return [
diff --git a/composer.json b/composer.json
index de027354..e95b56a1 100644
--- a/composer.json
+++ b/composer.json
@@ -6,9 +6,10 @@
"license": "MIT",
"require": {
"php": "^7.3 || ^8.0",
- "sylius/sylius": "~1.8.0 || ~1.9.0 || ~1.10.0"
+ "sylius/sylius": "~1.10.0"
},
"require-dev": {
+ "ext-json": "*",
"behat/behat": "^3.6.1",
"behat/mink-selenium2-driver": "^1.4",
"dmore/behat-chrome-extension": "^1.3",
@@ -39,7 +40,12 @@
"vimeo/psalm": "^4.12",
"composer/xdebug-handler": "^2.0",
"friendsofphp/php-cs-fixer": "^3.0",
- "bitbag/coding-standard": "^1.0"
+ "bitbag/coding-standard": "^1.0",
+ "lchrusciel/api-test-case": "^5.1",
+ "polishsymfonycommunity/symfony-mocker-container": "^1.0"
+ },
+ "conflict": {
+ "doctrine/orm": "^2.10.0"
},
"autoload": {
"psr-4": {
diff --git a/phpstan.neon b/phpstan.neon
index f7eee75f..af854e06 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -3,6 +3,10 @@ parameters:
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
+ level: 7
+ paths:
+ - src
+
excludes_analyse:
# Makes PHPStan crash
- 'src/DependencyInjection/Configuration.php'
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 00000000..e5b1da95
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,25 @@
+
+
+
+
+
+ ./tests/Api
+
+
+ ./tests/Api
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BitBagSyliusProductBundlePlugin.php b/src/BitBagSyliusProductBundlePlugin.php
index b5853702..cb2f680d 100644
--- a/src/BitBagSyliusProductBundlePlugin.php
+++ b/src/BitBagSyliusProductBundlePlugin.php
@@ -10,10 +10,19 @@
namespace BitBag\SyliusProductBundlePlugin;
+use BitBag\SyliusProductBundlePlugin\DependencyInjection\CompilerPass\AuthenticationManagerPolyfillPass;
use Sylius\Bundle\CoreBundle\Application\SyliusPluginTrait;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class BitBagSyliusProductBundlePlugin extends Bundle
{
use SyliusPluginTrait;
+
+ public function build(ContainerBuilder $container): void
+ {
+ $container->addCompilerPass(new AuthenticationManagerPolyfillPass());
+
+ parent::build($container);
+ }
}
diff --git a/src/Command/AddProductBundleToCartCommand.php b/src/Command/AddProductBundleToCartCommand.php
index ea0e2ccd..7ef3e44c 100644
--- a/src/Command/AddProductBundleToCartCommand.php
+++ b/src/Command/AddProductBundleToCartCommand.php
@@ -10,57 +10,39 @@
namespace BitBag\SyliusProductBundlePlugin\Command;
-use BitBag\SyliusProductBundlePlugin\Entity\OrderItemInterface;
-use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface;
-use BitBag\SyliusProductBundlePlugin\Entity\ProductInterface;
-use Sylius\Component\Order\Model\OrderInterface;
-
-final class AddProductBundleToCartCommand
+final class AddProductBundleToCartCommand implements OrderIdentityAwareInterface, ProductCodeAwareInterface
{
- /** @var OrderInterface */
- private $cart;
-
- /** @var OrderItemInterface */
- private $cartItem;
+ /** @var int */
+ private $orderId;
- /** @var ProductInterface */
- private $product;
+ /** @var string */
+ private $productCode;
- /** @var AddProductBundleItemToCartCommand[] */
- private $productBundleItems = [];
+ /** @var int */
+ private $quantity;
public function __construct(
- OrderInterface $cart,
- OrderItemInterface $cartItem,
- ProductInterface $product
+ int $orderId,
+ string $productCode,
+ int $quantity = 1
) {
- $this->cart = $cart;
- $this->cartItem = $cartItem;
- $this->product = $product;
- assert(null !== $product->getProductBundle());
- /** @var ProductBundleItemInterface $productBundleItem */
- foreach ($product->getProductBundle()->getProductBundleItems() as $productBundleItem) {
- $this->productBundleItems[] = new AddProductBundleItemToCartCommand($productBundleItem);
- }
- }
-
- public function getProduct(): ProductInterface
- {
- return $this->product;
+ $this->orderId = $orderId;
+ $this->productCode = $productCode;
+ $this->quantity = $quantity;
}
- public function getProductBundleItems(): array
+ public function getOrderId(): int
{
- return $this->productBundleItems;
+ return $this->orderId;
}
- public function getCart(): OrderInterface
+ public function getProductCode(): string
{
- return $this->cart;
+ return $this->productCode;
}
- public function getCartItem(): OrderItemInterface
+ public function getQuantity(): int
{
- return $this->cartItem;
+ return $this->quantity;
}
}
diff --git a/src/Command/OrderIdentityAwareInterface.php b/src/Command/OrderIdentityAwareInterface.php
new file mode 100644
index 00000000..da12ee71
--- /dev/null
+++ b/src/Command/OrderIdentityAwareInterface.php
@@ -0,0 +1,16 @@
+messageBus = $messageBus;
+ $this->orderRepository = $orderRepository;
+ $this->addProductBundleToCartDtoFactory = $addProductBundleToCartDtoFactory;
+ $this->addProductBundleToCartCommandFactory = $addProductBundleToCartCommandFactory;
}
public function addProductBundleAction(Request $request): ?Response
@@ -90,9 +108,11 @@ public function addProductBundleAction(Request $request): ?Response
/** @var ProductInterface $product */
$product = $orderItem->getProduct();
assert(null !== $configuration->getFormType());
+
+ $addProductBundleToCartDto = $this->addProductBundleToCartDtoFactory->createNew($cart, $orderItem, $product);
$form = $this->getFormFactory()->create(
$configuration->getFormType(),
- new AddProductBundleToCartCommand($cart, $orderItem, $product),
+ $addProductBundleToCartDto,
$configuration->getFormOptions()
);
@@ -120,9 +140,10 @@ private function handleForm(
OrderItemInterface $orderItem,
Request $request
): ?Response {
- /** @var AddProductBundleToCartCommand $addProductBundleToCartCommand */
- $addProductBundleToCartCommand = $form->getData();
- $errors = $this->getCartItemErrors($addProductBundleToCartCommand->getCartItem());
+ /** @var AddProductBundleToCartDto $addProductBundleToCartDto */
+ $addProductBundleToCartDto = $form->getData();
+
+ $errors = $this->getCartItemErrors($addProductBundleToCartDto->getCartItem());
if (0 < count($errors)) {
$form = $this->getAddToCartFormWithErrors($errors, $form);
@@ -137,7 +158,15 @@ private function handleForm(
return $this->redirectHandler->redirectToIndex($configuration, $orderItem);
}
+
+ $cart = $addProductBundleToCartDto->getCart();
+ if (null === $cart->getId()) {
+ $this->orderRepository->add($cart);
+ }
+
+ $addProductBundleToCartCommand = $this->addProductBundleToCartCommandFactory->createFromDto($addProductBundleToCartDto);
$this->messageBus->dispatch($addProductBundleToCartCommand);
+
$resourceControllerEvent = $this->eventDispatcher->dispatchPostEvent(CartActions::ADD, $configuration, $orderItem);
if ($resourceControllerEvent->hasResponse()) {
return $resourceControllerEvent->getResponse();
diff --git a/src/DataTransformer/AddProductBundleToCartDtoDataTransformer.php b/src/DataTransformer/AddProductBundleToCartDtoDataTransformer.php
new file mode 100644
index 00000000..c7d70dc3
--- /dev/null
+++ b/src/DataTransformer/AddProductBundleToCartDtoDataTransformer.php
@@ -0,0 +1,50 @@
+getProductCode();
+ $quantity = $object->getQuantity();
+
+ return new AddProductBundleToCartCommand($cart->getId(), $productCode, $quantity);
+ }
+
+ public function supportsTransformation(
+ $data,
+ string $to,
+ array $context = []
+ ): bool {
+ return isset($context['input']['class']) && AddProductBundleToCartDto::class === $context['input']['class'];
+ }
+}
diff --git a/src/DependencyInjection/CompilerPass/AuthenticationManagerPolyfillPass.php b/src/DependencyInjection/CompilerPass/AuthenticationManagerPolyfillPass.php
new file mode 100644
index 00000000..78336020
--- /dev/null
+++ b/src/DependencyInjection/CompilerPass/AuthenticationManagerPolyfillPass.php
@@ -0,0 +1,28 @@
+has('security.authentication_manager')
+ &&
+ true === $container->has('security.authentication.manager')
+ ) {
+ $container->setAlias('security.authentication_manager', 'security.authentication.manager');
+ }
+ }
+}
diff --git a/src/Dto/AddProductBundleToCartDto.php b/src/Dto/AddProductBundleToCartDto.php
new file mode 100644
index 00000000..6a320a13
--- /dev/null
+++ b/src/Dto/AddProductBundleToCartDto.php
@@ -0,0 +1,88 @@
+cart = $cart;
+ $this->cartItem = $cartItem;
+ $this->product = $product;
+ $this->productBundleItems = new ArrayCollection($productBundleItems);
+ }
+
+ public function getCart(): OrderInterface
+ {
+ return $this->cart;
+ }
+
+ public function setCart(OrderInterface $cart): void
+ {
+ $this->cart = $cart;
+ }
+
+ public function getCartItem(): OrderItemInterface
+ {
+ return $this->cartItem;
+ }
+
+ public function setCartItem(OrderItemInterface $cartItem): void
+ {
+ $this->cartItem = $cartItem;
+ }
+
+ public function getProduct(): ProductInterface
+ {
+ return $this->product;
+ }
+
+ public function setProduct(ProductInterface $product): void
+ {
+ $this->product = $product;
+ }
+
+ public function getProductBundleItems(): ArrayCollection
+ {
+ return $this->productBundleItems;
+ }
+
+ public function getProductCode(): string
+ {
+ return $this->product->getCode() ?? '';
+ }
+}
diff --git a/src/Dto/AddProductBundleToCartDtoInterface.php b/src/Dto/AddProductBundleToCartDtoInterface.php
new file mode 100644
index 00000000..ffb8e23b
--- /dev/null
+++ b/src/Dto/AddProductBundleToCartDtoInterface.php
@@ -0,0 +1,33 @@
+productCode = $productCode;
+ $this->quantity = $quantity;
+ }
+
+ public function getOrderTokenValue(): ?string
+ {
+ return $this->orderTokenValue;
+ }
+
+ public function setOrderTokenValue(?string $orderTokenValue): void
+ {
+ $this->orderTokenValue = $orderTokenValue;
+ }
+
+ public function getProductCode(): string
+ {
+ return $this->productCode;
+ }
+
+ public function getQuantity(): int
+ {
+ return $this->quantity;
+ }
+}
diff --git a/src/Factory/AddProductBundleItemToCartCommandFactory.php b/src/Factory/AddProductBundleItemToCartCommandFactory.php
new file mode 100644
index 00000000..2b52e760
--- /dev/null
+++ b/src/Factory/AddProductBundleItemToCartCommandFactory.php
@@ -0,0 +1,22 @@
+getCart()->getId();
+ $productCode = $dto->getProduct()->getCode() ?? '';
+ $quantity = $dto->getCartItem()->getQuantity();
+
+ return $this->createNew($cartId, $productCode, $quantity);
+ }
+}
diff --git a/src/Factory/AddProductBundleToCartCommandFactoryInterface.php b/src/Factory/AddProductBundleToCartCommandFactoryInterface.php
new file mode 100644
index 00000000..46e22d0d
--- /dev/null
+++ b/src/Factory/AddProductBundleToCartCommandFactoryInterface.php
@@ -0,0 +1,25 @@
+addProductBundleItemToCartCommandFactory = $addProductBundleItemToCartCommandFactory;
+ }
+
+ public function createNew(
+ OrderInterface $order,
+ OrderItemInterface $orderItem,
+ ProductInterface $product
+ ): AddProductBundleToCartDtoInterface {
+ /** @var ProductBundleInterface $productBundle */
+ $productBundle = $product->getProductBundle();
+ $processedProductBundleItems = $this->getProcessedProductBundleItems($productBundle);
+
+ return new AddProductBundleToCartDto($order, $orderItem, $product, $processedProductBundleItems);
+ }
+
+ /**
+ * @return AddProductBundleItemToCartCommand[]
+ */
+ private function getProcessedProductBundleItems(ProductBundleInterface $productBundle): array
+ {
+ $addProductBundleItemToCartCommands = [];
+
+ foreach ($productBundle->getProductBundleItems() as $bundleItem) {
+ $addProductBundleItemToCartCommands[] = $this->addProductBundleItemToCartCommandFactory->createNew($bundleItem);
+ }
+
+ return $addProductBundleItemToCartCommands;
+ }
+}
diff --git a/src/Factory/AddProductBundleToCartDtoFactoryInterface.php b/src/Factory/AddProductBundleToCartDtoFactoryInterface.php
new file mode 100644
index 00000000..ef9846ab
--- /dev/null
+++ b/src/Factory/AddProductBundleToCartDtoFactoryInterface.php
@@ -0,0 +1,25 @@
+decoratedFactory = $decoratedFactory;
+ }
+
+ public function createNew(): OrderItemInterface
+ {
+ /** @var OrderItemInterface $orderItem */
+ $orderItem = $this->decoratedFactory->createNew();
+
+ return $orderItem;
+ }
+
+ public function createWithVariant(ProductVariantInterface $productVariant): OrderItemInterface
+ {
+ $orderItem = $this->createNew();
+ $orderItem->setVariant($productVariant);
+
+ return $orderItem;
+ }
+
+ public function createForProduct(ProductInterface $product): \Sylius\Component\Core\Model\OrderItemInterface
+ {
+ return $this->decoratedFactory->createForProduct($product);
+ }
+
+ public function createForCart(OrderInterface $order): \Sylius\Component\Core\Model\OrderItemInterface
+ {
+ return $this->decoratedFactory->createForCart($order);
+ }
+}
diff --git a/src/Factory/OrderItemFactoryInterface.php b/src/Factory/OrderItemFactoryInterface.php
new file mode 100644
index 00000000..308ed03e
--- /dev/null
+++ b/src/Factory/OrderItemFactoryInterface.php
@@ -0,0 +1,21 @@
+decoratedFactory = $decoratedFactory;
+ }
+
+ public function createNew(): ProductBundleOrderItemInterface
+ {
+ /** @var ProductBundleOrderItemInterface $productBundleOrderItem */
+ $productBundleOrderItem = $this->decoratedFactory->createNew();
+
+ return $productBundleOrderItem;
+ }
+
+ public function createFromProductBundleItem(ProductBundleItemInterface $bundleItem): ProductBundleOrderItemInterface
+ {
+ /** @var ProductBundleOrderItemInterface $productBundleOrderItem */
+ $productBundleOrderItem = $this->decoratedFactory->createNew();
+
+ $productBundleOrderItem->setProductBundleItem($bundleItem);
+ $productBundleOrderItem->setProductVariant($bundleItem->getProductVariant());
+ $productBundleOrderItem->setQuantity($bundleItem->getQuantity());
+
+ return $productBundleOrderItem;
+ }
+}
diff --git a/src/Factory/ProductBundleOrderItemFactoryInterface.php b/src/Factory/ProductBundleOrderItemFactoryInterface.php
new file mode 100644
index 00000000..d24cb5f4
--- /dev/null
+++ b/src/Factory/ProductBundleOrderItemFactoryInterface.php
@@ -0,0 +1,20 @@
+setAllowedTypes('product', ProductInterface::class)
->setDefaults([
- 'data_class' => AddProductBundleToCartCommand::class,
+ 'data_class' => AddProductBundleToCartDto::class,
])
;
}
diff --git a/src/Handler/AddProductBundleToCartHandler.php b/src/Handler/AddProductBundleToCartHandler.php
index 10c5152f..5ae0af0c 100644
--- a/src/Handler/AddProductBundleToCartHandler.php
+++ b/src/Handler/AddProductBundleToCartHandler.php
@@ -10,53 +10,54 @@
namespace BitBag\SyliusProductBundlePlugin\Handler;
-use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommand;
use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleToCartCommand;
-use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItemInterface;
-use Doctrine\ORM\EntityManagerInterface;
-use Sylius\Component\Order\Modifier\OrderModifierInterface;
-use Sylius\Component\Resource\Factory\FactoryInterface;
+use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleInterface;
+use BitBag\SyliusProductBundlePlugin\Entity\ProductInterface;
+use BitBag\SyliusProductBundlePlugin\Handler\AddProductBundleToCartHandler\CartProcessorInterface;
+use Sylius\Component\Core\Repository\OrderRepositoryInterface;
+use Sylius\Component\Core\Repository\ProductRepositoryInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
+use Webmozart\Assert\Assert;
final class AddProductBundleToCartHandler implements MessageHandlerInterface
{
- /** @var FactoryInterface */
- private $productBundleOrderItemFactory;
+ /** @var OrderRepositoryInterface */
+ private $orderRepository;
- /** @var OrderModifierInterface */
- private $orderModifier;
+ /** @var ProductRepositoryInterface */
+ private $productRepository;
- /** @var EntityManagerInterface */
- private $orderEntityManager;
+ /** @var CartProcessorInterface */
+ private $cartProcessor;
public function __construct(
- FactoryInterface $productBundleOrderItemFactory,
- OrderModifierInterface $orderModifier,
- EntityManagerInterface $orderEntityManager
+ OrderRepositoryInterface $orderRepository,
+ ProductRepositoryInterface $productRepository,
+ CartProcessorInterface $cartItemProcessor
) {
- $this->productBundleOrderItemFactory = $productBundleOrderItemFactory;
- $this->orderModifier = $orderModifier;
- $this->orderEntityManager = $orderEntityManager;
+ $this->orderRepository = $orderRepository;
+ $this->productRepository = $productRepository;
+ $this->cartProcessor = $cartItemProcessor;
}
public function __invoke(AddProductBundleToCartCommand $addProductBundleToCartCommand): void
{
- $cart = $addProductBundleToCartCommand->getCart();
- $cartItem = $addProductBundleToCartCommand->getCartItem();
-
- /** @var AddProductBundleItemToCartCommand $productBundleItem */
- foreach ($addProductBundleToCartCommand->getProductBundleItems() as $productBundleItem) {
- /** @var ProductBundleOrderItemInterface $productBundleOrderItem */
- $productBundleOrderItem = $this->productBundleOrderItemFactory->createNew();
-
- $productBundleOrderItem->setProductVariant($productBundleItem->getProductVariant());
- $productBundleOrderItem->setQuantity($productBundleItem->getQuantity());
- $productBundleOrderItem->setProductBundleItem($productBundleItem->getProductBundleItem());
- $cartItem->addProductBundleOrderItem($productBundleOrderItem);
- }
-
- $this->orderModifier->addToOrder($cart, $cartItem);
- $this->orderEntityManager->persist($cart);
- $this->orderEntityManager->flush();
+ $cart = $this->orderRepository->findCartById($addProductBundleToCartCommand->getOrderId());
+ Assert::notNull($cart);
+
+ /** @var ProductInterface|null $product */
+ $product = $this->productRepository->findOneByCode($addProductBundleToCartCommand->getProductCode());
+ Assert::notNull($product);
+ Assert::true($product->isBundle());
+
+ /** @var ProductBundleInterface|null $productBundle */
+ $productBundle = $product->getProductBundle();
+ Assert::notNull($productBundle);
+
+ $quantity = $addProductBundleToCartCommand->getQuantity();
+ Assert::greaterThan($quantity, 0);
+
+ $this->cartProcessor->process($cart, $productBundle, $quantity);
+ $this->orderRepository->add($cart);
}
}
diff --git a/src/Handler/AddProductBundleToCartHandler/CartProcessor.php b/src/Handler/AddProductBundleToCartHandler/CartProcessor.php
new file mode 100644
index 00000000..bb2ffe32
--- /dev/null
+++ b/src/Handler/AddProductBundleToCartHandler/CartProcessor.php
@@ -0,0 +1,72 @@
+orderItemQuantityModifier = $orderItemQuantityModifier;
+ $this->productBundleOrderItemFactory = $productBundleOrderItemFactory;
+ $this->orderModifier = $orderModifier;
+ $this->cartItemFactory = $cartItemFactory;
+ }
+
+ public function process(
+ OrderInterface $cart,
+ ProductBundleInterface $productBundle,
+ int $quantity
+ ): void {
+ Assert::greaterThan($quantity, 0);
+
+ $product = $productBundle->getProduct();
+ Assert::notNull($product);
+
+ /** @var ProductVariantInterface|false $productVariant */
+ $productVariant = $product->getVariants()->first();
+ Assert::notFalse($productVariant);
+
+ $cartItem = $this->cartItemFactory->createWithVariant($productVariant);
+ $this->orderItemQuantityModifier->modify($cartItem, $quantity);
+
+ foreach ($productBundle->getProductBundleItems() as $bundleItem) {
+ $productBundleOrderItem = $this->productBundleOrderItemFactory->createFromProductBundleItem($bundleItem);
+ $cartItem->addProductBundleOrderItem($productBundleOrderItem);
+ }
+
+ $this->orderModifier->addToOrder($cart, $cartItem);
+ }
+}
diff --git a/src/Handler/AddProductBundleToCartHandler/CartProcessorInterface.php b/src/Handler/AddProductBundleToCartHandler/CartProcessorInterface.php
new file mode 100644
index 00000000..fb01ece9
--- /dev/null
+++ b/src/Handler/AddProductBundleToCartHandler/CartProcessorInterface.php
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+ admin:order:read
+
+
+
+ sylius
+
+
+
+ GET
+ admin/orders
+
+
+
+ POST
+ /shop/orders
+ input
+ Sylius\Bundle\ApiBundle\Command\Cart\PickupCart
+
+ shop:order:create
+
+
+ Pickups a new cart. Provided locale code has to be one of available for a particular channel.
+
+
+
+ GET
+ /shop/orders
+
+
+ shop:order:read
+
+
+
+
+
+
+
+ GET
+ /admin/orders/{tokenValue}
+
+
+
+ GET
+ /shop/orders/{tokenValue}
+
+ shop:cart:read
+
+
+
+
+ DELETE
+ /shop/orders/{tokenValue}
+
+ Deletes cart
+
+
+ shop:order:read
+
+
+
+
+ PATCH
+ /admin/orders/{tokenValue}/cancel
+ false
+ sylius.api.order_state_machine_transition_applicator:cancel
+
+ admin:order:update
+
+
+ Cancels Order
+
+
+
+
+ PATCH
+ /shop/orders/{tokenValue}/items
+ input
+ Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart
+
+ shop:cart:read
+
+
+ shop:cart:add_item
+
+
+ Adds Item to cart
+
+
+
+
+ PATCH
+ /shop/orders/{tokenValue}/product-bundle
+ 200
+ input
+ BitBag\SyliusProductBundlePlugin\Dto\Api\AddProductBundleToCartDto
+ false
+
+ Default
+
+
+ shop:cart:add_product_bundle
+
+
+ Adds Product Bundle to cart
+
+
+ Product bundle added to the cart
+
+
+
+
+
+
+ PATCH
+ /shop/orders/{tokenValue}/address
+ input
+ Sylius\Bundle\ApiBundle\Command\Checkout\AddressOrder
+
+ shop:cart:address
+
+
+ shop:cart:read
+
+
+ Addresses cart to given location, logged in Customer does not have to provide an email
+
+
+
+
+ PATCH
+
+ sylius
+
+ /shop/orders/{tokenValue}/shipments/{shipmentId}
+ input
+ Sylius\Bundle\ApiBundle\Command\Checkout\ChooseShippingMethod
+
+ shop:cart:select_shipping_method
+
+
+ shop:cart:read
+
+
+ Selects shipping methods for particular shipment
+
+
+ tokenValue
+ path
+ true
+
+ string
+
+
+
+ shipmentId
+ path
+ true
+
+ string
+
+
+
+
+
+
+
+ PATCH
+ /shop/orders/{tokenValue}/payments/{paymentId}
+ input
+ Sylius\Bundle\ApiBundle\Command\Checkout\ChoosePaymentMethod
+
+ shop:cart:select_payment_method
+
+
+ shop:cart:read
+
+
+ Selects payment methods for particular payment
+
+
+ tokenValue
+ path
+ true
+
+ string
+
+
+
+ paymentId
+ path
+ true
+
+ string
+
+
+
+
+
+
+
+ PATCH
+ /shop/account/orders/{tokenValue}/payments/{paymentId}
+ input
+ Sylius\Bundle\ApiBundle\Command\Account\ChangePaymentMethod
+
+ shop:order:account:change_payment_method
+
+
+ shop:order:account:read
+
+
+ Change the payment method as logged shop user
+
+
+ tokenValue
+ path
+ true
+
+ string
+
+
+
+ paymentId
+ path
+ true
+
+ string
+
+
+
+
+
+
+
+ GET
+ sylius.api.get_configuration_action
+ /shop/orders/{tokenValue}/payments/{paymentId}/configuration
+
+ Retrieve payment method configuration
+
+
+ tokenValue
+ path
+ true
+
+ string
+
+
+
+ paymentId
+ path
+ true
+
+ string
+
+
+
+
+
+
+
+ PATCH
+ /shop/orders/{tokenValue}/complete
+
+ sylius
+ sylius_checkout_complete
+
+ input
+ Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder
+
+ shop:cart:complete
+
+
+ shop:cart:read
+
+
+ Completes checkout
+
+
+
+
+ DELETE
+ /shop/orders/{tokenValue}/items/{itemId}
+ input
+ Sylius\Bundle\ApiBundle\Controller\DeleteOrderItemAction
+ false
+
+ shop:cart:remove_item
+
+
+
+
+ tokenValue
+ path
+ true
+
+ string
+
+
+
+ itemId
+ path
+ true
+
+ string
+
+
+
+
+
+
+
+ PATCH
+ /shop/orders/{tokenValue}/items/{orderItemId}
+ input
+ Sylius\Bundle\ApiBundle\Command\Cart\ChangeItemQuantityInCart
+
+ shop:cart:change_quantity
+
+
+ Changes quantity of order item
+
+
+ tokenValue
+ path
+ true
+
+ string
+
+
+
+ orderItemId
+ path
+ true
+
+ string
+
+
+
+
+
+
+
+ PATCH
+ /shop/orders/{tokenValue}/apply-coupon
+ input
+ Sylius\Bundle\ApiBundle\Command\Cart\ApplyCouponToCart
+
+ shop:cart:apply_coupon
+
+
+ Applies coupon to cart
+
+
+
+
+ PUT
+ /shop/orders/{tokenValue}
+
+ admin:cart:update
+
+
+
+
+
+
+ GET
+ /shop/orders/{tokenValue}/items
+
+
+
+ GET
+ /admin/orders/{tokenValue}/shipments
+
+
+
+ GET
+ /admin/orders/{tokenValue}/payments
+
+
+
+ GET
+ /shop/orders/{tokenValue}/adjustments
+
+
+
+ GET
+ /shop/orders/{tokenValue}/payments/{payments}/methods
+
+
+
+ GET
+ /shop/orders/{tokenValue}/shipments/{shipments}/methods
+
+
+
+ GET
+ /shop/orders/{tokenValue}/items/{items}/adjustments
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/api_resources/Product.xml b/src/Resources/config/api_resources/Product.xml
new file mode 100644
index 00000000..854c50ed
--- /dev/null
+++ b/src/Resources/config/api_resources/Product.xml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+ sylius
+
+
+ ASC
+
+
+
+
+ GET
+ /admin/products
+
+ sylius.api.product_name_filter
+ sylius.api.product_order_filter
+ sylius.api.product_taxon_code_filter
+ sylius.api.translation_order_name_and_locale_filter
+
+
+ admin:product:read
+
+
+
+
+ GET
+ /shop/products
+
+ sylius.api.product_name_filter
+ sylius.api.product_order_filter
+ sylius.api.product_taxon_code_filter
+ sylius.api.translation_order_name_and_locale_filter
+ sylius.api.product_taxon_filter
+
+
+ shop:product:read
+
+
+
+
+ POST
+ /admin/products
+
+ admin:product:create
+
+
+
+
+
+
+ GET
+ /admin/products/{code}
+
+ Use code to retrieve a product resource.
+
+
+ admin:product:read
+
+
+
+
+ GET
+ /shop/products/{code}
+
+ Use code to retrieve a product resource.
+
+
+ shop:product:read
+
+
+
+
+ PUT
+ /admin/products/{code}
+
+ admin:product:update
+
+
+
+
+ DELETE
+ /admin/products/{code}
+
+
+
+
+
+ /shop/products/{code}/bundle
+
+
+
+
+
+
+
+
+
+ object
+
+
+ string
+ string
+ string
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/api_resources/ProductBundle.xml b/src/Resources/config/api_resources/ProductBundle.xml
new file mode 100644
index 00000000..09ca5316
--- /dev/null
+++ b/src/Resources/config/api_resources/ProductBundle.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+ GET
+ /admin/product-bundles
+
+
+ admin:product_bundle:read
+
+
+
+
+
+ POST
+ /admin/product-bundles
+
+ admin:product_bundle:create
+
+
+
+
+
+ GET
+ /admin/product-bundles/{id}
+
+
+ admin:product_bundle:read
+
+
+
+
+ PUT
+ /admin/product-bundles/{id}
+
+
+ admin:product_bundle:update
+
+
+
+
+ DELETE
+ /admin/product-bundles/{id}
+
+
+ GET
+ /shop/product-bundles/{id}
+
+
+ shop:product_bundle:read
+
+
+
+
+ PATCH
+ /shop/product-bundles/{id}/add-to-cart
+ 200
+ input
+ BitBag\SyliusProductBundlePlugin\Command\AddProductBundleToCartCommand
+ false
+
+
+ shop:product_bundle:add_to_cart
+
+
+
+
+ Adds the product bundle to the cart retrieved by token.
+
+
+
+ Product bundle added to the cart
+
+
+
+
+
+
+
+
+
+ shop:product_bundle:read
+
+
+
+
+
+
diff --git a/src/Resources/config/api_resources/ProductBundleItem.xml b/src/Resources/config/api_resources/ProductBundleItem.xml
new file mode 100644
index 00000000..abb91cbe
--- /dev/null
+++ b/src/Resources/config/api_resources/ProductBundleItem.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+ GET
+ /shop/product-bundle-items/{id}
+
+
+ shop:product_bundle_items:read
+
+
+
+
+
+
diff --git a/src/Resources/config/serialization/AddProductBundleToCartCommand.xml b/src/Resources/config/serialization/AddProductBundleToCartCommand.xml
new file mode 100644
index 00000000..38abb5de
--- /dev/null
+++ b/src/Resources/config/serialization/AddProductBundleToCartCommand.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+ shop:product_bundle:add_to_cart
+
+
+ shop:product_bundle:add_to_cart
+
+
+
diff --git a/src/Resources/config/serialization/AddProductBundleToCartDto.xml b/src/Resources/config/serialization/AddProductBundleToCartDto.xml
new file mode 100644
index 00000000..7d6fe771
--- /dev/null
+++ b/src/Resources/config/serialization/AddProductBundleToCartDto.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+ shop:cart:add_product_bundle
+
+
+ shop:cart:add_product_bundle
+
+
+
diff --git a/src/Resources/config/serialization/OrderItem.xml b/src/Resources/config/serialization/OrderItem.xml
new file mode 100644
index 00000000..0ec5838b
--- /dev/null
+++ b/src/Resources/config/serialization/OrderItem.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+ admin:order:read
+ admin:order_item:read
+ shop:order_item:read
+ shop:cart:read
+
+
+
diff --git a/src/Resources/config/serialization/Product.xml b/src/Resources/config/serialization/Product.xml
new file mode 100644
index 00000000..2d762ec0
--- /dev/null
+++ b/src/Resources/config/serialization/Product.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+ shop:product:read
+
+
+ shop:product:read
+
+
+
diff --git a/src/Resources/config/serialization/ProductBundle.xml b/src/Resources/config/serialization/ProductBundle.xml
new file mode 100644
index 00000000..e55042cf
--- /dev/null
+++ b/src/Resources/config/serialization/ProductBundle.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+ admin:product_bundle:read
+
+
+ shop:product_bundle:read
+ admin:product_bundle:read
+ admin:product_bundle:create
+ admin:product_bundle:update
+
+
+ shop:product:read
+ shop:product_bundle:read
+ admin:product_bundle:read
+ admin:product_bundle:create
+ admin:product_bundle:update
+
+
+ admin:product_bundle:create
+ admin:product_bundle:update
+
+
+ shop:product:read
+ shop:product_bundle:read
+ admin:product_bundle:read
+
+
+ admin:product_bundle:read
+
+
+ admin:product_bundle:read
+
+
+
diff --git a/src/Resources/config/serialization/ProductBundleItem.xml b/src/Resources/config/serialization/ProductBundleItem.xml
new file mode 100644
index 00000000..fb599916
--- /dev/null
+++ b/src/Resources/config/serialization/ProductBundleItem.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ admin:product_bundle:create
+ admin:product_bundle:update
+ shop:product:read
+ shop:product_bundle:read
+
+
+ admin:product_bundle:create
+ admin:product_bundle:update
+ shop:product:read
+ shop:product_bundle:read
+
+
+
diff --git a/src/Resources/config/serialization/ProductBundleOrderItem.xml b/src/Resources/config/serialization/ProductBundleOrderItem.xml
new file mode 100644
index 00000000..6b068a55
--- /dev/null
+++ b/src/Resources/config/serialization/ProductBundleOrderItem.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+ admin:order:read
+ admin:order_item:read
+
+
+ admin:order:read
+ admin:order_item:read
+ shop:order_item:read
+ shop:cart:read
+
+
+ admin:order:read
+ admin:order_item:read
+ shop:order_item:read
+ shop:cart:read
+
+
+
diff --git a/src/Resources/config/serialization/ProductVariant.xml b/src/Resources/config/serialization/ProductVariant.xml
new file mode 100644
index 00000000..26fadc39
--- /dev/null
+++ b/src/Resources/config/serialization/ProductVariant.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ shop:product:read
+ shop:product_bundle:read
+ shop:order_item:read
+ shop:cart:read
+
+
+ shop:product_bundle:read
+
+
+ shop:product:read
+ shop:product_bundle:read
+
+
+ shop:product:read
+ shop:product_bundle:read
+
+
+
diff --git a/src/Resources/config/serialization/ProductVariantTranslation.xml b/src/Resources/config/serialization/ProductVariantTranslation.xml
new file mode 100644
index 00000000..50bf19f2
--- /dev/null
+++ b/src/Resources/config/serialization/ProductVariantTranslation.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ shop:product:read
+ shop:product_bundle:read
+
+
+
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index e96f46cd..0ef0affb 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -7,6 +7,9 @@
+
+
+
diff --git a/src/Resources/config/services/controller.xml b/src/Resources/config/services/controller.xml
index ca789898..c51fcb10 100644
--- a/src/Resources/config/services/controller.xml
+++ b/src/Resources/config/services/controller.xml
@@ -4,6 +4,9 @@
+
+
+
diff --git a/src/Resources/config/services/factory.xml b/src/Resources/config/services/factory.xml
index c0360415..26e912a5 100644
--- a/src/Resources/config/services/factory.xml
+++ b/src/Resources/config/services/factory.xml
@@ -6,5 +6,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/services/handler.xml b/src/Resources/config/services/handler.xml
index 7e4f6394..66cfaf68 100644
--- a/src/Resources/config/services/handler.xml
+++ b/src/Resources/config/services/handler.xml
@@ -3,9 +3,10 @@
-
-
-
+
+
+
+
diff --git a/src/Resources/config/services/processor.xml b/src/Resources/config/services/processor.xml
new file mode 100644
index 00000000..dec8a8cf
--- /dev/null
+++ b/src/Resources/config/services/processor.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/services/transformer.xml b/src/Resources/config/services/transformer.xml
new file mode 100644
index 00000000..1cacdd6a
--- /dev/null
+++ b/src/Resources/config/services/transformer.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/services/validator.xml b/src/Resources/config/services/validator.xml
new file mode 100644
index 00000000..9994250d
--- /dev/null
+++ b/src/Resources/config/services/validator.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/validation/AddProductBundleToCartCommand.xml b/src/Resources/config/validation/AddProductBundleToCartCommand.xml
new file mode 100644
index 00000000..0b6f78bc
--- /dev/null
+++ b/src/Resources/config/validation/AddProductBundleToCartCommand.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/validation/AddProductBundleToCartDto.xml b/src/Resources/config/validation/AddProductBundleToCartDto.xml
new file mode 100644
index 00000000..73877de2
--- /dev/null
+++ b/src/Resources/config/validation/AddProductBundleToCartDto.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/validation/ProductBundle.xml b/src/Resources/config/validation/ProductBundle.xml
index 59843d06..6557647b 100644
--- a/src/Resources/config/validation/ProductBundle.xml
+++ b/src/Resources/config/validation/ProductBundle.xml
@@ -1,4 +1,11 @@
+
+
+
+
+
+
productRepository = $productRepository;
+ $this->orderRepository = $orderRepository;
+ $this->availabilityChecker = $availabilityChecker;
+ }
+
+ /**
+ * @param AddProductBundleToCartCommand|mixed $value
+ * @param HasAvailableProductBundle|Constraint $constraint
+ */
+ public function validate($value, Constraint $constraint): void
+ {
+ Assert::isInstanceOf($value, AddProductBundleToCartCommand::class);
+ Assert::isInstanceOf($constraint, HasAvailableProductBundle::class);
+
+ $product = $this->productRepository->findOneByCode($value->getProductCode());
+ Assert::notNull($product);
+
+ if (false === $this->validateIfProductIsEnabled($product)) {
+ return;
+ }
+
+ /** @var ProductVariantInterface $productVariant */
+ $productVariant = $product->getVariants()->first();
+ if (false === $this->validateIfProductVariantIsEnabled($productVariant)) {
+ return;
+ }
+
+ /** @var OrderInterface|null $cart */
+ $cart = $this->orderRepository->findCartById($value->getOrderId());
+ Assert::notNull($cart);
+
+ if (false === $this->validateIfProductAndCartChannelsMatch($product, $cart)) {
+ return;
+ }
+
+ $targetQuantity = $value->getQuantity() + $this->getCurrentProductVariantQuantityFromCart($cart, $productVariant);
+ $this->validateProductVariantStock($productVariant, $targetQuantity);
+ }
+
+ private function validateIfProductIsEnabled(ProductInterface $product): bool
+ {
+ if (!$product->isEnabled()) {
+ $this->context->addViolation(HasAvailableProductBundle::PRODUCT_DISABLED_MESSAGE, [
+ '{{ code }}' => $product->getCode(),
+ ]);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private function validateIfProductVariantIsEnabled(ProductVariantInterface $productVariant): bool
+ {
+ if (!$productVariant->isEnabled()) {
+ $this->context->addViolation(HasAvailableProductBundle::PRODUCT_VARIANT_DISABLED_MESSAGE, [
+ '{{ code }}' => $productVariant->getCode(),
+ ]);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private function validateIfProductAndCartChannelsMatch(ProductInterface $product, OrderInterface $cart): bool
+ {
+ /** @var ChannelInterface $channel */
+ $channel = $cart->getChannel();
+
+ if (!$product->hasChannel($channel)) {
+ $this->context->addViolation(HasAvailableProductBundle::PRODUCT_DOESNT_EXIST_IN_CHANNEL_MESSAGE, [
+ '{{ channel }}' => $channel->getName(),
+ '{{ code }}' => $product->getCode(),
+ ]);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private function validateProductVariantStock(ProductVariantInterface $productVariant, int $targetQuantity): void
+ {
+ if ($this->availabilityChecker->isStockSufficient($productVariant, $targetQuantity)) {
+ return;
+ }
+
+ $this->context->addViolation(
+ HasAvailableProductBundle::PRODUCT_VARIANT_INSUFFICIENT_STOCK_MESSAGE,
+ [
+ '{{ code }}' => $productVariant->getCode(),
+ ]
+ );
+ }
+
+ private function getCurrentProductVariantQuantityFromCart(
+ OrderInterface $cart,
+ ProductVariantInterface $productVariant
+ ): int {
+ /** @var OrderItemInterface $item */
+ foreach ($cart->getItems() as $item) {
+ /** @var ProductVariantInterface $itemProductVariant */
+ $itemProductVariant = $item->getVariant();
+
+ if ($productVariant->isTracked() && $itemProductVariant->getCode() === $productVariant->getCode()) {
+ return $item->getQuantity();
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/Validator/HasExistingCart.php b/src/Validator/HasExistingCart.php
new file mode 100644
index 00000000..0c5b48f6
--- /dev/null
+++ b/src/Validator/HasExistingCart.php
@@ -0,0 +1,28 @@
+orderRepository = $orderRepository;
+ }
+
+ /**
+ * @param OrderIdentityAwareInterface|mixed $value
+ * @param HasExistingCart|Constraint $constraint
+ */
+ public function validate($value, Constraint $constraint): void
+ {
+ Assert::isInstanceOf($constraint, HasExistingCart::class);
+
+ if (!$value instanceof OrderIdentityAwareInterface) {
+ throw new UnexpectedValueException($value, OrderIdentityAwareInterface::class);
+ }
+
+ $cart = $this->orderRepository->findCartById($value->getOrderId());
+
+ if (null !== $cart) {
+ return;
+ }
+
+ $this->context->addViolation(HasExistingCart::CART_DOESNT_EXIST_MESSAGE);
+ }
+}
diff --git a/src/Validator/HasProductBundle.php b/src/Validator/HasProductBundle.php
new file mode 100644
index 00000000..1791d5f9
--- /dev/null
+++ b/src/Validator/HasProductBundle.php
@@ -0,0 +1,30 @@
+productRepository = $productRepository;
+ }
+
+ /**
+ * @param ProductCodeAwareInterface|mixed $value
+ * @param HasProductBundle|Constraint $constraint
+ */
+ public function validate($value, Constraint $constraint): void
+ {
+ Assert::isInstanceOf($constraint, HasProductBundle::class);
+
+ if (!$value instanceof ProductCodeAwareInterface) {
+ throw new UnexpectedValueException($value, ProductCodeAwareInterface::class);
+ }
+
+ /** @var ProductInterface|null $product */
+ $product = $this->productRepository->findOneByCode($value->getProductCode());
+
+ if (null === $product) {
+ $this->context->addViolation(HasProductBundle::PRODUCT_DOESNT_EXIST_MESSAGE);
+
+ return;
+ }
+
+ if (!$product->isBundle()) {
+ $this->context->addViolation(HasProductBundle::NOT_A_BUNDLE_MESSAGE);
+
+ return;
+ }
+ }
+}
diff --git a/src/Validator/Sequentially.php b/src/Validator/Sequentially.php
new file mode 100644
index 00000000..c92c6c42
--- /dev/null
+++ b/src/Validator/Sequentially.php
@@ -0,0 +1,52 @@
+|string
+ */
+ public function getTargets()
+ {
+ return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT];
+ }
+
+ public function validatedBy(): string
+ {
+ return 'bitbag_sylius_product_bundle_validator_sequentially';
+ }
+}
diff --git a/src/Validator/SequentiallyValidator.php b/src/Validator/SequentiallyValidator.php
new file mode 100644
index 00000000..280c2a6b
--- /dev/null
+++ b/src/Validator/SequentiallyValidator.php
@@ -0,0 +1,37 @@
+context;
+
+ $validator = $context->getValidator()->inContext($context);
+
+ $originalCount = $validator->getViolations()->count();
+
+ foreach ($constraint->constraints as $c) {
+ if ($originalCount !== $validator->validate($value, $c)->getViolations()->count()) {
+ break;
+ }
+ }
+ }
+}
diff --git a/tests/Api/Admin/OrderTest.php b/tests/Api/Admin/OrderTest.php
new file mode 100644
index 00000000..4ffb59eb
--- /dev/null
+++ b/tests/Api/Admin/OrderTest.php
@@ -0,0 +1,57 @@
+fixtures = $this->loadFixturesFromFiles([
+ 'general/channels.yml',
+ 'general/authentication.yml',
+ 'shop/product_bundles.yml',
+ 'shop/orders.yml',
+ ]);
+ $authToken = $this->getAuthToken('api@example.com', 'sylius');
+ $this->authHeaders = $this->getHeaders($authToken);
+ }
+
+ /** @test */
+ public function it_gets_order_data_containing_info_about_bundled_items(): void
+ {
+ /** @var OrderInterface $order */
+ $order = $this->fixtures['order_with_bundle'];
+
+ $this->client->request(
+ Request::METHOD_GET,
+ sprintf(self::ENDPOINT_ORDERS_ITEM, $order->getTokenValue()),
+ [],
+ [],
+ $this->authHeaders
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'admin/get_order_with_bundle_response', Response::HTTP_OK);
+ }
+}
diff --git a/tests/Api/Admin/ProductBundleTest.php b/tests/Api/Admin/ProductBundleTest.php
new file mode 100644
index 00000000..c41f5255
--- /dev/null
+++ b/tests/Api/Admin/ProductBundleTest.php
@@ -0,0 +1,184 @@
+fixtures = $this->loadFixturesFromFiles(['general/channels.yml', 'general/authentication.yml', 'shop/product_bundles.yml']);
+ $authToken = $this->getAuthToken('api@example.com', 'sylius');
+ $this->authHeaders = $this->getHeaders($authToken);
+ }
+
+ /** @test */
+ public function it_gets_product_bundles_collection(): void
+ {
+ $this->client->request(
+ Request::METHOD_GET,
+ self::ENDPOINT_PRODUCT_BUNDLES_COLLECTION,
+ [],
+ [],
+ $this->authHeaders
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'admin/get_product_bundle_collection_response', Response::HTTP_OK);
+ }
+
+ /** @test */
+ public function it_gets_product_bundle(): void
+ {
+ /** @var ProductBundleInterface $productBundleId */
+ $productBundleId = $this->fixtures['productBundle1'];
+
+ $this->client->request(
+ Request::METHOD_GET,
+ sprintf(self::ENDPOINT_PRODUCT_BUNDLES_ITEM, $productBundleId->getId()),
+ [],
+ [],
+ $this->authHeaders
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'admin/get_product_bundle_response', Response::HTTP_OK);
+ }
+
+ /** @test */
+ public function it_creates_product_bundle(): void
+ {
+ $johnnyWalkerBlack = $this->createProductBundleItemObject('JOHNNY_WALKER_BLACK');
+ $johnnyWalkerBlue = $this->createProductBundleItemObject('JOHNNY_WALKER_BLUE');
+
+ $this->client->request(
+ Request::METHOD_POST,
+ self::ENDPOINT_PRODUCT_BUNDLES_COLLECTION,
+ [],
+ [],
+ $this->authHeaders,
+ json_encode([
+ 'product' => self::JOHNNY_WALKER_BUNDLE_PRODUCT_IRI,
+ 'items' => [
+ $johnnyWalkerBlack,
+ $johnnyWalkerBlue,
+ ],
+ 'isPacked' => true,
+ ], \JSON_THROW_ON_ERROR)
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'admin/post_product_bundle_response', Response::HTTP_CREATED);
+ }
+
+ /** @test */
+ public function it_updates_product_bundle(): void
+ {
+ /** @var ProductBundleInterface $productBundle */
+ $productBundle = $this->fixtures['productBundle1'];
+
+ $this->client->request(
+ Request::METHOD_GET,
+ sprintf(self::ENDPOINT_PRODUCT_BUNDLES_ITEM, $productBundle->getId()),
+ [],
+ [],
+ $this->authHeaders
+ );
+ /** @var string $baseResponseContent */
+ $baseResponseContent = $this->client->getResponse()->getContent();
+ $baseProductBundle = json_decode($baseResponseContent, true, 512, \JSON_THROW_ON_ERROR);
+ $baseBundleItems = $baseProductBundle['items'] ?? [];
+
+ $johnnyWalkerBlue = $this->createProductBundleItemObject('JOHNNY_WALKER_BLUE');
+ $johnnyWalkerGold = $this->createProductBundleItemObject('JOHNNY_WALKER_GOLD');
+
+ $this->client->request(
+ Request::METHOD_PUT,
+ sprintf(self::ENDPOINT_PRODUCT_BUNDLES_ITEM, $productBundle->getId()),
+ [],
+ [],
+ $this->authHeaders,
+ json_encode([
+ 'items' => [
+ $johnnyWalkerBlue,
+ $johnnyWalkerGold,
+ ],
+ 'isPacked' => false,
+ ], \JSON_THROW_ON_ERROR)
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'admin/put_product_bundle_response', Response::HTTP_OK);
+
+ /** @var string $updateResponseContent */
+ $updateResponseContent = $response->getContent();
+ $updatedProductBundle = json_decode($updateResponseContent, true, 512, \JSON_THROW_ON_ERROR);
+ $updatedBundleItems = $updatedProductBundle['items'] ?? [];
+
+ foreach ($updatedBundleItems as $bundleItem) {
+ self::assertNotContains($bundleItem, $baseBundleItems);
+ }
+ }
+
+ private function createProductBundleItemObject(string $productVariantCode, int $quantity = 1): object
+ {
+ $productBundleItem = new \stdClass();
+ $productBundleItem->productVariant = '/api/v2/admin/product-variants/' . $productVariantCode;
+ $productBundleItem->quantity = $quantity;
+
+ return $productBundleItem;
+ }
+
+ /** @test */
+ public function it_deletes_product_bundle(): void
+ {
+ /** @var ProductBundleInterface $productBundle */
+ $productBundle = $this->fixtures['productBundle1'];
+
+ $this->client->request(
+ Request::METHOD_DELETE,
+ sprintf(self::ENDPOINT_PRODUCT_BUNDLES_ITEM, $productBundle->getId()),
+ [],
+ [],
+ $this->authHeaders
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponseCode($response, Response::HTTP_NO_CONTENT);
+
+ $this->client->request(
+ Request::METHOD_GET,
+ sprintf(self::ENDPOINT_PRODUCT_BUNDLES_ITEM, $productBundle->getId()),
+ [],
+ [],
+ $this->authHeaders
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponseCode($response, Response::HTTP_NOT_FOUND);
+ }
+}
diff --git a/tests/Api/AdminJsonApiTestCase.php b/tests/Api/AdminJsonApiTestCase.php
new file mode 100644
index 00000000..3a9632af
--- /dev/null
+++ b/tests/Api/AdminJsonApiTestCase.php
@@ -0,0 +1,42 @@
+client->request(
+ 'POST',
+ '/api/v2/admin/authentication-token',
+ [],
+ [],
+ ['CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'],
+ json_encode(['email' => $email, 'password' => $password])
+ );
+
+ return json_decode($this->client->getResponse()->getContent(), true)['token'];
+ }
+
+ public function getHeaders($authToken = null): array
+ {
+ $headers = ['CONTENT_TYPE' => 'application/ld+json', 'HTTP_ACCEPT' => 'application/ld+json'];
+
+ if (null === $authToken) {
+ return $headers;
+ }
+
+ $authorizationHeader = self::getContainer()->getParameter('sylius.api.authorization_header');
+ $headers['HTTP_' . $authorizationHeader] = 'Bearer ' . $authToken;
+
+ return $headers;
+ }
+}
diff --git a/tests/Api/DataFixtures/ORM/general/authentication.yml b/tests/Api/DataFixtures/ORM/general/authentication.yml
new file mode 100644
index 00000000..93c89f34
--- /dev/null
+++ b/tests/Api/DataFixtures/ORM/general/authentication.yml
@@ -0,0 +1,8 @@
+Sylius\Component\Core\Model\AdminUser:
+ admin:
+ plainPassword: sylius
+ roles: [ROLE_API_ACCESS]
+ enabled: true
+ email: api@example.com
+ username: sylius
+ localeCode: en_US
diff --git a/tests/Api/DataFixtures/ORM/general/channels.yml b/tests/Api/DataFixtures/ORM/general/channels.yml
new file mode 100644
index 00000000..ebe65c16
--- /dev/null
+++ b/tests/Api/DataFixtures/ORM/general/channels.yml
@@ -0,0 +1,20 @@
+Sylius\Component\Core\Model\Channel:
+ channel_web:
+ code: "WEB"
+ name: "Web Channel"
+ hostname: "localhost"
+ description: "Lorem ipsum"
+ baseCurrency: "@currency1"
+ defaultLocale: "@locale"
+ locales: ["@locale"]
+ color: "black"
+ enabled: true
+ taxCalculationStrategy: "order_items_based"
+
+Sylius\Component\Currency\Model\Currency:
+ currency1:
+ code: "EUR"
+
+Sylius\Component\Locale\Model\Locale:
+ locale:
+ code: "en_US"
diff --git a/tests/Api/DataFixtures/ORM/shop/orders.yml b/tests/Api/DataFixtures/ORM/shop/orders.yml
new file mode 100644
index 00000000..4ad75072
--- /dev/null
+++ b/tests/Api/DataFixtures/ORM/shop/orders.yml
@@ -0,0 +1,85 @@
+Sylius\Component\Core\Model\Address:
+ address_joe_doe:
+ firstName: Joe
+ lastName: Doe
+ street: 'Warszawska'
+ city: 'Warszawa'
+ postcode: '00-001'
+ countryCode: 'PL'
+
+Sylius\Component\Core\Model\Customer:
+ customer_joe_doe:
+ email: joe@localhost
+ emailCanonical: joe@localhost
+
+Sylius\Component\Addressing\Model\Zone:
+ zone_world:
+ code: 'WORLD'
+ name: 'World'
+ type: 'country'
+ scope: 'all'
+
+Sylius\Component\Shipping\Model\ShippingMethodTranslation:
+ shipping_method_translation_ups:
+ name: 'UPS'
+ description: 'UPS'
+ locale: 'en_US'
+ translatable: '@shipping_method_ups'
+
+Sylius\Component\Core\Model\ShippingMethod:
+ shipping_method_ups:
+ zone: '@zone_world'
+ code: 'ups'
+ channels:
+ - '@channel_web'
+ position: 0
+ categoryRequirement: 1
+ calculator: 'flat_rate'
+ configuration:
+ WEB:
+ amount: 0
+ translations:
+ - '@shipping_method_translation_ups'
+
+Sylius\Component\Core\Model\Shipment:
+ shipment_1:
+ state: 'ready'
+ method: '@shipping_method_ups'
+ order: '@order_with_bundle'
+
+Sylius\Component\Core\Model\OrderItemUnit:
+ order_item_unit_1:
+ __construct: ['@order_item_1']
+ shipment: '@shipment_1'
+
+BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItem:
+ product_bundle_order_item_{1..2}:
+ orderItem: '@order_item_1'
+ productBundleItem: '@productBundleItem'
+ productVariant: '@productVariant'
+ quantity: 1
+
+Tests\BitBag\SyliusProductBundlePlugin\Entity\OrderItem:
+ order_item_1:
+ order: '@order_with_bundle'
+ unitPrice: 1800
+ variant: '@productVariant3'
+ productName: 'Whiskey Double Pack'
+ variantname: 'Whiskey Double Pack'
+
+Sylius\Component\Core\Model\Order:
+ order_with_bundle:
+ shippingAddress: '@address_joe_doe'
+ billingAddress: '@address_joe_doe'
+ channel: '@channel_web'
+ customer: '@customer_joe_doe'
+ state: 'new'
+ items:
+ - '@order_item_1'
+ currencyCode: 'EUR'
+ localeCode: 'en_US'
+ checkoutState: 'completed'
+ payment_state: 'awaiting_payment'
+ shipping_state: 'ready'
+ tokenValue: 'zszRdAaZIx'
+ number: "000000001"
diff --git a/tests/Api/DataFixtures/ORM/shop/product_bundles.yml b/tests/Api/DataFixtures/ORM/shop/product_bundles.yml
new file mode 100644
index 00000000..2e84fcf6
--- /dev/null
+++ b/tests/Api/DataFixtures/ORM/shop/product_bundles.yml
@@ -0,0 +1,120 @@
+Sylius\Component\Core\Model\ProductTranslation:
+ productTranslation (template):
+ locale: "en_US"
+ description:
+ translatable: "@product"
+ productTranslation{1} (extends productTranslation):
+ name: "Johnny Walker Black"
+ slug: "johnny-walker-black"
+ productTranslation{2} (extends productTranslation):
+ name: "Jack Daniel's Gentleman Jack"
+ slug: "jack-daniels-gentleman-jack"
+ productTranslation{3} (extends productTranslation):
+ name: "Whiskey Double Pack"
+ slug: "whiskey-double-pack"
+ productTranslation{4} (extends productTranslation) :
+ name: "Johnny Walker Blue"
+ slug: "johnny-walker-blue"
+ productTranslation{5} (extends productTranslation) :
+ name: "Johnny Walker Bundle"
+ slug: "johnny-walker-bundle"
+ productTranslation{6} (extends productTranslation) :
+ name: "Johnny Walker Gold"
+ slug: "johnny-walker-gold"
+
+Tests\BitBag\SyliusProductBundlePlugin\Entity\Product:
+ product (template):
+ fallbackLocale: "en_US"
+ currentLocale: "en_US"
+ channels:
+ - "@channel_web"
+ translations:
+ - "@productTranslation"
+ product{1} (extends product):
+ code: "JOHNNY_WALKER_BLACK"
+ product{2} (extends product):
+ code: "JACK_DANIELS_GENTLEMAN"
+ product{3} (extends product):
+ code: "WHISKEY_DOUBLE_PACK"
+ product{4} (extends product):
+ code: "JOHNNY_WALKER_BLUE"
+ product{5} (extends product):
+ code: "JOHNNY_WALKER_BUNDLE"
+ product{6} (extends product):
+ code: "JOHNNY_WALKER_GOLD"
+
+Sylius\Component\Product\Model\ProductVariantTranslation:
+ productVariantTranslation (template):
+ locale: en_US
+ translatable: "@productVariant"
+ productVariantTranslation{1} (extends productVariantTranslation):
+ name: "Johnny Walker Black"
+ productVariantTranslation{2} (extends productVariantTranslation):
+ name: "Jack Daniel's Gentleman Jack"
+ productVariantTranslation{3} (extends productVariantTranslation):
+ name: "Whiskey Double Pack"
+ productVariantTranslation{4} (extends productVariantTranslation):
+ name: "Johnny Walker Blue"
+ productVariantTranslation{5} (extends productVariantTranslation):
+ name: "Johnny Walker Bundle"
+ productVariantTranslation{6} (extends productVariantTranslation):
+ name: "Johnny Walker Gold"
+
+Sylius\Component\Core\Model\ChannelPricing:
+ channelPricing (template):
+ productVariant: "@productVariant"
+ channelCode: "WEB"
+ channelPricing{1} (extends channelPricing):
+ price: 1000
+ originalPrice: 1000
+ channelPricing{2} (extends channelPricing):
+ price: 1000
+ originalPrice: 1000
+ channelPricing{3} (extends channelPricing):
+ price: 1800
+ originalPrice: 1800
+ channelPricing{4} (extends channelPricing):
+ price: 2000
+ originalPrice: 2000
+ channelPricing{5} (extends channelPricing):
+ price: 2500
+ originalPrice: 2500
+ channelPricing{6} (extends channelPricing):
+ price: 1000
+ originalPrice: 1000
+
+Sylius\Component\Core\Model\ProductVariant:
+ productVariant (template):
+ version: 1
+ product: "@product"
+ fallbackLocale: "en_US"
+ currentLocale: "en_US"
+ position: ""
+ translations:
+ - "@productVariantTranslation"
+ productVariant{1} (extends productVariant):
+ code: "JOHNNY_WALKER_BLACK"
+ productVariant{2} (extends productVariant):
+ code: "JACK_DANIELS_GENTLEMAN"
+ productVariant{3} (extends productVariant):
+ code: "WHISKEY_DOUBLE_PACK"
+ productVariant{4} (extends productVariant):
+ code: "JOHNNY_WALKER_BLUE"
+ productVariant{5} (extends productVariant):
+ code: "JOHNNY_WALKER_BUNDLE"
+ productVariant{6} (extends productVariant):
+ code: "JOHNNY_WALKER_GOLD"
+
+BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItem:
+ productBundleItem{1..2}:
+ productVariant: "@productVariant"
+ quantity: 1
+ productBundle: "@productBundle1"
+
+BitBag\SyliusProductBundlePlugin\Entity\ProductBundle:
+ productBundle1:
+ product: "@product3"
+ productBundleItems:
+ - "@productBundleItem1"
+ - "@productBundleItem2"
+ isPackedProduct: true
diff --git a/tests/Api/JsonApiTestCase.php b/tests/Api/JsonApiTestCase.php
new file mode 100644
index 00000000..542e309e
--- /dev/null
+++ b/tests/Api/JsonApiTestCase.php
@@ -0,0 +1,31 @@
+ 'application/ld+json', 'HTTP_ACCEPT' => 'application/ld+json'];
+
+ public const PATCH_HEADER = ['CONTENT_TYPE' => 'application/merge-patch+json', 'HTTP_ACCEPT' => 'application/ld+json'];
+
+ protected static function getContainer(): ContainerInterface
+ {
+ if (is_callable('parent::getContainer')) {
+ /* @phpstan-ignore-next-line */
+ return parent::getContainer();
+ }
+
+ return self::$container;
+ }
+}
diff --git a/tests/Api/Responses/admin/get_order_with_bundle_response.json b/tests/Api/Responses/admin/get_order_with_bundle_response.json
new file mode 100644
index 00000000..d30173f2
--- /dev/null
+++ b/tests/Api/Responses/admin/get_order_with_bundle_response.json
@@ -0,0 +1,52 @@
+{
+ "@context": "\/api\/v2\/contexts\/Order",
+ "@id": "\/api\/v2\/admin\/orders\/zszRdAaZIx",
+ "@type": "Order",
+ "customer": "\/api\/v2\/admin\/customers\/@integer@",
+ "channel": "\/api\/v2\/admin\/channels\/WEB",
+ "shippingAddress": "@*@",
+ "billingAddress": "@*@",
+ "payments": "@*@",
+ "shipments": "@*@",
+ "currencyCode": "EUR",
+ "localeCode": "en_US",
+ "checkoutState": "completed",
+ "paymentState": "awaiting_payment",
+ "shippingState": "ready",
+ "tokenValue": "zszRdAaZIx",
+ "id": "@integer@",
+ "number": "@string@",
+ "items": [
+ {
+ "@id": "\/api\/v2\/admin\/order-items\/@integer@",
+ "@type": "OrderItem",
+ "variant": "\/api\/v2\/admin\/product-variants\/WHISKEY_DOUBLE_PACK",
+ "productName": "Whiskey Double Pack",
+ "id": "@integer@",
+ "quantity": 1,
+ "total": 1800,
+ "productBundleOrderItems": [
+ {
+ "@type": "ProductBundleOrderItem",
+ "@id": "@string@",
+ "id": "@integer@",
+ "productVariant": "\/api\/v2\/admin\/product-variants\/@string@",
+ "quantity": 1
+ },
+ {
+ "@type": "ProductBundleOrderItem",
+ "@id": "@string@",
+ "id": "@integer@",
+ "productVariant": "\/api\/v2\/admin\/product-variants\/@string@",
+ "quantity": 1
+ }
+ ],
+ "subtotal": 1800
+ }
+ ],
+ "total": 1800,
+ "state": "new",
+ "taxTotal": 0,
+ "shippingTotal": 0,
+ "orderPromotionTotal": 0
+}
diff --git a/tests/Api/Responses/admin/get_product_bundle_collection_response.json b/tests/Api/Responses/admin/get_product_bundle_collection_response.json
new file mode 100644
index 00000000..67e2a136
--- /dev/null
+++ b/tests/Api/Responses/admin/get_product_bundle_collection_response.json
@@ -0,0 +1,21 @@
+{
+ "@context": "\/api\/v2\/contexts\/ProductBundle",
+ "@id": "\/api\/v2\/admin\/product-bundles",
+ "@type": "hydra:Collection",
+ "hydra:member": [
+ {
+ "@id": "\/api\/v2\/admin\/product-bundles\/@integer@",
+ "@type": "ProductBundle",
+ "id": "@integer@",
+ "product": "\/api\/v2\/admin\/products\/WHISKEY_DOUBLE_PACK",
+ "items": [
+ "\/api\/v2\/shop\/product-bundle-items\/@integer@",
+ "\/api\/v2\/shop\/product-bundle-items\/@integer@"
+ ],
+ "createdAt": "@datetime@",
+ "updatedAt": "@datetime@",
+ "isPacked": true
+ }
+ ],
+ "hydra:totalItems": 1
+}
diff --git a/tests/Api/Responses/admin/get_product_bundle_response.json b/tests/Api/Responses/admin/get_product_bundle_response.json
new file mode 100644
index 00000000..3592341d
--- /dev/null
+++ b/tests/Api/Responses/admin/get_product_bundle_response.json
@@ -0,0 +1,14 @@
+{
+ "@context": "\/api\/v2\/contexts\/ProductBundle",
+ "@id": "\/api\/v2\/admin\/product-bundles\/@integer@",
+ "@type": "ProductBundle",
+ "id": "@integer@",
+ "product": "\/api\/v2\/admin\/products\/WHISKEY_DOUBLE_PACK",
+ "items": [
+ "\/api\/v2\/shop\/product-bundle-items\/@integer@",
+ "\/api\/v2\/shop\/product-bundle-items\/@integer@"
+ ],
+ "createdAt": "@datetime@",
+ "updatedAt": "@datetime@",
+ "isPacked": true
+}
diff --git a/tests/Api/Responses/admin/post_product_bundle_response.json b/tests/Api/Responses/admin/post_product_bundle_response.json
new file mode 100644
index 00000000..7fec9ba3
--- /dev/null
+++ b/tests/Api/Responses/admin/post_product_bundle_response.json
@@ -0,0 +1,14 @@
+{
+ "@context": "\/api\/v2\/contexts\/ProductBundle",
+ "@id": "\/api\/v2\/admin\/product-bundles\/@integer@",
+ "@type": "ProductBundle",
+ "id": "@integer@",
+ "product": "\/api\/v2\/admin\/products\/JOHNNY_WALKER_BUNDLE",
+ "items": [
+ "\/api\/v2\/shop\/product-bundle-items\/@integer@",
+ "\/api\/v2\/shop\/product-bundle-items\/@integer@"
+ ],
+ "createdAt": "@datetime@",
+ "updatedAt": "@datetime@",
+ "isPacked": true
+}
diff --git a/tests/Api/Responses/admin/put_product_bundle_response.json b/tests/Api/Responses/admin/put_product_bundle_response.json
new file mode 100644
index 00000000..33e86896
--- /dev/null
+++ b/tests/Api/Responses/admin/put_product_bundle_response.json
@@ -0,0 +1,14 @@
+{
+ "@context": "\/api\/v2\/contexts\/ProductBundle",
+ "@id": "\/api\/v2\/admin\/product-bundles\/@integer@",
+ "@type": "ProductBundle",
+ "id": "@integer@",
+ "product": "\/api\/v2\/admin\/products\/WHISKEY_DOUBLE_PACK",
+ "items": [
+ "\/api\/v2\/shop\/product-bundle-items\/@integer@",
+ "\/api\/v2\/shop\/product-bundle-items\/@integer@"
+ ],
+ "createdAt": "@datetime@",
+ "updatedAt": "@datetime@",
+ "isPacked": false
+}
diff --git a/tests/Api/Responses/shop/get_bundled_product_response.json b/tests/Api/Responses/shop/get_bundled_product_response.json
new file mode 100644
index 00000000..ebc68fb3
--- /dev/null
+++ b/tests/Api/Responses/shop/get_bundled_product_response.json
@@ -0,0 +1,67 @@
+{
+ "@context": "\/api\/v2\/contexts\/Product",
+ "@id": "\/api\/v2\/shop\/products\/WHISKEY_DOUBLE_PACK",
+ "@type": "Product",
+ "productTaxons": "@*@",
+ "mainTaxon": "@string@||@null@",
+ "reviews": "@*@",
+ "averageRating": "@integer@",
+ "images": "@*@",
+ "id": "@integer@",
+ "code": "WHISKEY_DOUBLE_PACK",
+ "variants": "@*@",
+ "options": "@*@",
+ "createdAt": "@string@",
+ "updatedAt": "@string@",
+ "translations": "@json@",
+ "bundle": {
+ "@id": "\/api\/v2\/shop\/product-bundles\/@integer@",
+ "@type": "ProductBundle",
+ "items": [
+ {
+ "@type": "ProductBundleItem",
+ "@id": "@string@",
+ "productVariant": {
+ "@id": "\/api\/v2\/shop\/product-variants\/JOHNNY_WALKER_BLACK",
+ "@type": "ProductVariant",
+ "code": "JOHNNY_WALKER_BLACK",
+ "optionValues": "@*@",
+ "translations": {
+ "en_US": {
+ "@id": "/api/v2/shop/product-variant-translation/@integer@",
+ "@type": "ProductVariantTranslation",
+ "name": "Johnny Walker Black"
+ }
+ },
+ "price": 1000,
+ "inStock": true
+ },
+ "quantity": 1
+ },
+ {
+ "@type": "ProductBundleItem",
+ "@id": "@string@",
+ "productVariant": {
+ "@id": "\/api\/v2\/shop\/product-variants\/JACK_DANIELS_GENTLEMAN",
+ "@type": "ProductVariant",
+ "code": "JACK_DANIELS_GENTLEMAN",
+ "optionValues": "@*@",
+ "translations": {
+ "en_US": {
+ "@id": "/api/v2/shop/product-variant-translation/@integer@",
+ "@type": "ProductVariantTranslation",
+ "name": "Jack Daniel's Gentleman Jack"
+ }
+ },
+ "price": 1000,
+ "inStock": true
+ },
+ "quantity": 1
+ }
+ ],
+ "isPacked": true
+ },
+ "isBundle": true,
+ "description": "@string@",
+ "defaultVariant": "@string@"
+}
diff --git a/tests/Api/Responses/shop/get_not_bundled_product_response.json b/tests/Api/Responses/shop/get_not_bundled_product_response.json
new file mode 100644
index 00000000..2e4e59b2
--- /dev/null
+++ b/tests/Api/Responses/shop/get_not_bundled_product_response.json
@@ -0,0 +1,21 @@
+{
+ "@context": "\/api\/v2\/contexts\/Product",
+ "@id": "\/api\/v2\/shop\/products\/JOHNNY_WALKER_BLACK",
+ "@type": "Product",
+ "productTaxons": "@*@",
+ "mainTaxon": "@string@||@null@",
+ "reviews": "@*@",
+ "averageRating": "@integer@",
+ "images": "@*@",
+ "id": "@integer@",
+ "code": "JOHNNY_WALKER_BLACK",
+ "variants": "@*@",
+ "options": "@*@",
+ "createdAt": "@string@",
+ "updatedAt": "@string@",
+ "translations": "@json@",
+ "bundle": "@null@",
+ "isBundle": false,
+ "description": "@string@",
+ "defaultVariant": "@string@"
+}
diff --git a/tests/Api/Responses/shop/get_order_with_bundle_response.json b/tests/Api/Responses/shop/get_order_with_bundle_response.json
new file mode 100644
index 00000000..f3d8e637
--- /dev/null
+++ b/tests/Api/Responses/shop/get_order_with_bundle_response.json
@@ -0,0 +1,67 @@
+{
+ "@context": "\/api\/v2\/contexts\/Order",
+ "@id": "\/api\/v2\/shop\/orders\/zszRdAaZIx",
+ "@type": "Order",
+ "shippingAddress": "@*@",
+ "billingAddress": "@*@",
+ "payments": "@*@",
+ "shipments": "@*@",
+ "currencyCode": "EUR",
+ "localeCode": "en_US",
+ "checkoutState": "completed",
+ "paymentState": "awaiting_payment",
+ "shippingState": "ready",
+ "tokenValue": "zszRdAaZIx",
+ "id": "@integer@",
+ "number": "@string@",
+ "items": [
+ {
+ "@id": "\/api\/v2\/shop\/order-items\/@integer@",
+ "@type": "OrderItem",
+ "variant": {
+ "@id": "\/api\/v2\/shop\/product-variants\/WHISKEY_DOUBLE_PACK",
+ "@type": "ProductVariant",
+ "code": "WHISKEY_DOUBLE_PACK",
+ "price": 1800,
+ "inStock": true
+ },
+ "productName": "Whiskey Double Pack",
+ "id": "@integer@",
+ "quantity": 1,
+ "unitPrice": 1800,
+ "total": 1800,
+ "productBundleOrderItems": [
+ {
+ "@type": "ProductBundleOrderItem",
+ "@id": "@string@",
+ "productVariant": {
+ "@id": "\/api\/v2\/shop\/product-variants\/@string@",
+ "@type": "ProductVariant",
+ "code": "@string@",
+ "price": 1000,
+ "inStock": true
+ },
+ "quantity": 1
+ },
+ {
+ "@type": "ProductBundleOrderItem",
+ "@id": "@string@",
+ "productVariant": {
+ "@id": "\/api\/v2\/shop\/product-variants\/@string@",
+ "@type": "ProductVariant",
+ "code": "@string@",
+ "price": 1000,
+ "inStock": true
+ },
+ "quantity": 1
+ }
+ ],
+ "subtotal": 1800
+ }
+ ],
+ "itemsTotal": 1800,
+ "total": 1800,
+ "taxTotal": 0,
+ "shippingTotal": 0,
+ "orderPromotionTotal": 0
+}
diff --git a/tests/Api/Responses/shop/get_product_bundle_response.json b/tests/Api/Responses/shop/get_product_bundle_response.json
new file mode 100644
index 00000000..c349ec5d
--- /dev/null
+++ b/tests/Api/Responses/shop/get_product_bundle_response.json
@@ -0,0 +1,51 @@
+{
+ "@context": "\/api\/v2\/contexts\/ProductBundle",
+ "@id": "\/api\/v2\/shop\/product-bundles\/@integer@",
+ "@type": "ProductBundle",
+ "product": "\/api\/v2\/shop\/products\/WHISKEY_DOUBLE_PACK",
+ "items": [
+ {
+ "@type": "ProductBundleItem",
+ "@id": "@string@",
+ "productVariant": {
+ "@id": "/api/v2/shop/product-variants/JOHNNY_WALKER_BLACK",
+ "@type": "ProductVariant",
+ "code": "JOHNNY_WALKER_BLACK",
+ "product": "\/api\/v2\/shop\/products\/JOHNNY_WALKER_BLACK",
+ "optionValues": "@*@",
+ "translations": {
+ "en_US": {
+ "@id": "/api/v2/shop/product-variant-translation/@integer@",
+ "@type": "ProductVariantTranslation",
+ "name": "Johnny Walker Black"
+ }
+ },
+ "price": 1000,
+ "inStock": true
+ },
+ "quantity": 1
+ },
+ {
+ "@type": "ProductBundleItem",
+ "@id": "@string@",
+ "productVariant": {
+ "@id": "/api/v2/shop/product-variants/JACK_DANIELS_GENTLEMAN",
+ "@type": "ProductVariant",
+ "code": "JACK_DANIELS_GENTLEMAN",
+ "product": "\/api\/v2\/shop\/products\/JACK_DANIELS_GENTLEMAN",
+ "optionValues": "@*@",
+ "translations": {
+ "en_US": {
+ "@id": "/api/v2/shop/product-variant-translation/@integer@",
+ "@type": "ProductVariantTranslation",
+ "name": "Jack Daniel's Gentleman Jack"
+ }
+ },
+ "price": 1000,
+ "inStock": true
+ },
+ "quantity": 1
+ }
+ ],
+ "isPacked": true
+}
diff --git a/tests/Api/Shop/OrderTest.php b/tests/Api/Shop/OrderTest.php
new file mode 100644
index 00000000..38a49dc7
--- /dev/null
+++ b/tests/Api/Shop/OrderTest.php
@@ -0,0 +1,91 @@
+fixtures = $this->loadFixturesFromFiles([
+ 'general/channels.yml',
+ 'shop/product_bundles.yml',
+ 'shop/orders.yml',
+ ]);
+ }
+
+ /** @test */
+ public function it_gets_order_data_containing_info_about_bundled_items(): void
+ {
+ /** @var OrderInterface $order */
+ $order = $this->fixtures['order_with_bundle'];
+
+ $this->client->request(
+ Request::METHOD_GET,
+ sprintf(self::ENDPOINT_ORDERS_ITEM, $order->getTokenValue()),
+ [],
+ [],
+ self::DEFAULT_HEADER
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'shop/get_order_with_bundle_response', Response::HTTP_OK);
+ }
+
+ /** @test */
+ public function it_adds_product_bundle_to_a_cart(): void
+ {
+ $this->createCart(self::CART_TOKEN);
+ /** @var ProductBundleInterface $productBundle */
+ $productBundle = $this->fixtures['productBundle1'];
+
+ $this->client->request(
+ Request::METHOD_PATCH,
+ sprintf(self::ENDPOINT_ORDERS_PRODUCT_BUNDLE, self::CART_TOKEN),
+ [],
+ [],
+ self::PATCH_HEADER,
+ json_encode([
+ 'productCode' => $productBundle->getProduct()->getCode(),
+ ], \JSON_THROW_ON_ERROR)
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponseCode($response, Response::HTTP_OK);
+
+ $cart = $this->findCart(self::CART_TOKEN);
+ /** @var OrderItemInterface $cartItem */
+ $cartItem = $cart->getItems()->first();
+ $cartProduct = $cartItem->getProduct();
+
+ self::assertCount(1, $cart->getItems());
+ self::assertCount(2, $cartItem->getProductBundleOrderItems());
+ self::assertSame($productBundle->getProduct()->getId(), $cartProduct->getId());
+ }
+}
diff --git a/tests/Api/Shop/ProductTest.php b/tests/Api/Shop/ProductTest.php
new file mode 100644
index 00000000..6f4f0405
--- /dev/null
+++ b/tests/Api/Shop/ProductTest.php
@@ -0,0 +1,72 @@
+loadFixturesFromFiles(['general/channels.yml', 'shop/product_bundles.yml']);
+ }
+
+ /** @test */
+ public function it_gets_bundled_product(): void
+ {
+ $this->client->request(
+ Request::METHOD_GET,
+ sprintf(self::ENDPOINT_PRODUCTS_ITEM, 'WHISKEY_DOUBLE_PACK'),
+ [],
+ [],
+ self::DEFAULT_HEADER
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'shop/get_bundled_product_response', Response::HTTP_OK);
+ }
+
+ /** @test */
+ public function it_gets_not_bundled_product(): void
+ {
+ $this->client->request(
+ Request::METHOD_GET,
+ sprintf(self::ENDPOINT_PRODUCTS_ITEM, 'JOHNNY_WALKER_BLACK'),
+ [],
+ [],
+ self::DEFAULT_HEADER
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'shop/get_not_bundled_product_response', Response::HTTP_OK);
+ }
+
+ /** @test */
+ public function it_gets_product_bundle_as_a_subresource(): void
+ {
+ $this->client->request(
+ Request::METHOD_GET,
+ sprintf(self::ENDPOINT_PRODUCTS_ITEM_PRODUCT_BUNDLE, 'WHISKEY_DOUBLE_PACK'),
+ [],
+ [],
+ self::DEFAULT_HEADER
+ );
+ $response = $this->client->getResponse();
+
+ $this->assertResponse($response, 'shop/get_product_bundle_response', Response::HTTP_OK);
+ }
+}
diff --git a/tests/Api/Utils/CartHelperTrait.php b/tests/Api/Utils/CartHelperTrait.php
new file mode 100644
index 00000000..51f826f7
--- /dev/null
+++ b/tests/Api/Utils/CartHelperTrait.php
@@ -0,0 +1,38 @@
+get('sylius.command_bus');
+
+ $command = new PickupCart($tokenValue, 'en_US');
+ $command->setChannelCode('WEB');
+
+ $commandBus->dispatch($command);
+ }
+
+ public function findCart(string $tokenValue): ?OrderInterface
+ {
+ /** @var OrderRepositoryInterface $orderManager */
+ $orderManager = self::getContainer()->get('sylius.repository.order');
+
+ return $orderManager->findCartByTokenValue($tokenValue);
+ }
+}
diff --git a/tests/Application/config/bundles.php b/tests/Application/config/bundles.php
index 845f4bd0..59b57b19 100644
--- a/tests/Application/config/bundles.php
+++ b/tests/Application/config/bundles.php
@@ -47,7 +47,6 @@
Sylius\Bundle\ThemeBundle\SyliusThemeBundle::class => ['all' => true],
Sylius\Bundle\AdminBundle\SyliusAdminBundle::class => ['all' => true],
Sylius\Bundle\ShopBundle\SyliusShopBundle::class => ['all' => true],
- BitBag\SyliusProductBundlePlugin\BitBagSyliusProductBundlePlugin::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true],
FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true, 'test_cached' => true],
@@ -55,4 +54,7 @@
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
Sylius\Bundle\ApiBundle\SyliusApiBundle::class => ['all' => true],
SyliusLabs\DoctrineMigrationsExtraBundle\SyliusLabsDoctrineMigrationsExtraBundle::class => ['all' => true],
+ BitBag\SyliusProductBundlePlugin\BitBagSyliusProductBundlePlugin::class => ['all' => true],
+ Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['test' => true],
+ Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['test' => true],
];
diff --git a/tests/Application/config/sylius/1.10/packages/_sylius.yaml b/tests/Application/config/sylius/1.10/packages/_sylius.yaml
new file mode 100644
index 00000000..cd01aaf7
--- /dev/null
+++ b/tests/Application/config/sylius/1.10/packages/_sylius.yaml
@@ -0,0 +1,2 @@
+sylius_api:
+ enabled: true
diff --git a/tests/Application/config/sylius/1.10/packages/security.yaml b/tests/Application/config/sylius/1.10/packages/security.yaml
index 10628102..51a6ec9a 100644
--- a/tests/Application/config/sylius/1.10/packages/security.yaml
+++ b/tests/Application/config/sylius/1.10/packages/security.yaml
@@ -1,13 +1,14 @@
parameters:
sylius.security.admin_regex: "^/%sylius_admin.path_name%"
- sylius.security.api_regex: "^/api"
- sylius.security.shop_regex: "^/(?!%sylius_admin.path_name%|new-api|api/.*|api$|media/.*)[^/]++"
- sylius.security.new_api_route: "/new-api"
+ sylius.security.shop_regex: "^/(?!%sylius_admin.path_name%|api/.*|api$|media/.*)[^/]++"
+ sylius.security.new_api_route: "/api/v2"
sylius.security.new_api_regex: "^%sylius.security.new_api_route%"
sylius.security.new_api_admin_route: "%sylius.security.new_api_route%/admin"
sylius.security.new_api_admin_regex: "^%sylius.security.new_api_admin_route%"
sylius.security.new_api_shop_route: "%sylius.security.new_api_route%/shop"
sylius.security.new_api_shop_regex: "^%sylius.security.new_api_shop_route%"
+ sylius.security.new_api_user_account_route: "%sylius.security.new_api_shop_route%/account"
+ sylius.security.new_api_user_account_regex: "^%sylius.security.new_api_user_account_route%"
security:
always_authenticate_before_granting: true
@@ -20,9 +21,6 @@ security:
id: sylius.shop_user_provider.email_or_name_based
sylius_api_shop_user_provider:
id: sylius.shop_user_provider.email_or_name_based
- sylius_api_chain_provider:
- chain:
- providers: [sylius_api_shop_user_provider, sylius_api_admin_user_provider]
encoders:
Sylius\Component\User\Model\UserInterface: argon2i
@@ -55,12 +53,12 @@ security:
anonymous: true
new_api_admin_user:
- pattern: "%sylius.security.new_api_route%/admin-user-authentication-token"
- provider: sylius_admin_user_provider
+ pattern: "%sylius.security.new_api_admin_regex%/.*"
+ provider: sylius_api_admin_user_provider
stateless: true
anonymous: true
json_login:
- check_path: "%sylius.security.new_api_route%/admin-user-authentication-token"
+ check_path: "%sylius.security.new_api_admin_route%/authentication-token"
username_path: email
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
@@ -70,12 +68,12 @@ security:
- lexik_jwt_authentication.jwt_token_authenticator
new_api_shop_user:
- pattern: "%sylius.security.new_api_route%/shop-user-authentication-token"
- provider: sylius_shop_user_provider
+ pattern: "%sylius.security.new_api_shop_regex%/.*"
+ provider: sylius_api_shop_user_provider
stateless: true
anonymous: true
json_login:
- check_path: "%sylius.security.new_api_route%/shop-user-authentication-token"
+ check_path: "%sylius.security.new_api_shop_route%/authentication-token"
username_path: email
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
@@ -84,15 +82,6 @@ security:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
- new_api:
- pattern: "%sylius.security.new_api_regex%/*"
- provider: sylius_api_chain_provider
- stateless: true
- anonymous: lazy
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
-
shop:
switch_user: { role: ROLE_ALLOWED_TO_SWITCH }
context: shop
@@ -124,7 +113,11 @@ security:
anonymous: true
dev:
- pattern: ^/(_(profiler|wdt)|css|images|js)/
+ pattern: ^/(_(profiler|wdt)|css|images|js)/
+ security: false
+
+ image_resolver:
+ pattern: ^/media/cache/resolve
security: false
access_control:
@@ -134,15 +127,16 @@ security:
- { path: "%sylius.security.shop_regex%/_partial", role: ROLE_NO_ACCESS }
- { path: "%sylius.security.admin_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%sylius.security.api_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.shop_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.shop_regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.shop_regex%/verify", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.admin_regex%", role: ROLE_ADMINISTRATION_ACCESS }
- - { path: "%sylius.security.api_regex%/.*", role: ROLE_API_ACCESS }
- { path: "%sylius.security.shop_regex%/account", role: ROLE_USER }
- { path: "%sylius.security.new_api_admin_regex%/.*", role: ROLE_API_ACCESS }
+ - { path: "%sylius.security.new_api_admin_route%/authentication-token", role: IS_AUTHENTICATED_ANONYMOUSLY }
+ - { path: "%sylius.security.new_api_user_account_regex%/.*", role: ROLE_USER }
+ - { path: "%sylius.security.new_api_shop_route%/authentication-token", role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "%sylius.security.new_api_shop_regex%/.*", role: IS_AUTHENTICATED_ANONYMOUSLY }
diff --git a/tests/Application/config/sylius/1.8/bundles.php b/tests/Application/config/sylius/1.8/bundles.php
deleted file mode 100644
index 5cc9825d..00000000
--- a/tests/Application/config/sylius/1.8/bundles.php
+++ /dev/null
@@ -1,20 +0,0 @@
- true];
-}
-if (class_exists('WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle')) {
- $bundles[WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle::class] = ['all' => true];
-}
-if (class_exists('FOS\OAuthServerBundle\FOSOAuthServerBundle')) {
- $bundles[FOS\OAuthServerBundle\FOSOAuthServerBundle::class] = ['all' => true];
-}
-if (class_exists('Sylius\Bundle\AdminApiBundle\SyliusAdminApiBundle')) {
- $bundles[Sylius\Bundle\AdminApiBundle\SyliusAdminApiBundle::class] = ['all' => true];
-}
-
-return $bundles;
diff --git a/tests/Application/config/sylius/1.8/packages/_sylius.yaml b/tests/Application/config/sylius/1.8/packages/_sylius.yaml
deleted file mode 100644
index 1674a972..00000000
--- a/tests/Application/config/sylius/1.8/packages/_sylius.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-imports:
- - { resource: "@SyliusAdminApiBundle/Resources/config/app/config.yml" }
diff --git a/tests/Application/config/sylius/1.8/packages/security.yaml b/tests/Application/config/sylius/1.8/packages/security.yaml
deleted file mode 100644
index 8161bdab..00000000
--- a/tests/Application/config/sylius/1.8/packages/security.yaml
+++ /dev/null
@@ -1,159 +0,0 @@
-parameters:
- sylius.security.admin_regex: "^/%sylius_admin.path_name%"
- sylius.security.api_regex: "^/api"
- sylius.security.shop_regex: "^/(?!%sylius_admin.path_name%|new-api|api/.*|api$|media/.*)[^/]++"
- sylius.security.new_api_route: "/new-api"
- sylius.security.new_api_regex: "^%sylius.security.new_api_route%"
- sylius.security.new_api_admin_route: "%sylius.security.new_api_route%/admin"
- sylius.security.new_api_admin_regex: "^%sylius.security.new_api_admin_route%"
- sylius.security.new_api_shop_route: "%sylius.security.new_api_route%/shop"
- sylius.security.new_api_shop_regex: "^%sylius.security.new_api_shop_route%"
-
-security:
- always_authenticate_before_granting: true
- providers:
- sylius_admin_user_provider:
- id: sylius.admin_user_provider.email_or_name_based
- sylius_api_admin_user_provider:
- id: sylius.admin_user_provider.email_or_name_based
- sylius_shop_user_provider:
- id: sylius.shop_user_provider.email_or_name_based
- sylius_api_shop_user_provider:
- id: sylius.shop_user_provider.email_or_name_based
- sylius_api_chain_provider:
- chain:
- providers: [sylius_api_shop_user_provider, sylius_api_admin_user_provider]
-
- encoders:
- Sylius\Component\User\Model\UserInterface: argon2i
- firewalls:
- admin:
- switch_user: true
- context: admin
- pattern: "%sylius.security.admin_regex%"
- provider: sylius_admin_user_provider
- form_login:
- provider: sylius_admin_user_provider
- login_path: sylius_admin_login
- check_path: sylius_admin_login_check
- failure_path: sylius_admin_login
- default_target_path: sylius_admin_dashboard
- use_forward: false
- use_referer: true
- csrf_token_generator: security.csrf.token_manager
- csrf_parameter: _csrf_admin_security_token
- csrf_token_id: admin_authenticate
- remember_me:
- secret: "%env(APP_SECRET)%"
- path: "/%sylius_admin.path_name%"
- name: APP_ADMIN_REMEMBER_ME
- lifetime: 31536000
- remember_me_parameter: _remember_me
- logout:
- path: sylius_admin_logout
- target: sylius_admin_login
- anonymous: true
-
- oauth_token:
- pattern: "%sylius.security.api_regex%/oauth/v2/token"
- security: false
-
- new_api_admin_user:
- pattern: "%sylius.security.new_api_route%/admin-user-authentication-token"
- provider: sylius_admin_user_provider
- stateless: true
- anonymous: true
- json_login:
- check_path: "%sylius.security.new_api_route%/admin-user-authentication-token"
- username_path: email
- password_path: password
- success_handler: lexik_jwt_authentication.handler.authentication_success
- failure_handler: lexik_jwt_authentication.handler.authentication_failure
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
-
- new_api_shop_user:
- pattern: "%sylius.security.new_api_route%/shop-user-authentication-token"
- provider: sylius_shop_user_provider
- stateless: true
- anonymous: true
- json_login:
- check_path: "%sylius.security.new_api_route%/shop-user-authentication-token"
- username_path: email
- password_path: password
- success_handler: lexik_jwt_authentication.handler.authentication_success
- failure_handler: lexik_jwt_authentication.handler.authentication_failure
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
-
- new_api:
- pattern: "%sylius.security.new_api_regex%/*"
- provider: sylius_api_chain_provider
- stateless: true
- anonymous: lazy
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
-
- api:
- pattern: "%sylius.security.api_regex%/.*"
- provider: sylius_admin_user_provider
- fos_oauth: true
- stateless: true
- anonymous: true
-
- shop:
- switch_user: { role: ROLE_ALLOWED_TO_SWITCH }
- context: shop
- pattern: "%sylius.security.shop_regex%"
- provider: sylius_shop_user_provider
- form_login:
- success_handler: sylius.authentication.success_handler
- failure_handler: sylius.authentication.failure_handler
- provider: sylius_shop_user_provider
- login_path: sylius_shop_login
- check_path: sylius_shop_login_check
- failure_path: sylius_shop_login
- default_target_path: sylius_shop_homepage
- use_forward: false
- use_referer: true
- csrf_token_generator: security.csrf.token_manager
- csrf_parameter: _csrf_shop_security_token
- csrf_token_id: shop_authenticate
- remember_me:
- secret: "%env(APP_SECRET)%"
- name: APP_SHOP_REMEMBER_ME
- lifetime: 31536000
- remember_me_parameter: _remember_me
- logout:
- path: sylius_shop_logout
- target: sylius_shop_login
- invalidate_session: false
- success_handler: sylius.handler.shop_user_logout
- anonymous: true
-
- dev:
- pattern: ^/(_(profiler|wdt)|css|images|js)/
- security: false
-
- access_control:
- - { path: "%sylius.security.admin_regex%/_partial", role: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] }
- - { path: "%sylius.security.admin_regex%/_partial", role: ROLE_NO_ACCESS }
- - { path: "%sylius.security.shop_regex%/_partial", role: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] }
- - { path: "%sylius.security.shop_regex%/_partial", role: ROLE_NO_ACCESS }
-
- - { path: "%sylius.security.admin_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%sylius.security.api_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%sylius.security.shop_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
-
- - { path: "%sylius.security.shop_regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%sylius.security.shop_regex%/verify", role: IS_AUTHENTICATED_ANONYMOUSLY }
-
- - { path: "%sylius.security.admin_regex%", role: ROLE_ADMINISTRATION_ACCESS }
- - { path: "%sylius.security.api_regex%/.*", role: ROLE_API_ACCESS }
- - { path: "%sylius.security.shop_regex%/account", role: ROLE_USER }
-
- - { path: "%sylius.security.new_api_admin_regex%/.*", role: ROLE_API_ACCESS }
- - { path: "%sylius.security.new_api_shop_regex%/.*", role: IS_AUTHENTICATED_ANONYMOUSLY }
diff --git a/tests/Application/config/sylius/1.8/routes/sylius_admin_api.yaml b/tests/Application/config/sylius/1.8/routes/sylius_admin_api.yaml
deleted file mode 100644
index 80aed457..00000000
--- a/tests/Application/config/sylius/1.8/routes/sylius_admin_api.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-sylius_admin_api:
- resource: "@SyliusAdminApiBundle/Resources/config/routing.yml"
- prefix: /api
diff --git a/tests/Application/config/sylius/1.9/bundles.php b/tests/Application/config/sylius/1.9/bundles.php
deleted file mode 100644
index 7e956f7d..00000000
--- a/tests/Application/config/sylius/1.9/bundles.php
+++ /dev/null
@@ -1,20 +0,0 @@
- true];
-}
-if (class_exists('SyliusLabs\Polyfill\Symfony\Security\Bundle\SyliusLabsPolyfillSymfonySecurityBundle')) {
- $bundles[SyliusLabs\Polyfill\Symfony\Security\Bundle\SyliusLabsPolyfillSymfonySecurityBundle::class] = ['all' => true];
-}
-if (class_exists('FOS\OAuthServerBundle\FOSOAuthServerBundle')) {
- $bundles[FOS\OAuthServerBundle\FOSOAuthServerBundle::class] = ['all' => true];
-}
-if (class_exists('Sylius\Bundle\AdminApiBundle\SyliusAdminApiBundle')) {
- $bundles[Sylius\Bundle\AdminApiBundle\SyliusAdminApiBundle::class] = ['all' => true];
-}
-
-return $bundles;
diff --git a/tests/Application/config/sylius/1.9/packages/_sylius.yaml b/tests/Application/config/sylius/1.9/packages/_sylius.yaml
deleted file mode 100644
index 1674a972..00000000
--- a/tests/Application/config/sylius/1.9/packages/_sylius.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-imports:
- - { resource: "@SyliusAdminApiBundle/Resources/config/app/config.yml" }
diff --git a/tests/Application/config/sylius/1.9/packages/security.yaml b/tests/Application/config/sylius/1.9/packages/security.yaml
deleted file mode 100644
index 8161bdab..00000000
--- a/tests/Application/config/sylius/1.9/packages/security.yaml
+++ /dev/null
@@ -1,159 +0,0 @@
-parameters:
- sylius.security.admin_regex: "^/%sylius_admin.path_name%"
- sylius.security.api_regex: "^/api"
- sylius.security.shop_regex: "^/(?!%sylius_admin.path_name%|new-api|api/.*|api$|media/.*)[^/]++"
- sylius.security.new_api_route: "/new-api"
- sylius.security.new_api_regex: "^%sylius.security.new_api_route%"
- sylius.security.new_api_admin_route: "%sylius.security.new_api_route%/admin"
- sylius.security.new_api_admin_regex: "^%sylius.security.new_api_admin_route%"
- sylius.security.new_api_shop_route: "%sylius.security.new_api_route%/shop"
- sylius.security.new_api_shop_regex: "^%sylius.security.new_api_shop_route%"
-
-security:
- always_authenticate_before_granting: true
- providers:
- sylius_admin_user_provider:
- id: sylius.admin_user_provider.email_or_name_based
- sylius_api_admin_user_provider:
- id: sylius.admin_user_provider.email_or_name_based
- sylius_shop_user_provider:
- id: sylius.shop_user_provider.email_or_name_based
- sylius_api_shop_user_provider:
- id: sylius.shop_user_provider.email_or_name_based
- sylius_api_chain_provider:
- chain:
- providers: [sylius_api_shop_user_provider, sylius_api_admin_user_provider]
-
- encoders:
- Sylius\Component\User\Model\UserInterface: argon2i
- firewalls:
- admin:
- switch_user: true
- context: admin
- pattern: "%sylius.security.admin_regex%"
- provider: sylius_admin_user_provider
- form_login:
- provider: sylius_admin_user_provider
- login_path: sylius_admin_login
- check_path: sylius_admin_login_check
- failure_path: sylius_admin_login
- default_target_path: sylius_admin_dashboard
- use_forward: false
- use_referer: true
- csrf_token_generator: security.csrf.token_manager
- csrf_parameter: _csrf_admin_security_token
- csrf_token_id: admin_authenticate
- remember_me:
- secret: "%env(APP_SECRET)%"
- path: "/%sylius_admin.path_name%"
- name: APP_ADMIN_REMEMBER_ME
- lifetime: 31536000
- remember_me_parameter: _remember_me
- logout:
- path: sylius_admin_logout
- target: sylius_admin_login
- anonymous: true
-
- oauth_token:
- pattern: "%sylius.security.api_regex%/oauth/v2/token"
- security: false
-
- new_api_admin_user:
- pattern: "%sylius.security.new_api_route%/admin-user-authentication-token"
- provider: sylius_admin_user_provider
- stateless: true
- anonymous: true
- json_login:
- check_path: "%sylius.security.new_api_route%/admin-user-authentication-token"
- username_path: email
- password_path: password
- success_handler: lexik_jwt_authentication.handler.authentication_success
- failure_handler: lexik_jwt_authentication.handler.authentication_failure
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
-
- new_api_shop_user:
- pattern: "%sylius.security.new_api_route%/shop-user-authentication-token"
- provider: sylius_shop_user_provider
- stateless: true
- anonymous: true
- json_login:
- check_path: "%sylius.security.new_api_route%/shop-user-authentication-token"
- username_path: email
- password_path: password
- success_handler: lexik_jwt_authentication.handler.authentication_success
- failure_handler: lexik_jwt_authentication.handler.authentication_failure
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
-
- new_api:
- pattern: "%sylius.security.new_api_regex%/*"
- provider: sylius_api_chain_provider
- stateless: true
- anonymous: lazy
- guard:
- authenticators:
- - lexik_jwt_authentication.jwt_token_authenticator
-
- api:
- pattern: "%sylius.security.api_regex%/.*"
- provider: sylius_admin_user_provider
- fos_oauth: true
- stateless: true
- anonymous: true
-
- shop:
- switch_user: { role: ROLE_ALLOWED_TO_SWITCH }
- context: shop
- pattern: "%sylius.security.shop_regex%"
- provider: sylius_shop_user_provider
- form_login:
- success_handler: sylius.authentication.success_handler
- failure_handler: sylius.authentication.failure_handler
- provider: sylius_shop_user_provider
- login_path: sylius_shop_login
- check_path: sylius_shop_login_check
- failure_path: sylius_shop_login
- default_target_path: sylius_shop_homepage
- use_forward: false
- use_referer: true
- csrf_token_generator: security.csrf.token_manager
- csrf_parameter: _csrf_shop_security_token
- csrf_token_id: shop_authenticate
- remember_me:
- secret: "%env(APP_SECRET)%"
- name: APP_SHOP_REMEMBER_ME
- lifetime: 31536000
- remember_me_parameter: _remember_me
- logout:
- path: sylius_shop_logout
- target: sylius_shop_login
- invalidate_session: false
- success_handler: sylius.handler.shop_user_logout
- anonymous: true
-
- dev:
- pattern: ^/(_(profiler|wdt)|css|images|js)/
- security: false
-
- access_control:
- - { path: "%sylius.security.admin_regex%/_partial", role: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] }
- - { path: "%sylius.security.admin_regex%/_partial", role: ROLE_NO_ACCESS }
- - { path: "%sylius.security.shop_regex%/_partial", role: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] }
- - { path: "%sylius.security.shop_regex%/_partial", role: ROLE_NO_ACCESS }
-
- - { path: "%sylius.security.admin_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%sylius.security.api_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%sylius.security.shop_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY }
-
- - { path: "%sylius.security.shop_regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY }
- - { path: "%sylius.security.shop_regex%/verify", role: IS_AUTHENTICATED_ANONYMOUSLY }
-
- - { path: "%sylius.security.admin_regex%", role: ROLE_ADMINISTRATION_ACCESS }
- - { path: "%sylius.security.api_regex%/.*", role: ROLE_API_ACCESS }
- - { path: "%sylius.security.shop_regex%/account", role: ROLE_USER }
-
- - { path: "%sylius.security.new_api_admin_regex%/.*", role: ROLE_API_ACCESS }
- - { path: "%sylius.security.new_api_shop_regex%/.*", role: IS_AUTHENTICATED_ANONYMOUSLY }
diff --git a/tests/Application/config/sylius/1.9/routes/sylius_admin_api.yaml b/tests/Application/config/sylius/1.9/routes/sylius_admin_api.yaml
deleted file mode 100644
index 80aed457..00000000
--- a/tests/Application/config/sylius/1.9/routes/sylius_admin_api.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-sylius_admin_api:
- resource: "@SyliusAdminApiBundle/Resources/config/routing.yml"
- prefix: /api
diff --git a/tests/Unit/DataTransformer/AddProductBundleToCartDtoDataTransformerTest.php b/tests/Unit/DataTransformer/AddProductBundleToCartDtoDataTransformerTest.php
new file mode 100644
index 00000000..c99e1ca5
--- /dev/null
+++ b/tests/Unit/DataTransformer/AddProductBundleToCartDtoDataTransformerTest.php
@@ -0,0 +1,69 @@
+expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ sprintf(TypeExceptionMessage::EXPECTED_INSTANCE_OF_X_GOT_Y, AddProductBundleToCartDto::class, \stdClass::class)
+ );
+
+ $object = new \stdClass();
+ $dataTransformer = new AddProductBundleToCartDtoDataTransformer();
+
+ $dataTransformer->transform($object, '');
+ }
+
+ public function testThrowIfObjectToPopulateDoesntExist(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(TypeExceptionMessage::EXPECTED_VALUE_OTHER_THAN_NULL);
+
+ $object = AddProductBundleToCartDtoMother::create('PRODUCT_CODE');
+ $dataTransformer = new AddProductBundleToCartDtoDataTransformer();
+
+ $dataTransformer->transform($object, '');
+ }
+
+ public function testReturnAddProductBundleToCart(): void
+ {
+ $object = AddProductBundleToCartDtoMother::create('PRODUCT_CODE', 2);
+ $context = [
+ AddProductBundleToCartDtoDataTransformer::OBJECT_TO_POPULATE => OrderMother::createWithId(3),
+ ];
+ $dataTransformer = new AddProductBundleToCartDtoDataTransformer();
+
+ $addProductBundleToCartCommand = $dataTransformer->transform($object, '', $context);
+
+ self::assertInstanceOf(AddProductBundleToCartCommand::class, $addProductBundleToCartCommand);
+ self::assertSame('PRODUCT_CODE', $addProductBundleToCartCommand->getProductCode());
+ self::assertSame(2, $addProductBundleToCartCommand->getQuantity());
+ self::assertSame(3, $addProductBundleToCartCommand->getOrderId());
+ }
+}
diff --git a/tests/Unit/Factory/AddProductBundleItemToCartCommandFactoryTest.php b/tests/Unit/Factory/AddProductBundleItemToCartCommandFactoryTest.php
new file mode 100644
index 00000000..211cf9af
--- /dev/null
+++ b/tests/Unit/Factory/AddProductBundleItemToCartCommandFactoryTest.php
@@ -0,0 +1,29 @@
+createNew($productBundleItem);
+
+ self::assertInstanceOf(AddProductBundleItemToCartCommand::class, $command);
+ }
+}
diff --git a/tests/Unit/Factory/AddProductBundleToCartCommandFactoryTest.php b/tests/Unit/Factory/AddProductBundleToCartCommandFactoryTest.php
new file mode 100644
index 00000000..c146ffae
--- /dev/null
+++ b/tests/Unit/Factory/AddProductBundleToCartCommandFactoryTest.php
@@ -0,0 +1,49 @@
+createNew(self::ORDER_ID, self::PRODUCT_CODE, self::QUANTITY);
+
+ self::assertInstanceOf(AddProductBundleToCartCommand::class, $command);
+ self::assertEquals(self::ORDER_ID, $command->getOrderId());
+ self::assertEquals(self::PRODUCT_CODE, $command->getProductCode());
+ self::assertEquals(self::QUANTITY, $command->getQuantity());
+ }
+
+ public function testCreateAddProductBundleToCartCommandObjectFromDto(): void
+ {
+ $dto = AddProductBundleToCartDtoMother::createWithOrderIdAndProductCode(self::ORDER_ID, self::PRODUCT_CODE);
+
+ $factory = new AddProductBundleToCartCommandFactory();
+ $command = $factory->createFromDto($dto);
+
+ self::assertInstanceOf(AddProductBundleToCartCommand::class, $command);
+ self::assertEquals(self::ORDER_ID, $command->getOrderId());
+ self::assertEquals(self::PRODUCT_CODE, $command->getProductCode());
+ self::assertEquals(0, $command->getQuantity());
+ }
+}
diff --git a/tests/Unit/Factory/AddProductBundleToCartDtoFactoryTest.php b/tests/Unit/Factory/AddProductBundleToCartDtoFactoryTest.php
new file mode 100644
index 00000000..a7a4fd07
--- /dev/null
+++ b/tests/Unit/Factory/AddProductBundleToCartDtoFactoryTest.php
@@ -0,0 +1,64 @@
+addProductBundleItemToCartCommandFactory = $this->createMock(
+ AddProductBundleItemToCartCommandFactoryInterface::class
+ );
+ }
+
+ public function testCreateAddProductBundleToCartDtoObject(): void
+ {
+ $bundleItem1 = ProductBundleItemMother::create();
+ $bundleItem2 = ProductBundleItemMother::create();
+ $addProductBundleItemToCartCommand1 = AddProductBundleItemToCartCommandMother::create($bundleItem1);
+ $addProductBundleItemToCartCommand2 = AddProductBundleItemToCartCommandMother::create($bundleItem2);
+
+ $this->addProductBundleItemToCartCommandFactory->expects(self::exactly(2))
+ ->method('createNew')
+ ->withConsecutive([$bundleItem1], [$bundleItem2])
+ ->willReturnOnConsecutiveCalls($addProductBundleItemToCartCommand1, $addProductBundleItemToCartCommand2)
+ ;
+
+ $factory = new AddProductBundleToCartDtoFactory($this->addProductBundleItemToCartCommandFactory);
+
+ $order = OrderMother::create();
+ $orderItem = OrderItemMother::create();
+ $productBundle = ProductBundleMother::createWithBundleItems($bundleItem1, $bundleItem2);
+ $product = ProductMother::createWithBundle($productBundle);
+ $dto = $factory->createNew($order, $orderItem, $product);
+
+ self::assertInstanceOf(AddProductBundleToCartDtoInterface::class, $dto);
+ self::assertSame($order, $dto->getCart());
+ self::assertSame($orderItem, $dto->getCartItem());
+ self::assertSame($product, $dto->getProduct());
+ self::assertCount(2, $dto->getProductBundleItems());
+ }
+}
diff --git a/tests/Unit/Factory/OrderItemFactoryTest.php b/tests/Unit/Factory/OrderItemFactoryTest.php
new file mode 100644
index 00000000..7dc5bf6a
--- /dev/null
+++ b/tests/Unit/Factory/OrderItemFactoryTest.php
@@ -0,0 +1,37 @@
+createMock(CartItemFactoryInterface::class);
+ $baseFactory->expects(self::once())
+ ->method('createNew')
+ ->willReturn($orderItem)
+ ;
+
+ $factory = new OrderItemFactory($baseFactory);
+ $orderItemWithVariant = $factory->createWithVariant($productVariant);
+
+ self::assertSame($productVariant, $orderItemWithVariant->getVariant());
+ }
+}
diff --git a/tests/Unit/Factory/ProductBundleOrderItemFactoryTest.php b/tests/Unit/Factory/ProductBundleOrderItemFactoryTest.php
new file mode 100644
index 00000000..4ccd57bf
--- /dev/null
+++ b/tests/Unit/Factory/ProductBundleOrderItemFactoryTest.php
@@ -0,0 +1,55 @@
+baseProductBundleOrderItemFactory = $this->createMock(FactoryInterface::class);
+ $this->baseProductBundleOrderItemFactory
+ ->method('createNew')
+ ->willReturn(new ProductBundleOrderItem())
+ ;
+ }
+
+ public function testCreateProductBundleOrderItemFromProductBundleItem(): void
+ {
+ $factory = new ProductBundleOrderItemFactory($this->baseProductBundleOrderItemFactory);
+
+ $productVariant = new ProductVariant();
+ $productVariant->setCode(self::PRODUCT_VARIANT_CODE);
+
+ $productBundleItem = new ProductBundleItem();
+ $productBundleItem->setProductVariant($productVariant);
+ $productBundleItem->setQuantity(2);
+
+ $orderItem = $factory->createFromProductBundleItem($productBundleItem);
+ $orderItemProductVariant = $orderItem->getProductVariant();
+
+ self::assertEquals($productBundleItem, $orderItem->getProductBundleItem());
+ self::assertSame($productBundleItem->getQuantity(), $orderItem->getQuantity());
+ self::assertSame($productVariant->getCode(), $orderItemProductVariant->getCode());
+ }
+}
diff --git a/tests/Unit/Handler/AddProductBundleToCartHandler/CartProcessorTest.php b/tests/Unit/Handler/AddProductBundleToCartHandler/CartProcessorTest.php
new file mode 100644
index 00000000..91059c37
--- /dev/null
+++ b/tests/Unit/Handler/AddProductBundleToCartHandler/CartProcessorTest.php
@@ -0,0 +1,241 @@
+orderItemQuantityModifier = $this->createMock(OrderItemQuantityModifierInterface::class);
+ $this->productBundleOrderItemFactory = $this->createMock(ProductBundleOrderItemFactoryInterface::class);
+ $this->orderModifier = $this->createMock(OrderModifierInterface::class);
+ $this->cartItemFactory = $this->createMock(OrderItemFactoryInterface::class);
+ }
+
+ public function testThrowExceptionIfQuantityNotGreaterThanZero(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ $cart = $this->createCart();
+ $productBundle = $this->createProductBundle();
+
+ $processor = $this->createProcessor();
+ $processor->process($cart, $productBundle, 0);
+ }
+
+ public function testThrowExceptionIfProductIsNull(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ $cart = $this->createCart();
+ $productBundle = $this->createProductBundle();
+
+ $processor = $this->createProcessor();
+ $processor->process($cart, $productBundle, 1);
+ }
+
+ public function testThrowExceptionIfProductHasNoVariant(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ $cart = $this->createCart();
+ $productBundle = $this->createProductBundleWithProduct();
+
+ $processor = $this->createProcessor();
+ $processor->process($cart, $productBundle, 1);
+ }
+
+ public function testCreateCartItem(): void
+ {
+ $cart = $this->createCart();
+ $productVariant = new ProductVariant();
+ $product = $this->createProductWithVariant($productVariant);
+ $productBundle = $this->createProductBundleWithProduct($product);
+
+ $this->cartItemFactory->expects(self::once())
+ ->method('createWithVariant')
+ ->with($productVariant)
+ ;
+
+ $processor = $this->createProcessor();
+ $processor->process($cart, $productBundle, 2);
+ }
+
+ public function testModifyCartItemQuantity(): void
+ {
+ $cart = $this->createCart();
+ $productVariant = new ProductVariant();
+ $product = $this->createProductWithVariant($productVariant);
+ $productBundle = $this->createProductBundleWithProduct($product);
+ $cartItem = $this->createCartItem();
+
+ $this->cartItemFactory
+ ->method('createWithVariant')
+ ->willReturn($cartItem)
+ ;
+ $this->orderItemQuantityModifier->expects(self::once())
+ ->method('modify')
+ ->with($cartItem, 2)
+ ;
+
+ $processor = $this->createProcessor();
+ $processor->process($cart, $productBundle, 2);
+ }
+
+ public function testCreateBundleOrderItemsFromBundleItems(): void
+ {
+ $bundleItem1 = $this->createProductBundleItem();
+ $bundleItem2 = $this->createProductBundleItem();
+
+ $productBundleOrderItem1 = $this->createProductBundleOrderItem();
+ $productBundleOrderItem2 = $this->createProductBundleOrderItem();
+
+ $cart = $this->createCart();
+ $product = $this->createProductWithVariant();
+ $productBundle = $this->createProductBundleWithProduct($product);
+ $productBundle->addProductBundleItem($bundleItem1);
+ $productBundle->addProductBundleItem($bundleItem2);
+
+ $cartItem = $this->createMock(OrderItemInterface::class);
+ $cartItem->expects(self::exactly(2))
+ ->method('addProductBundleOrderItem')
+ ->withConsecutive([$productBundleOrderItem1], [$productBundleOrderItem2])
+ ;
+
+ $this->cartItemFactory
+ ->method('createWithVariant')
+ ->willReturn($cartItem)
+ ;
+ $this->productBundleOrderItemFactory->expects(self::exactly(2))
+ ->method('createFromProductBundleItem')
+ ->withConsecutive([$bundleItem1], [$bundleItem2])
+ ->willReturn($productBundleOrderItem1, $productBundleOrderItem2)
+ ;
+
+ $processor = $this->createProcessor();
+ $processor->process($cart, $productBundle, 1);
+ }
+
+ public function testAddCartItemToOrder(): void
+ {
+ $cart = $this->createCart();
+ $product = $this->createProductWithVariant();
+ $productBundle = $this->createProductBundleWithProduct($product);
+ $cartItem = $this->createCartItem();
+
+ $this->cartItemFactory
+ ->method('createWithVariant')
+ ->willReturn($cartItem)
+ ;
+ $this->orderModifier->expects(self::once())
+ ->method('addToOrder')
+ ->with($cart, $cartItem)
+ ;
+
+ $processor = $this->createProcessor();
+ $processor->process($cart, $productBundle, 1);
+ }
+
+ private function createProcessor(): CartProcessorInterface
+ {
+ return new CartProcessor(
+ $this->orderItemQuantityModifier,
+ $this->productBundleOrderItemFactory,
+ $this->orderModifier,
+ $this->cartItemFactory
+ );
+ }
+
+ private function createCart(): OrderInterface
+ {
+ return new Order();
+ }
+
+ private function createCartItem(): OrderItemInterface
+ {
+ return new OrderItem();
+ }
+
+ private function createProductWithVariant(?ProductVariantInterface $productVariant = null): ProductInterface
+ {
+ if (null === $productVariant) {
+ $productVariant = new ProductVariant();
+ }
+
+ $product = new Product();
+ $product->addVariant($productVariant);
+
+ return $product;
+ }
+
+ private function createProductBundle(): ProductBundleInterface
+ {
+ return new ProductBundle();
+ }
+
+ private function createProductBundleWithProduct(?ProductInterface $product = null): ProductBundleInterface
+ {
+ if (null === $product) {
+ $product = new Product();
+ }
+
+ $productBundle = $this->createProductBundle();
+ $productBundle->setProduct($product);
+
+ return $productBundle;
+ }
+
+ private function createProductBundleItem(): ProductBundleItemInterface
+ {
+ return new ProductBundleItem();
+ }
+
+ private function createProductBundleOrderItem(): ProductBundleOrderItemInterface
+ {
+ return new ProductBundleOrderItem();
+ }
+}
diff --git a/tests/Unit/Handler/AddProductBundleToCartHandlerTest.php b/tests/Unit/Handler/AddProductBundleToCartHandlerTest.php
new file mode 100644
index 00000000..60bbbf5b
--- /dev/null
+++ b/tests/Unit/Handler/AddProductBundleToCartHandlerTest.php
@@ -0,0 +1,163 @@
+orderRepository = $this->createMock(OrderRepositoryInterface::class);
+ $this->productRepository = $this->createMock(ProductRepositoryInterface::class);
+ $this->cartProcessor = $this->createMock(CartProcessorInterface::class);
+ }
+
+ public function testThrowExceptionIfCartDoesntExist(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(TypeExceptionMessage::EXPECTED_VALUE_OTHER_THAN_NULL);
+
+ $this->orderRepository->expects(self::once())
+ ->method('findCartById')
+ ->willReturn(null)
+ ;
+
+ $command = new AddProductBundleToCartCommand(0, '', 1);
+ $handler = $this->createHandler();
+ $handler($command);
+ }
+
+ /**
+ * @dataProvider pessimisticDataProvider
+ */
+ public function testPessimisticCase(
+ string $exceptionMessage,
+ ?OrderInterface $cart,
+ ?ProductInterface $product,
+ int $quantity
+ ): void {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage($exceptionMessage);
+
+ $this->orderRepository->method('findCartById')
+ ->willReturn($cart)
+ ;
+
+ $this->productRepository->method('findOneByCode')
+ ->willReturn($product)
+ ;
+
+ $command = new AddProductBundleToCartCommand(0, '', $quantity);
+ $handler = $this->createHandler();
+ $handler($command);
+ }
+
+ public function pessimisticDataProvider(): array
+ {
+ $productBundle = ProductBundleMother::create();
+ $productWithBundle = ProductMother::createWithBundle($productBundle);
+
+ return [
+ 'order is a null' => [TypeExceptionMessage::EXPECTED_VALUE_OTHER_THAN_NULL, null, null, 1],
+ 'product is a null' => [TypeExceptionMessage::EXPECTED_VALUE_OTHER_THAN_NULL, OrderMother::create(), null, 1],
+ 'product is not a bundle' => [
+ 'Expected a value to be true. Got: false',
+ OrderMother::create(),
+ ProductMother::create(),
+ 1,
+ ],
+ 'quantity is not greater than 0' => [
+ 'Expected a value greater than 0. Got: 0',
+ OrderMother::create(),
+ $productWithBundle,
+ 0,
+ ],
+ ];
+ }
+
+ public function testProcessCart(): void
+ {
+ $cart = OrderMother::create();
+ $this->orderRepository->method('findCartById')
+ ->willReturn($cart)
+ ;
+
+ $productBundle = ProductBundleMother::create();
+ $product = ProductMother::createWithBundle($productBundle);
+ $this->productRepository->method('findOneByCode')
+ ->willReturn($product)
+ ;
+
+ $this->cartProcessor->expects(self::once())
+ ->method('process')
+ ->with($cart, $productBundle, 2)
+ ;
+
+ $command = new AddProductBundleToCartCommand(1, '', 2);
+ $handler = $this->createHandler();
+ $handler($command);
+ }
+
+ public function testAddCartToRepository(): void
+ {
+ $cart = OrderMother::create();
+ $this->orderRepository->method('findCartById')
+ ->willReturn($cart)
+ ;
+
+ $productBundle = ProductBundleMother::create();
+ $product = ProductMother::createWithBundle($productBundle);
+ $this->productRepository->method('findOneByCode')
+ ->willReturn($product)
+ ;
+
+ $this->orderRepository->expects(self::once())
+ ->method('add')
+ ->with($cart)
+ ;
+
+ $command = new AddProductBundleToCartCommand(1, '', 1);
+ $handler = $this->createHandler();
+ $handler($command);
+ }
+
+ private function createHandler(): AddProductBundleToCartHandler
+ {
+ return new AddProductBundleToCartHandler(
+ $this->orderRepository,
+ $this->productRepository,
+ $this->cartProcessor
+ );
+ }
+}
diff --git a/tests/Unit/MotherObject/AddProductBundleItemToCartCommandMother.php b/tests/Unit/MotherObject/AddProductBundleItemToCartCommandMother.php
new file mode 100644
index 00000000..3e8ba122
--- /dev/null
+++ b/tests/Unit/MotherObject/AddProductBundleItemToCartCommandMother.php
@@ -0,0 +1,22 @@
+setName($name);
+
+ return $channel;
+ }
+}
diff --git a/tests/Unit/MotherObject/OrderItemMother.php b/tests/Unit/MotherObject/OrderItemMother.php
new file mode 100644
index 00000000..d6187cc4
--- /dev/null
+++ b/tests/Unit/MotherObject/OrderItemMother.php
@@ -0,0 +1,22 @@
+id = $id;
+ };
+ ($setIdClosure->bindTo($order, $order))($id);
+
+ return $order;
+ }
+
+ public static function createWithChannel(ChannelInterface $channel): OrderInterface
+ {
+ $order = self::create();
+
+ $order->setChannel($channel);
+
+ return $order;
+ }
+}
diff --git a/tests/Unit/MotherObject/ProductBundleItemMother.php b/tests/Unit/MotherObject/ProductBundleItemMother.php
new file mode 100644
index 00000000..da8ba40f
--- /dev/null
+++ b/tests/Unit/MotherObject/ProductBundleItemMother.php
@@ -0,0 +1,22 @@
+addProductBundleItem($bundleItem);
+ }
+
+ return $productBundle;
+ }
+}
diff --git a/tests/Unit/MotherObject/ProductMother.php b/tests/Unit/MotherObject/ProductMother.php
new file mode 100644
index 00000000..95789d73
--- /dev/null
+++ b/tests/Unit/MotherObject/ProductMother.php
@@ -0,0 +1,76 @@
+setProductBundle($productBundle);
+
+ return $product;
+ }
+
+ public static function createWithProductVariantAndCode(
+ ProductVariantInterface $productVariant,
+ string $code
+ ): ProductInterface {
+ $product = self::create();
+
+ $product->addVariant($productVariant);
+ $product->setCode($code);
+
+ return $product;
+ }
+
+ public static function createWithChannelAndProductVariantAndCode(
+ ChannelInterface $channel,
+ ProductVariantInterface $productVariant,
+ string $code
+ ): ProductInterface {
+ $product = self::createWithProductVariantAndCode($productVariant, $code);
+
+ $product->addChannel($channel);
+
+ return $product;
+ }
+
+ public static function createWithCode(string $code): ProductInterface
+ {
+ $product = self::create();
+
+ $product->setCode($code);
+
+ return $product;
+ }
+
+ public static function createDisabledWithCode(string $code): ProductInterface
+ {
+ $product = self::createWithCode($code);
+
+ $product->disable();
+
+ return $product;
+ }
+}
diff --git a/tests/Unit/MotherObject/ProductVariantMother.php b/tests/Unit/MotherObject/ProductVariantMother.php
new file mode 100644
index 00000000..dabb38b9
--- /dev/null
+++ b/tests/Unit/MotherObject/ProductVariantMother.php
@@ -0,0 +1,40 @@
+setCode($code);
+
+ return $productVariant;
+ }
+
+ public static function createDisabledWithCode(string $code): ProductVariantInterface
+ {
+ $productVariant = self::createWithCode($code);
+
+ $productVariant->disable();
+
+ return $productVariant;
+ }
+}
diff --git a/tests/Unit/TypeExceptionMessage.php b/tests/Unit/TypeExceptionMessage.php
new file mode 100644
index 00000000..05ea3e09
--- /dev/null
+++ b/tests/Unit/TypeExceptionMessage.php
@@ -0,0 +1,18 @@
+productRepository = $this->createMock(ProductRepositoryInterface::class);
+ $this->orderRepository = $this->createMock(OrderRepositoryInterface::class);
+ $this->availabilityChecker = $this->createMock(AvailabilityCheckerInterface::class);
+
+ parent::setUp();
+ }
+
+ /**
+ * @dataProvider pessimisticDataProvider
+ */
+ public function testPessimisticCase(
+ ProductInterface $product,
+ ?OrderInterface $cart,
+ bool $isStockSufficient,
+ string $violationMessage,
+ array $violationParameters
+ ): void {
+ $this->productRepository->method('findOneByCode')
+ ->with(self::PRODUCT_CODE)
+ ->willReturn($product)
+ ;
+
+ $this->orderRepository->method('findCartById')
+ ->with(self::ORDER_ID)
+ ->willReturn($cart)
+ ;
+
+ $productVariant = $product->getVariants()->first();
+ $this->availabilityChecker->method('isStockSufficient')
+ ->with($productVariant, 1)
+ ->willReturn($isStockSufficient)
+ ;
+
+ $command = new AddProductBundleToCartCommand(self::ORDER_ID, self::PRODUCT_CODE);
+ $constraint = new HasAvailableProductBundle();
+
+ $this->validator->validate($command, $constraint);
+
+ $this->buildViolation($violationMessage)
+ ->setParameters($violationParameters)
+ ->assertRaised();
+ }
+
+ public function pessimisticDataProvider(): iterable
+ {
+ yield 'product is disabled' => $this->getProductDisabledCaseData();
+ yield 'product variant is disabled' => $this->getProductVariantDisabledCaseData();
+ yield 'product\'s channel and cart\'s channel are different' => $this->getProductAndCartChannelsAreDifferentCaseData();
+ yield 'product\'s quantity in the cart exceeds the stock' => $this->getProductQuantityExceedsStockCaseData();
+ }
+
+ private function getProductDisabledCaseData(): array
+ {
+ $product = ProductMother::createDisabledWithCode(self::PRODUCT_CODE);
+ $violationMessage = HasAvailableProductBundle::PRODUCT_DISABLED_MESSAGE;
+ $violationParameters = [
+ '{{ code }}' => self::PRODUCT_CODE,
+ ];
+
+ return [$product, null, false, $violationMessage, $violationParameters];
+ }
+
+ private function getProductVariantDisabledCaseData(): array
+ {
+ $productVariant = ProductVariantMother::createDisabledWithCode(self::PRODUCT_CODE);
+ $product = ProductMother::createWithProductVariantAndCode($productVariant, self::PRODUCT_CODE);
+ $violationMessage = HasAvailableProductBundle::PRODUCT_VARIANT_DISABLED_MESSAGE;
+ $violationParameters = [
+ '{{ code }}' => self::PRODUCT_CODE,
+ ];
+
+ return [$product, null, false, $violationMessage, $violationParameters];
+ }
+
+ private function getProductAndCartChannelsAreDifferentCaseData(): array
+ {
+ $productVariant = ProductVariantMother::createWithCode(self::PRODUCT_CODE);
+ $product = ProductMother::createWithProductVariantAndCode($productVariant, self::PRODUCT_CODE);
+
+ $channel = ChannelMother::createWithName(self::CHANNEL_NAME);
+ $cart = OrderMother::createWithChannel($channel);
+
+ $violationMessage = HasAvailableProductBundle::PRODUCT_DOESNT_EXIST_IN_CHANNEL_MESSAGE;
+ $violationParameters = [
+ '{{ channel }}' => self::CHANNEL_NAME,
+ '{{ code }}' => self::PRODUCT_CODE,
+ ];
+
+ return [$product, $cart, false, $violationMessage, $violationParameters];
+ }
+
+ private function getProductQuantityExceedsStockCaseData(): array
+ {
+ $channel = ChannelMother::createWithName(self::CHANNEL_NAME);
+ $productVariant = ProductVariantMother::createWithCode(self::PRODUCT_CODE);
+ $product = ProductMother::createWithChannelAndProductVariantAndCode(
+ $channel,
+ $productVariant,
+ self::PRODUCT_CODE
+ );
+
+ $cart = OrderMother::createWithChannel($channel);
+
+ $violationMessage = HasAvailableProductBundle::PRODUCT_VARIANT_INSUFFICIENT_STOCK_MESSAGE;
+ $violationParameters = [
+ '{{ code }}' => self::PRODUCT_CODE,
+ ];
+
+ return [$product, $cart, false, $violationMessage, $violationParameters];
+ }
+
+ protected function createValidator(): HasAvailableProductBundleValidator
+ {
+ return new HasAvailableProductBundleValidator(
+ $this->productRepository,
+ $this->orderRepository,
+ $this->availabilityChecker
+ );
+ }
+}
diff --git a/tests/Unit/Validator/HasExistingCartValidatorTest.php b/tests/Unit/Validator/HasExistingCartValidatorTest.php
new file mode 100644
index 00000000..f8b951d8
--- /dev/null
+++ b/tests/Unit/Validator/HasExistingCartValidatorTest.php
@@ -0,0 +1,90 @@
+orderRepository = $this->createMock(OrderRepositoryInterface::class);
+
+ parent::setUp();
+ }
+
+ public function testThrowExceptionIfValueIsNotImplementingOrderIdentityAwareInterface(): void
+ {
+ $this->expectException(UnexpectedValueException::class);
+
+ $value = new \stdClass();
+ $constraint = new HasExistingCart();
+
+ $this->validator->validate($value, $constraint);
+ }
+
+ public function testQueryOrderFromRepositoryIfValueIsInt(): void
+ {
+ $this->orderRepository->expects(self::once())
+ ->method('findCartById')
+ ->with(5)
+ ->willReturn(OrderMother::create())
+ ;
+
+ $value = new AddProductBundleToCartCommand(self::ORDER_ID, self::PRODUCT_CODE);
+ $constraint = new HasExistingCart();
+
+ $this->validator->validate($value, $constraint);
+ }
+
+ public function testAddViolationIfValueIsNull(): void
+ {
+ $this->orderRepository->method('findCartById')
+ ->willReturn(null)
+ ;
+
+ $value = new AddProductBundleToCartCommand(self::ORDER_ID, self::PRODUCT_CODE);
+ $constraint = new HasExistingCart();
+
+ $this->validator->validate($value, $constraint);
+
+ $this->buildViolation(HasExistingCart::CART_DOESNT_EXIST_MESSAGE)->assertRaised();
+ }
+
+ public function testAddViolationIfOrderHasNoId(): void
+ {
+ $value = new AddProductBundleToCartCommand(self::ORDER_ID, self::PRODUCT_CODE);
+ $constraint = new HasExistingCart();
+
+ $this->validator->validate($value, $constraint);
+
+ $this->buildViolation(HasExistingCart::CART_DOESNT_EXIST_MESSAGE)->assertRaised();
+ }
+
+ protected function createValidator(): HasExistingCartValidator
+ {
+ return new HasExistingCartValidator($this->orderRepository);
+ }
+}
diff --git a/tests/Unit/Validator/HasProductBundleTest.php b/tests/Unit/Validator/HasProductBundleTest.php
new file mode 100644
index 00000000..76c23137
--- /dev/null
+++ b/tests/Unit/Validator/HasProductBundleTest.php
@@ -0,0 +1,87 @@
+productRepository = $this->createMock(ProductRepositoryInterface::class);
+
+ parent::setUp();
+ }
+
+ public function testThrowExceptionIfValueIsNotImplementingProductCodeAwareInterface(): void
+ {
+ $this->expectException(UnexpectedValueException::class);
+
+ $value = new \stdClass();
+ $constraint = new HasProductBundle();
+
+ $this->validator->validate($value, $constraint);
+ }
+
+ /**
+ * @dataProvider pessimisticDataProvider
+ */
+ public function testPessimisticCase(?ProductInterface $product, ?string $violationMessage): void
+ {
+ $this->productRepository->expects(self::once())
+ ->method('findOneByCode')
+ ->with(self::PRODUCT_CODE)
+ ->willReturn($product)
+ ;
+
+ $value = new AddProductBundleToCartCommand(self::ORDER_ID, self::PRODUCT_CODE);
+ $constraint = new HasProductBundle();
+
+ $this->validator->validate($value, $constraint);
+
+ if (null !== $violationMessage) {
+ $this->buildViolation($violationMessage)->assertRaised();
+ } else {
+ $this->assertNoViolation();
+ }
+ }
+
+ public function pessimisticDataProvider(): array
+ {
+ return [
+ 'product is a null' => [null, HasProductBundle::PRODUCT_DOESNT_EXIST_MESSAGE],
+ 'product is not a bundle' => [ProductMother::create(), HasProductBundle::NOT_A_BUNDLE_MESSAGE],
+ 'product is a bundle' => [ProductMother::createWithBundle(ProductBundleMother::create()), null],
+ ];
+ }
+
+ protected function createValidator(): HasProductBundleValidator
+ {
+ return new HasProductBundleValidator($this->productRepository);
+ }
+}