[API] Adding product from another channel

This commit is contained in:
arti0090 2021-05-07 09:05:24 +02:00
parent 7e36ac986e
commit 43f2068de1
10 changed files with 275 additions and 1 deletions

View file

@ -1,7 +1,7 @@
@checkout
Feature: Preventing adding to cart disabled products
In order to have correct products in cart when adding them
As a customer
As a Customer
I want to have the added products validated
Background:

View file

@ -0,0 +1,27 @@
@checkout
Feature: Preventing adding to cart product or variant from another channel
In order to have correct products in cart when adding them
As a Customer
I want to have the added products validated
Background:
Given the store operates on a channel named "France" with hostname "france"
And the store has a "Baquette" configurable product
And this product has "Small", "Medium" and "Large" variants
And the store has a "Pain" product
And the store operates on a channel named "Poland" with hostname "poland"
And I am a logged in customer
@api
Scenario: Preventing customer from adding simple product from another channel
Given I am browsing channel "Poland"
When I pick up my cart
And I try to add product "Pain" to the cart
Then I should be informed that product "Pain" does not exist
@api
Scenario: Preventing customer from adding product with variant from another channel
Given I am browsing channel "Poland"
When I pick up my cart
And I try to add "Large" variant of product "Baquette"
Then I should be informed that product "Baquette" does not exist

View file

@ -859,6 +859,7 @@ final class CheckoutContext implements Context
/**
* @When /^I try to add ("([^"]+)" product variant)$/
* @When /^I try to add ("([^"]+)" variant of product "([^"]+)")$/
*/
public function iTryToAddProductVariant(ProductVariantInterface $productVariant): void
{

View file

@ -48,6 +48,7 @@ default:
- sylius.behat.context.api.admin.managing_orders
- sylius.behat.context.api.shop.cart
- sylius.behat.context.api.shop.channel
- sylius.behat.context.api.shop.address
- sylius.behat.context.api.shop.checkout
- sylius.behat.context.api.shop.login

View file

@ -38,6 +38,12 @@
<tag name="validator.constraint_validator" alias="sylius_api_validator_order_shipping_method_eligibility" />
</service>
<service id="sylius.api.validator.product_available_in_channel" class="Sylius\Bundle\ApiBundle\Validator\Constraints\ProductAvailableInChannelValidator">
<argument type="service" id="sylius.repository.product_variant" />
<argument type="service" id="sylius.repository.order" />
<tag name="validator.constraint_validator" alias="sylius_api_validator_product_available_in_channel" />
</service>
<service id="sylius.api.validator.product_or_variant_enabled" class="Sylius\Bundle\ApiBundle\Validator\Constraints\ProductOrVariantEnabledValidator">
<argument type="service" id="sylius.repository.product_variant" />
<tag name="validator.constraint_validator" alias="sylius_api_validator_product_or_variant_enabled" />

View file

@ -18,6 +18,11 @@
<value>sylius</value>
</option>
</constraint>
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\ProductAvailableInChannel">
<option name="groups">
<value>sylius</value>
</option>
</constraint>
<property name="quantity">
<constraint name="Range">
<option name="min">1</option>

View file

@ -1,3 +1,8 @@
sylius:
order:
not_empty: 'An empty order cannot be completed.'
product:
channel:
does_not_exist: 'The product %productName% does not exist.'
shipment:
shipped: 'You cannot ship a shipment that was shipped before.'

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/** @experimental */
final class ProductAvailableInChannel extends Constraint
{
/** @var string */
public $message = 'sylius.product.channel.does_not_exist';
public function validatedBy(): string
{
return 'sylius_api_validator_product_available_in_channel';
}
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View file

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
use Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Product\Repository\ProductVariantRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
/** @experimental */
final class ProductAvailableInChannelValidator extends ConstraintValidator
{
/** @var ProductVariantRepositoryInterface */
private $productVariantRepository;
/** @var OrderRepositoryInterface */
private $orderRepository;
public function __construct(
ProductVariantRepositoryInterface $productVariantRepository,
OrderRepositoryInterface $orderRepository
) {
$this->productVariantRepository = $productVariantRepository;
$this->orderRepository = $orderRepository;
}
public function validate($value, Constraint $constraint)
{
Assert::isInstanceOf($value, AddItemToCart::class);
/** @var ProductAvailableInChannel $constraint */
Assert::isInstanceOf($constraint, ProductAvailableInChannel::class);
/** @var OrderInterface $cart */
$cart = $this->orderRepository->findCartByTokenValue($value->getOrderTokenValue());
/** @var ProductVariantInterface $productVariant */
$productVariant = $this->productVariantRepository->findOneBy(['code' => $value->productVariantCode]);
Assert::notNull($cartChannel = $cart->getChannel());
/** @var ProductInterface $product */
$product = $productVariant->getProduct();
if (!$product->hasChannel($cartChannel)) {
$this->context->addViolation(
$constraint->message,
['%productName%' => $product->getName()]
);
}
}
}

View file

@ -0,0 +1,128 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart;
use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder;
use Sylius\Bundle\ApiBundle\Validator\Constraints\ProductAvailableInChannel;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Core\Repository\ProductVariantRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class ProductAvailableInChannelValidatorSpec extends ObjectBehavior
{
function let(
ProductVariantRepositoryInterface $productVariantRepository,
OrderRepositoryInterface $orderRepository
): void {
$this->beConstructedWith($productVariantRepository, $orderRepository);
}
function it_is_a_constraint_validator(): void
{
$this->shouldImplement(ConstraintValidatorInterface::class);
}
function it_throws_an_exception_if_value_is_not_an_instance_of_product_or_variant_enabled_class(): void
{
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', [new CompleteOrder(), new class() extends Constraint {
}]);
}
function it_throws_an_exception_if_constraint_is_not_an_instance_of_add_item_to_cart(): void
{
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', ['', new class() extends Constraint {
}]);
}
function it_adds_violation_if_product_is_not_available_in_channel(
ProductVariantRepositoryInterface $productVariantRepository,
OrderRepositoryInterface $orderRepository,
ChannelInterface $channel,
ProductVariantInterface $productVariant,
ProductInterface $product,
OrderInterface $cart,
ExecutionContextInterface $executionContext
): void {
$this->initialize($executionContext);
$value = new AddItemToCart('productCode', 'productVariantCode', 1);
$value->setOrderTokenValue('TOKEN');
$constraint = new ProductAvailableInChannel();
$constraint->message = 'message';
$orderRepository->findCartByTokenValue('TOKEN')->willReturn($cart);
$productVariantRepository->findOneBy(['code' => 'productVariantCode'])->willReturn($productVariant);
$cart->getChannel()->willReturn($channel);
$productVariant->getProduct()->willReturn($product);
$product->hasChannel($channel)->willReturn(false);
$product->getName()->willReturn('PRODUCTNAME');
$executionContext
->addViolation(
'message',
['%productName%' => 'PRODUCTNAME']
)
->shouldBeCalled();
$this->validate($value, $constraint);
}
function it_does_nothing_if_product_is_available_in_channel(
ProductVariantRepositoryInterface $productVariantRepository,
OrderRepositoryInterface $orderRepository,
ChannelInterface $channel,
ProductVariantInterface $productVariant,
ProductInterface $product,
OrderInterface $cart,
ExecutionContextInterface $executionContext
): void {
$this->initialize($executionContext);
$value = new AddItemToCart('productCode', 'productVariantCode', 1);
$value->setOrderTokenValue('TOKEN');
$constraint = new ProductAvailableInChannel();
$constraint->message = 'message';
$orderRepository->findCartByTokenValue('TOKEN')->willReturn($cart);
$productVariantRepository->findOneBy(['code' => 'productVariantCode'])->willReturn($productVariant);
$cart->getChannel()->willReturn($channel);
$productVariant->getProduct()->willReturn($product);
$product->hasChannel($channel)->willReturn(true);
$product->getName()->willReturn('PRODUCTNAME');
$executionContext
->addViolation(
'message',
['%productName%' => 'PRODUCTNAME']
)
->shouldNotBeCalled();
$this->validate($value, $constraint);
}
}