Merge branch '1.12' into 1.13

* 1.12:
  Provide post-CR fixes
  Fix static analysis
  Adjust scenarios to use less technical language
  Prevent placing an order with not available shipping method
  Add behat scenario covering preventing a placing an order with a disabled shipping method
  Add behat scenario covering expires session while cart updating
  [API] Fix CommandDenormalizer ignoring custom property names
  Update OrderController.php with early return
  check before refreshing from db
This commit is contained in:
Grzegorz Sadowski 2023-07-21 07:19:19 +02:00
commit 34e77e5d0f
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
25 changed files with 598 additions and 22 deletions

View file

@ -11,3 +11,12 @@ Feature: Viewing a cart summary
Scenario: Viewing information about empty cart Scenario: Viewing information about empty cart
When I see the summary of my cart When I see the summary of my cart
Then my cart should be empty Then my cart should be empty
@ui @no-api
Scenario: Viewing information about empty cart after clearing cookies
Given the store has a product "T-Shirt banana" priced at "$12.54"
And I added this product to the cart
And I am on the summary of my cart page
But I've been gone for a long time
When I try to update my cart
Then I should see an empty cart

View file

@ -0,0 +1,28 @@
@checkout
Feature: Preventing placing an order with a disabled shipping method
In order to have my order shipped without issues
As a Customer
I want to be prevented from placing an order with a disabled shipping method
Background:
Given the store operates on a single channel in the "United States" named "US Web Store"
And the store has a product "Ubi T-Shirt" priced at "$19.99"
And the store has "Raven Post" shipping method with "$4.00" fee
And the store allows paying "Offline"
And I am a logged in customer
@ui @api
Scenario: Being prevented from placing an order with a shipping method that's disabled after completing the shipping method choice step
Given I added product "Ubi T-Shirt" to the cart
And I have proceeded through checkout process with "Raven Post" shipping method
But this shipping method has been disabled
When I try to confirm my order
Then I should not be able to confirm order because the "Raven Post" shipping method is not available
@ui @api
Scenario: Being prevented from placing an order with a shipping method that's has been disabled for the customer's country after completing the shipping method choice step
Given I added product "Ubi T-Shirt" to the cart
And I have proceeded through checkout process with "Raven Post" shipping method
But this shipping method has been disabled for "US Web Store" channel
When I try to confirm my order
Then I should not be able to confirm order because the "Raven Post" shipping method is not available

View file

@ -39,3 +39,4 @@ parameters:
- '/Class Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken constructor invoked with 4 parameters\, 2\-3 required./' - '/Class Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken constructor invoked with 4 parameters\, 2\-3 required./'
- '/Method Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface\:\:supportsNormalization\(\) invoked with 3 parameters\, 1\-2 required\./' - '/Method Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface\:\:supportsNormalization\(\) invoked with 3 parameters\, 1\-2 required\./'
- '/(Interface|Class) [a-zA-Z\\]+ specifies template type (\w+) of interface [a-zA-Z\\]+ as [a-zA-Z\\]+ (of [a-zA-Z\\]+ )?but it''s already specified as/' # turns off a weird generics check behavior, we are basing on Psalm for generics checks - '/(Interface|Class) [a-zA-Z\\]+ specifies template type (\w+) of interface [a-zA-Z\\]+ as [a-zA-Z\\]+ (of [a-zA-Z\\]+ )?but it''s already specified as/' # turns off a weird generics check behavior, we are basing on Psalm for generics checks
- '/Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::normalize\(\) invoked with 2 parameters, 1 required./'

View file

@ -252,6 +252,7 @@
<referencedFunction name="Symfony\Component\HttpKernel\Config\FileLocator::__construct" /> <referencedFunction name="Symfony\Component\HttpKernel\Config\FileLocator::__construct" />
<referencedFunction name="Symfony\Contracts\EventDispatcher\EventDispatcherInterface::dispatch" /> <referencedFunction name="Symfony\Contracts\EventDispatcher\EventDispatcherInterface::dispatch" />
<referencedFunction name="Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken::__construct" /> <!-- removed parameter in Symfony 5.4 --> <referencedFunction name="Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken::__construct" /> <!-- removed parameter in Symfony 5.4 -->
<referencedFunction name="Symfony\Component\Serializer\NameConverter\NameConverterInterface::normalize" />
</errorLevel> </errorLevel>
<errorLevel type="suppress"> <errorLevel type="suppress">
<referencedFunction name="Doctrine\ORM\Query\Expr::andX" /> <referencedFunction name="Doctrine\ORM\Query\Expr::andX" />

View file

@ -523,6 +523,22 @@ final class CheckoutContext implements Context
$this->iChoosePaymentMethod($paymentMethod); $this->iChoosePaymentMethod($paymentMethod);
} }
/**
* @Given I have proceeded through checkout process with :shippingMethod shipping method
*/
public function iHaveProceededThroughCheckoutProcessWithShippingMethod(ShippingMethodInterface $shippingMethod): void
{
$this->addressOrder($this->getArrayWithDefaultAddress());
$this->selectShippingMethod($shippingMethod);
/** @var PaymentMethodInterface $paymentMethod */
$paymentMethod = $this->paymentMethodRepository->findOneBy([]);
$this->iChoosePaymentMethod($paymentMethod);
$this->sharedStorage->set('shipping_method', $shippingMethod);
}
/** /**
* @Then /^(address "[^"]+", "[^"]+", "[^"]+", "[^"]+", "[^"]+", "[^"]+") should be filled as (billing) address$/ * @Then /^(address "[^"]+", "[^"]+", "[^"]+", "[^"]+", "[^"]+", "[^"]+") should be filled as (billing) address$/
* @Then /^the visitor should has ("[^"]+", "[^"]+", "[^"]+", "[^"]+", "[^"]+" specified as) (billing) address$/ * @Then /^the visitor should has ("[^"]+", "[^"]+", "[^"]+", "[^"]+", "[^"]+" specified as) (billing) address$/
@ -1226,6 +1242,20 @@ final class CheckoutContext implements Context
); );
} }
/**
* @Then I should not be able to confirm order because the :shippingMethodName shipping method is not available
*/
public function iShouldNotBeAbleToConfirmOrderBecauseTheShippingMethodIsNotAvailable(string $shippingMethodName): void
{
Assert::same(
$this->responseChecker->getError($this->client->getLastResponse()),
sprintf(
'The "%s" shipping method is not available. Please reselect your shipping method.',
$shippingMethodName,
),
);
}
/** /**
* @When /^I should see (product "[^"]+") with unit price ("[^"]+")$/ * @When /^I should see (product "[^"]+") with unit price ("[^"]+")$/
*/ */

View file

@ -546,6 +546,24 @@ final class ShippingContext implements Context
$this->addRuleToShippingMethod($rule, $shippingMethod); $this->addRuleToShippingMethod($rule, $shippingMethod);
} }
/**
* @Given /^(this shipping method) has been disabled$/
* @Given /^(this shipping method) has been disabled for ("[^"]+" channel)$/
*/
public function thisShippingMethodHasBeenDisabled(ShippingMethodInterface $shippingMethod, ?ChannelInterface $channel = null): void
{
/** @var ShippingMethodInterface $shippingMethod */
$shippingMethod = $this->shippingMethodRepository->findOneBy(['code' => $shippingMethod->getCode()]);
if (null === $channel) {
$shippingMethod->disable();
} else {
$shippingMethod->removeChannel($channel);
}
$this->shippingMethodManager->flush();
}
private function getConfigurationByChannels(array $channels, int $amount = 0): array private function getConfigurationByChannels(array $channels, int $amount = 0): array
{ {
$configuration = []; $configuration = [];

View file

@ -15,6 +15,7 @@ namespace Sylius\Behat\Context\Ui\Shop;
use Behat\Behat\Context\Context; use Behat\Behat\Context\Context;
use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Exception\ElementNotFoundException;
use Sylius\Behat\Element\BrowserElementInterface;
use Sylius\Behat\Element\Shop\CartWidgetElementInterface; use Sylius\Behat\Element\Shop\CartWidgetElementInterface;
use Sylius\Behat\Element\Shop\CheckoutSubtotalElementInterface; use Sylius\Behat\Element\Shop\CheckoutSubtotalElementInterface;
use Sylius\Behat\NotificationType; use Sylius\Behat\NotificationType;
@ -40,6 +41,7 @@ final class CartContext implements Context
private CartWidgetElementInterface $cartWidgetElement, private CartWidgetElementInterface $cartWidgetElement,
private NotificationCheckerInterface $notificationChecker, private NotificationCheckerInterface $notificationChecker,
private SessionManagerInterface $sessionManager, private SessionManagerInterface $sessionManager,
private BrowserElementInterface $browserElement,
) { ) {
} }
@ -53,8 +55,17 @@ final class CartContext implements Context
$this->summaryPage->open(); $this->summaryPage->open();
} }
/**
* @Given I've been gone for a long time
*/
public function iveBeenGoneForLongTime(): void
{
$this->browserElement->resetSession();
}
/** /**
* @When I update my cart * @When I update my cart
* @When I try to update my cart
*/ */
public function iUpdateMyCart() public function iUpdateMyCart()
{ {
@ -590,6 +601,14 @@ final class CartContext implements Context
Assert::same($this->checkoutSubtotalElement->getProductQuantity($productName), $quantity); Assert::same($this->checkoutSubtotalElement->getProductQuantity($productName), $quantity);
} }
/**
* @Then I should see an empty cart
*/
public function iShouldSeeAnEmptyCart(): void
{
Assert::true($this->summaryPage->isEmpty());
}
private function getPriceFromString(string $price): int private function getPriceFromString(string $price): int
{ {
return (int) round((float) str_replace(['€', '£', '$'], '', $price) * 100, 2); return (int) round((float) str_replace(['€', '£', '$'], '', $price) * 100, 2);

View file

@ -372,6 +372,20 @@ final class CheckoutCompleteContext implements Context
throw new UnexpectedPageException('It should not be possible to complete checkout complete step.'); throw new UnexpectedPageException('It should not be possible to complete checkout complete step.');
} }
/**
* @Then I should not be able to confirm order because the :shippingMethodName shipping method is not available
*/
public function iShouldNotBeAbleToConfirmOrderBecauseTheShippingMethodIsNotAvailable(string $shippingMethodName): void
{
Assert::same(
$this->completePage->getValidationErrors(),
sprintf(
'The "%s" shipping method is not available. Please reselect your shipping method.',
$shippingMethodName,
),
);
}
/** /**
* @When /^I should see (product "[^"]+") with unit price ("[^"]+")$/ * @When /^I should see (product "[^"]+") with unit price ("[^"]+")$/
*/ */

View file

@ -25,7 +25,9 @@ use Sylius\Behat\Page\Shop\Checkout\CompletePageInterface;
use Sylius\Behat\Page\Shop\Checkout\SelectPaymentPageInterface; use Sylius\Behat\Page\Shop\Checkout\SelectPaymentPageInterface;
use Sylius\Behat\Page\Shop\Checkout\SelectShippingPageInterface; use Sylius\Behat\Page\Shop\Checkout\SelectShippingPageInterface;
use Sylius\Behat\Service\Resolver\CurrentPageResolverInterface; use Sylius\Behat\Service\Resolver\CurrentPageResolverInterface;
use Sylius\Behat\Service\SharedStorageInterface;
use Sylius\Component\Addressing\Model\CountryInterface; use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Core\Model\ShippingMethodInterface;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
final class CheckoutContext implements Context final class CheckoutContext implements Context
@ -41,6 +43,7 @@ final class CheckoutContext implements Context
private CheckoutAddressingContext $addressingContext, private CheckoutAddressingContext $addressingContext,
private CheckoutShippingContext $shippingContext, private CheckoutShippingContext $shippingContext,
private CheckoutPaymentContext $paymentContext, private CheckoutPaymentContext $paymentContext,
private SharedStorageInterface $sharedStorage,
) { ) {
} }
@ -99,6 +102,18 @@ final class CheckoutContext implements Context
$this->paymentContext->iCompleteThePaymentStep(); $this->paymentContext->iCompleteThePaymentStep();
} }
/**
* @Given I have proceeded through checkout process with :shippingMethod shipping method
*/
public function iHaveProceededThroughCheckoutProcessWithShippingMethod(ShippingMethodInterface $shippingMethod): void
{
$this->addressingContext->iProceedSelectingBillingCountry();
$this->shippingContext->iHaveProceededSelectingShippingMethod($shippingMethod->getName());
$this->paymentContext->iCompleteThePaymentStep();
$this->sharedStorage->set('shipping_method', $shippingMethod);
}
/** /**
* @When /^I proceed selecting ("[^"]+" as billing country) with "([^"]+)" method$/ * @When /^I proceed selecting ("[^"]+" as billing country) with "([^"]+)" method$/
*/ */

View file

@ -21,4 +21,9 @@ final class BrowserElement extends Element implements BrowserElementInterface
{ {
$this->getDriver()->back(); $this->getDriver()->back();
} }
public function resetSession(): void
{
$this->getSession()->setCookie('MOCKSESSID');
}
} }

View file

@ -16,4 +16,6 @@ namespace Sylius\Behat\Element;
interface BrowserElementInterface interface BrowserElementInterface
{ {
public function goBack(): void; public function goBack(): void;
public function resetSession(): void;
} }

View file

@ -372,6 +372,7 @@
<argument type="service" id="sylius.behat.context.ui.shop.checkout.addressing" /> <argument type="service" id="sylius.behat.context.ui.shop.checkout.addressing" />
<argument type="service" id="sylius.behat.context.ui.shop.checkout.shipping" /> <argument type="service" id="sylius.behat.context.ui.shop.checkout.shipping" />
<argument type="service" id="sylius.behat.context.ui.shop.checkout.payment" /> <argument type="service" id="sylius.behat.context.ui.shop.checkout.payment" />
<argument type="service" id="sylius.behat.shared_storage" />
</service> </service>
<service id="sylius.behat.context.ui.shop.checkout.thank_you" class="Sylius\Behat\Context\Ui\Shop\Checkout\CheckoutThankYouContext"> <service id="sylius.behat.context.ui.shop.checkout.thank_you" class="Sylius\Behat\Context\Ui\Shop\Checkout\CheckoutThankYouContext">
@ -484,6 +485,7 @@
<argument type="service" id="Sylius\Behat\Element\Shop\CartWidgetElementInterface" /> <argument type="service" id="Sylius\Behat\Element\Shop\CartWidgetElementInterface" />
<argument type="service" id="sylius.behat.notification_checker" /> <argument type="service" id="sylius.behat.notification_checker" />
<argument type="service" id="Sylius\Behat\Service\SessionManagerInterface"/> <argument type="service" id="Sylius\Behat\Service\SessionManagerInterface"/>
<argument type="service" id="sylius.behat.element.browser" />
</service> </service>
<service id="sylius.behat.context.ui.shop.contact" class="Sylius\Behat\Context\Ui\Shop\ContactContext"> <service id="sylius.behat.context.ui.shop.contact" class="Sylius\Behat\Context\Ui\Shop\ContactContext">

View file

@ -32,6 +32,7 @@ default:
- sylius.behat.context.setup.zone - sylius.behat.context.setup.zone
- Sylius\Behat\Context\Setup\CatalogPromotionContext - Sylius\Behat\Context\Setup\CatalogPromotionContext
- sylius.behat.context.ui.browser
- sylius.behat.context.ui.channel - sylius.behat.context.ui.channel
- sylius.behat.context.ui.shop.cart - sylius.behat.context.ui.shop.cart
- sylius.behat.context.ui.shop.currency - sylius.behat.context.ui.shop.currency

View file

@ -34,6 +34,7 @@
<service id="Sylius\Bundle\ApiBundle\Serializer\CommandDenormalizer"> <service id="Sylius\Bundle\ApiBundle\Serializer\CommandDenormalizer">
<argument type="service" id="api_platform.serializer.normalizer.item" /> <argument type="service" id="api_platform.serializer.normalizer.item" />
<argument type="service" id="api_platform.name_converter" />
<tag name="serializer.normalizer" /> <tag name="serializer.normalizer" />
</service> </service>

View file

@ -14,18 +14,19 @@ declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Serializer; namespace Sylius\Bundle\ApiBundle\Serializer;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/** /** @experimental */
* @experimental
*/
final class CommandDenormalizer implements ContextAwareDenormalizerInterface final class CommandDenormalizer implements ContextAwareDenormalizerInterface
{ {
private const OBJECT_TO_POPULATE = 'object_to_populate'; private const OBJECT_TO_POPULATE = 'object_to_populate';
public function __construct(private DenormalizerInterface $itemNormalizer) public function __construct(
{ private DenormalizerInterface $itemNormalizer,
private NameConverterInterface $nameConverter,
) {
} }
public function supportsDenormalization($data, $type, $format = null, array $context = []): bool public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
@ -39,23 +40,28 @@ final class CommandDenormalizer implements ContextAwareDenormalizerInterface
return $this->itemNormalizer->denormalize($data, $type, $format, $context); return $this->itemNormalizer->denormalize($data, $type, $format, $context);
} }
$constructor = (new \ReflectionClass($context['input']['class']))->getConstructor(); $class = $context['input']['class'];
$constructor = (new \ReflectionClass($class))->getConstructor();
if (null !== $constructor) { if (null !== $constructor) {
$this->assertConstructorArgumentsPresence($constructor, $data); $this->assertConstructorArgumentsPresence($constructor, $class, $data);
} }
return $this->itemNormalizer->denormalize($data, $type, $format, $context); return $this->itemNormalizer->denormalize($data, $type, $format, $context);
} }
private function assertConstructorArgumentsPresence(\ReflectionMethod $constructor, $data): void private function assertConstructorArgumentsPresence(
{ \ReflectionMethod $constructor,
string $class,
mixed $data
): void {
$parameters = $constructor->getParameters(); $parameters = $constructor->getParameters();
$missingFields = []; $missingFields = [];
foreach ($parameters as $parameter) { foreach ($parameters as $parameter) {
if (!isset($data[$parameter->getName()]) && !($parameter->allowsNull() || $parameter->isDefaultValueAvailable())) { $name = $this->nameConverter->normalize($parameter->getName(), $class);
$missingFields[] = $parameter->getName(); if (!isset($data[$name]) && !($parameter->allowsNull() || $parameter->isDefaultValueAvailable())) {
$missingFields[] = $name;
} }
} }

View file

@ -18,7 +18,28 @@ use Symfony\Component\Validator\Constraint;
/** @experimental */ /** @experimental */
final class OrderShippingMethodEligibility extends Constraint final class OrderShippingMethodEligibility extends Constraint
{ {
public string $message = 'sylius.order.shipping_method_eligibility'; /**
* @param array<array-key, mixed> $options
*/
public function __construct(
array $options = [],
public string $message = 'sylius.order.shipping_method_eligibility',
public string $methodNotAvailableMessage = 'sylius.order.shipping_method_not_available',
mixed $groups = null,
mixed $payload = null,
) {
parent::__construct($options, $groups, $payload);
}
public function getMessage(): string
{
return $this->message;
}
public function getMethodNotAvailableMessage(): string
{
return $this->methodNotAvailableMessage;
}
public function validatedBy(): string public function validatedBy(): string
{ {

View file

@ -32,7 +32,7 @@ final class OrderShippingMethodEligibilityValidator extends ConstraintValidator
) { ) {
} }
public function validate($value, Constraint $constraint) public function validate(mixed $value, Constraint $constraint)
{ {
Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class); Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class);
@ -49,9 +49,18 @@ final class OrderShippingMethodEligibilityValidator extends ConstraintValidator
/** @var ShippingMethodInterface $shippingMethod */ /** @var ShippingMethodInterface $shippingMethod */
$shippingMethod = $shipment->getMethod(); $shippingMethod = $shipment->getMethod();
if (!$shippingMethod->isEnabled() || !$shippingMethod->getChannels()->contains($order->getChannel())) {
$this->context->addViolation(
$constraint->getMethodNotAvailableMessage(),
['%shippingMethodName%' => $shippingMethod->getName()],
);
continue;
}
if (!$this->eligibilityChecker->isEligible($shipment, $shippingMethod)) { if (!$this->eligibilityChecker->isEligible($shipment, $shippingMethod)) {
$this->context->addViolation( $this->context->addViolation(
$constraint->message, $constraint->getMessage(),
['%shippingMethodName%' => $shippingMethod->getName()], ['%shippingMethodName%' => $shippingMethod->getName()],
); );
} }

View file

@ -19,21 +19,29 @@ use Sylius\Bundle\ApiBundle\Command\Account\RegisterShopUser;
use Sylius\Bundle\ApiBundle\Command\Account\VerifyCustomerAccount; use Sylius\Bundle\ApiBundle\Command\Account\VerifyCustomerAccount;
use Sylius\Component\Core\Model\Customer; use Sylius\Component\Core\Model\Customer;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
final class CommandDenormalizerSpec extends ObjectBehavior final class CommandDenormalizerSpec extends ObjectBehavior
{ {
function let(DenormalizerInterface $baseNormalizer): void function let(DenormalizerInterface $baseNormalizer, NameConverterInterface $nameConverter): void
{ {
$this->beConstructedWith($baseNormalizer); $this->beConstructedWith($baseNormalizer, $nameConverter);
} }
function it_throws_exception_if_not_all_required_parameters_are_present_in_the_context( function it_throws_exception_if_not_all_required_parameters_are_present_in_the_context(
DenormalizerInterface $baseNormalizer, DenormalizerInterface $baseNormalizer,
NameConverterInterface $nameConverter,
): void { ): void {
$baseNormalizer->denormalize(Argument::any())->shouldNotBeCalled(); $baseNormalizer->denormalize(Argument::any())->shouldNotBeCalled();
$nameConverter->normalize('firstName', RegisterShopUser::class)->willReturn('firstName');
$nameConverter->normalize('lastName', RegisterShopUser::class)->willReturn('lastName');
$nameConverter->normalize('email', RegisterShopUser::class)->willReturn('email');
$nameConverter->normalize('password', RegisterShopUser::class)->willReturn('password');
$nameConverter->normalize('subscribedToNewsletter', RegisterShopUser::class)->willReturn('subscribedToNewsletter');
$this $this
->shouldThrow(new MissingConstructorArgumentsException( ->shouldThrow(new MissingConstructorArgumentsException(
'Request does not have the following required fields specified: firstName, lastName.', 'Request does not have the following required fields specified: firstName, lastName.',
@ -52,7 +60,14 @@ final class CommandDenormalizerSpec extends ObjectBehavior
function it_denormalizes_data_if_all_required_parameters_are_specified( function it_denormalizes_data_if_all_required_parameters_are_specified(
DenormalizerInterface $baseNormalizer, DenormalizerInterface $baseNormalizer,
NameConverterInterface $nameConverter,
): void { ): void {
$nameConverter->normalize('firstName', RegisterShopUser::class)->willReturn('firstName');
$nameConverter->normalize('lastName', RegisterShopUser::class)->willReturn('lastName');
$nameConverter->normalize('email', RegisterShopUser::class)->willReturn('email');
$nameConverter->normalize('password', RegisterShopUser::class)->willReturn('password');
$nameConverter->normalize('subscribedToNewsletter', RegisterShopUser::class)->willReturn('subscribedToNewsletter');
$baseNormalizer $baseNormalizer
->denormalize( ->denormalize(
['firstName' => 'John', 'lastName' => 'Doe', 'email' => 'test@example.com', 'password' => 'pa$$word'], ['firstName' => 'John', 'lastName' => 'Doe', 'email' => 'test@example.com', 'password' => 'pa$$word'],
@ -71,6 +86,34 @@ final class CommandDenormalizerSpec extends ObjectBehavior
)->shouldReturn(['key' => 'value']); )->shouldReturn(['key' => 'value']);
} }
function it_denormalizes_data_if_all_required_parameters_are_specified_based_on_their_normalized_names(
DenormalizerInterface $baseNormalizer,
NameConverterInterface $nameConverter,
): void {
$baseNormalizer
->denormalize(
['first_name' => 'John', 'last_name' => 'Doe', 'email_address' => 'test@example.com', 'pass' => 'pa$$word'],
Customer::class,
null,
['input' => ['class' => RegisterShopUser::class]],
)
->willReturn(['key' => 'value'])
;
$nameConverter->normalize('firstName', RegisterShopUser::class)->willReturn('first_name');
$nameConverter->normalize('lastName', RegisterShopUser::class)->willReturn('last_name');
$nameConverter->normalize('email', RegisterShopUser::class)->willReturn('email_address');
$nameConverter->normalize('password', RegisterShopUser::class)->willReturn('pass');
$nameConverter->normalize('subscribedToNewsletter', RegisterShopUser::class)->willReturn('hasNewsletter');
$this->denormalize(
['first_name' => 'John', 'last_name' => 'Doe', 'email_address' => 'test@example.com', 'pass' => 'pa$$word'],
Customer::class,
null,
['input' => ['class' => RegisterShopUser::class]],
)->shouldReturn(['key' => 'value']);
}
function it_does_not_check_parameters_if_there_is_an_object_to_populate( function it_does_not_check_parameters_if_there_is_an_object_to_populate(
DenormalizerInterface $baseNormalizer, DenormalizerInterface $baseNormalizer,
): void { ): void {

View file

@ -14,10 +14,12 @@ declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints; namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use PhpSpec\ObjectBehavior; use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder; use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder;
use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface;
use Sylius\Bundle\ApiBundle\Validator\Constraints\OrderShippingMethodEligibility; use Sylius\Bundle\ApiBundle\Validator\Constraints\OrderShippingMethodEligibility;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ShipmentInterface; use Sylius\Component\Core\Model\ShipmentInterface;
use Sylius\Component\Core\Model\ShippingMethodInterface; use Sylius\Component\Core\Model\ShippingMethodInterface;
@ -87,12 +89,102 @@ final class OrderShippingMethodEligibilityValidatorSpec extends ObjectBehavior
; ;
} }
function it_adds_a_violation_for_every_not_available_shipping_method_attached_to_the_order(
OrderRepositoryInterface $orderRepository,
ExecutionContextInterface $context,
OrderTokenValueAwareInterface $value,
ChannelInterface $channel,
OrderInterface $order,
ShipmentInterface $shipmentOne,
ShipmentInterface $shipmentTwo,
ShippingMethodInterface $shippingMethodOne,
ShippingMethodInterface $shippingMethodTwo,
Collection $channelsCollectionOne,
Collection $channelsCollectionTwo,
): void {
$this->initialize($context);
$value->getOrderTokenValue()->willReturn('ORDERTOKENVALUE');
$orderRepository->findOneBy(['tokenValue' => 'ORDERTOKENVALUE'])->willReturn($order);
$order->getShipments()->willReturn(new ArrayCollection([$shipmentOne->getWrappedObject(), $shipmentTwo->getWrappedObject()]));
$order->getChannel()->willReturn($channel);
$shipmentOne->getMethod()->willReturn($shippingMethodOne);
$shipmentTwo->getMethod()->willReturn($shippingMethodTwo);
$shippingMethodOne->isEnabled()->willReturn(false);
$shippingMethodTwo->isEnabled()->willReturn(true);
$shippingMethodOne->getChannels()->willReturn($channelsCollectionOne);
$shippingMethodTwo->getChannels()->willReturn($channelsCollectionTwo);
$shippingMethodOne->getName()->willReturn('Shipping method one');
$shippingMethodTwo->getName()->willReturn('Shipping method two');
$channelsCollectionOne->contains($channel)->willReturn(true);
$channelsCollectionTwo->contains($channel)->willReturn(false);
$context->addViolation('sylius.order.shipping_method_not_available', ['%shippingMethodName%' => 'Shipping method one'])->shouldBeCalled();
$context->addViolation('sylius.order.shipping_method_not_available', ['%shippingMethodName%' => 'Shipping method two'])->shouldBeCalled();
$this->validate($value, new OrderShippingMethodEligibility());
}
function it_does_not_add_violation_if_all_shipping_methods_are_available(
OrderRepositoryInterface $orderRepository,
ShippingMethodEligibilityCheckerInterface $eligibilityChecker,
ExecutionContextInterface $context,
OrderTokenValueAwareInterface $value,
ChannelInterface $channel,
OrderInterface $order,
ShipmentInterface $shipmentOne,
ShipmentInterface $shipmentTwo,
ShippingMethodInterface $shippingMethodOne,
ShippingMethodInterface $shippingMethodTwo,
Collection $channelsCollectionOne,
Collection $channelsCollectionTwo,
): void {
$this->initialize($context);
$value->getOrderTokenValue()->willReturn('ORDERTOKENVALUE');
$orderRepository->findOneBy(['tokenValue' => 'ORDERTOKENVALUE'])->willReturn($order);
$order->getShipments()->willReturn(new ArrayCollection([$shipmentOne->getWrappedObject(), $shipmentTwo->getWrappedObject()]));
$order->getChannel()->willReturn($channel);
$shipmentOne->getMethod()->willReturn($shippingMethodOne);
$shipmentTwo->getMethod()->willReturn($shippingMethodTwo);
$shippingMethodOne->isEnabled()->willReturn(true);
$shippingMethodTwo->isEnabled()->willReturn(true);
$shippingMethodOne->getChannels()->willReturn($channelsCollectionOne);
$shippingMethodTwo->getChannels()->willReturn($channelsCollectionTwo);
$shippingMethodOne->getName()->willReturn('Shipping method one');
$shippingMethodTwo->getName()->willReturn('Shipping method two');
$channelsCollectionOne->contains($channel)->willReturn(true);
$channelsCollectionTwo->contains($channel)->willReturn(true);
$context->addViolation('sylius.order.shipping_method_not_available', ['%shippingMethodName%' => 'Shipping method one'])->shouldNotBeCalled();
$context->addViolation('sylius.order.shipping_method_not_available', ['%shippingMethodName%' => 'Shipping method two'])->shouldNotBeCalled();
$eligibilityChecker->isEligible($shipmentOne, $shippingMethodOne)->willReturn(true);
$eligibilityChecker->isEligible($shipmentTwo, $shippingMethodTwo)->willReturn(true);
$this->validate($value, new OrderShippingMethodEligibility());
}
function it_adds_violation_if_shipment_does_not_match_with_shipping_method( function it_adds_violation_if_shipment_does_not_match_with_shipping_method(
OrderRepositoryInterface $orderRepository, OrderRepositoryInterface $orderRepository,
ShippingMethodEligibilityCheckerInterface $eligibilityChecker, ShippingMethodEligibilityCheckerInterface $eligibilityChecker,
OrderInterface $order, OrderInterface $order,
ShipmentInterface $shipment, ShipmentInterface $shipment,
ShippingMethodInterface $shippingMethod, ShippingMethodInterface $shippingMethod,
Collection $channelsCollection,
ChannelInterface $channel,
ExecutionContextInterface $executionContext, ExecutionContextInterface $executionContext,
): void { ): void {
$this->initialize($executionContext); $this->initialize($executionContext);
@ -105,12 +197,17 @@ final class OrderShippingMethodEligibilityValidatorSpec extends ObjectBehavior
$orderRepository->findOneBy(['tokenValue' => 'token'])->willReturn($order); $orderRepository->findOneBy(['tokenValue' => 'token'])->willReturn($order);
$order->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()])); $order->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()]));
$order->getChannel()->willReturn($channel);
$shipment->getMethod()->willReturn($shippingMethod); $shipment->getMethod()->willReturn($shippingMethod);
$eligibilityChecker->isEligible($shipment, $shippingMethod)->willReturn(false); $eligibilityChecker->isEligible($shipment, $shippingMethod)->willReturn(false);
$shippingMethod->getName()->willReturn('InPost'); $shippingMethod->getName()->willReturn('InPost');
$shippingMethod->isEnabled()->willReturn(true);
$shippingMethod->getChannels()->willReturn($channelsCollection);
$channelsCollection->contains($channel)->willReturn(true);
$executionContext $executionContext
->addViolation( ->addViolation(
@ -129,6 +226,8 @@ final class OrderShippingMethodEligibilityValidatorSpec extends ObjectBehavior
OrderInterface $order, OrderInterface $order,
ShipmentInterface $shipment, ShipmentInterface $shipment,
ShippingMethodInterface $shippingMethod, ShippingMethodInterface $shippingMethod,
Collection $channelsCollection,
ChannelInterface $channel,
ExecutionContextInterface $executionContext, ExecutionContextInterface $executionContext,
): void { ): void {
$this->initialize($executionContext); $this->initialize($executionContext);
@ -141,12 +240,17 @@ final class OrderShippingMethodEligibilityValidatorSpec extends ObjectBehavior
$orderRepository->findOneBy(['tokenValue' => 'token'])->willReturn($order); $orderRepository->findOneBy(['tokenValue' => 'token'])->willReturn($order);
$order->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()])); $order->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()]));
$order->getChannel()->willReturn($channel);
$shipment->getMethod()->willReturn($shippingMethod); $shipment->getMethod()->willReturn($shippingMethod);
$eligibilityChecker->isEligible($shipment, $shippingMethod)->willReturn(true); $eligibilityChecker->isEligible($shipment, $shippingMethod)->willReturn(true);
$shippingMethod->getName()->willReturn('InPost'); $shippingMethod->getName()->willReturn('InPost');
$shippingMethod->isEnabled()->willReturn(true);
$shippingMethod->getChannels()->willReturn($channelsCollection);
$channelsCollection->contains($channel)->willReturn(true);
$executionContext $executionContext
->addViolation( ->addViolation(

View file

@ -64,7 +64,6 @@
<service id="sylius.validator.product_integrity" class="Sylius\Bundle\CoreBundle\Validator\Constraints\OrderProductEligibilityValidator"> <service id="sylius.validator.product_integrity" class="Sylius\Bundle\CoreBundle\Validator\Constraints\OrderProductEligibilityValidator">
<tag name="validator.constraint_validator" alias="sylius_order_product_eligibility_validator" /> <tag name="validator.constraint_validator" alias="sylius_order_product_eligibility_validator" />
</service> </service>
<service id="sylius.validator.channel_default_locale_enabled" class="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelDefaultLocaleEnabledValidator"> <service id="sylius.validator.channel_default_locale_enabled" class="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelDefaultLocaleEnabledValidator">
<tag name="validator.constraint_validator" alias="sylius_channel_default_locale_enabled" /> <tag name="validator.constraint_validator" alias="sylius_channel_default_locale_enabled" />
</service> </service>

View file

@ -77,6 +77,7 @@ sylius:
payment_method_eligibility: 'This payment method %paymentMethodName% has been disabled. Please reselect your payment method.' payment_method_eligibility: 'This payment method %paymentMethodName% has been disabled. Please reselect your payment method.'
product_eligibility: 'This product %productName% has been disabled.' product_eligibility: 'This product %productName% has been disabled.'
shipping_method_eligibility: 'Product does not fit requirements for %shippingMethodName% shipping method. Please reselect your shipping method.' shipping_method_eligibility: 'Product does not fit requirements for %shippingMethodName% shipping method. Please reselect your shipping method.'
shipping_method_not_available: 'The "%shippingMethodName%" shipping method is not available. Please reselect your shipping method.'
locale: locale:
enabled: enabled:
cannot_disable_base: The base locale cannot be disabled. cannot_disable_base: The base locale cannot be disabled.

View file

@ -17,7 +17,28 @@ use Symfony\Component\Validator\Constraint;
final class OrderShippingMethodEligibility extends Constraint final class OrderShippingMethodEligibility extends Constraint
{ {
public string $message = 'sylius.order.shipping_method_eligibility'; /**
* @param array<array-key, mixed> $options
*/
public function __construct(
array $options = [],
public string $message = 'sylius.order.shipping_method_eligibility',
public string $methodNotAvailableMessage = 'sylius.order.shipping_method_not_available',
mixed $groups = null,
mixed $payload = null,
) {
parent::__construct($options, $groups, $payload);
}
public function getMessage(): string
{
return $this->message;
}
public function getMethodNotAvailableMessage(): string
{
return $this->methodNotAvailableMessage;
}
public function validatedBy(): string public function validatedBy(): string
{ {

View file

@ -14,6 +14,7 @@ declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Validator\Constraints; namespace Sylius\Bundle\CoreBundle\Validator\Constraints;
use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ShippingMethodInterface;
use Sylius\Component\Shipping\Checker\Eligibility\ShippingMethodEligibilityCheckerInterface; use Sylius\Component\Shipping\Checker\Eligibility\ShippingMethodEligibilityCheckerInterface;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
@ -28,7 +29,7 @@ final class OrderShippingMethodEligibilityValidator extends ConstraintValidator
/** /**
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function validate($value, Constraint $constraint): void public function validate(mixed $value, Constraint $constraint): void
{ {
/** @var OrderInterface $value */ /** @var OrderInterface $value */
Assert::isInstanceOf($value, OrderInterface::class); Assert::isInstanceOf($value, OrderInterface::class);
@ -42,9 +43,21 @@ final class OrderShippingMethodEligibilityValidator extends ConstraintValidator
} }
foreach ($shipments as $shipment) { foreach ($shipments as $shipment) {
if (!$this->methodEligibilityChecker->isEligible($shipment, $shipment->getMethod())) { /** @var ShippingMethodInterface $shippingMethod */
$shippingMethod = $shipment->getMethod();
if (!$shippingMethod->isEnabled() || !$shippingMethod->getChannels()->contains($value->getChannel())) {
$this->context->addViolation( $this->context->addViolation(
$constraint->message, $constraint->getMethodNotAvailableMessage(),
['%shippingMethodName%' => $shippingMethod->getName()],
);
continue;
}
if (!$this->methodEligibilityChecker->isEligible($shipment, $shippingMethod)) {
$this->context->addViolation(
$constraint->getMessage(),
['%shippingMethodName%' => $shipment->getMethod()->getName()], ['%shippingMethodName%' => $shipment->getMethod()->getName()],
); );
} }

View file

@ -0,0 +1,209 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* 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\CoreBundle\Validator\Constraints;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\CoreBundle\Validator\Constraints\OrderShippingMethodEligibility;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\ShipmentInterface;
use Sylius\Component\Core\Model\ShippingMethodInterface;
use Sylius\Component\Shipping\Checker\Eligibility\ShippingMethodEligibilityCheckerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class OrderShippingMethodEligibilityValidatorSpec extends ObjectBehavior
{
function let(ShippingMethodEligibilityCheckerInterface $eligibilityChecker): void
{
$this->beConstructedWith($eligibilityChecker);
}
function it_is_a_constraint_validator(): void
{
$this->shouldImplement(ConstraintValidatorInterface::class);
}
function it_throws_an_exception_if_constraint_does_not_extend_order_token_value_aware_interface(): void
{
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', ['', new class() extends Constraint {
}])
;
}
function it_throws_an_exception_if_constraint_is_not_type_of_order_shipping_method_eligibility(Constraint $constraint): void
{
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', ['', $constraint])
;
}
function it_adds_a_violation_for_every_not_available_shipping_method_attached_to_the_order(
ExecutionContextInterface $context,
ChannelInterface $channel,
OrderInterface $value,
ShipmentInterface $shipmentOne,
ShipmentInterface $shipmentTwo,
ShippingMethodInterface $shippingMethodOne,
ShippingMethodInterface $shippingMethodTwo,
Collection $channelsCollectionOne,
Collection $channelsCollectionTwo,
): void {
$this->initialize($context);
$value->getShipments()->willReturn(new ArrayCollection([$shipmentOne->getWrappedObject(), $shipmentTwo->getWrappedObject()]));
$value->getChannel()->willReturn($channel);
$shipmentOne->getMethod()->willReturn($shippingMethodOne);
$shipmentTwo->getMethod()->willReturn($shippingMethodTwo);
$shippingMethodOne->isEnabled()->willReturn(false);
$shippingMethodTwo->isEnabled()->willReturn(true);
$shippingMethodOne->getChannels()->willReturn($channelsCollectionOne);
$shippingMethodTwo->getChannels()->willReturn($channelsCollectionTwo);
$shippingMethodOne->getName()->willReturn('Shipping method one');
$shippingMethodTwo->getName()->willReturn('Shipping method two');
$channelsCollectionOne->contains($channel)->willReturn(true);
$channelsCollectionTwo->contains($channel)->willReturn(false);
$context->addViolation('sylius.order.shipping_method_not_available', ['%shippingMethodName%' => 'Shipping method one'])->shouldBeCalled();
$context->addViolation('sylius.order.shipping_method_not_available', ['%shippingMethodName%' => 'Shipping method two'])->shouldBeCalled();
$this->validate($value, new OrderShippingMethodEligibility());
}
function it_does_not_add_violation_if_all_shipping_methods_are_available(
ShippingMethodEligibilityCheckerInterface $eligibilityChecker,
ExecutionContextInterface $context,
ChannelInterface $channel,
OrderInterface $value,
ShipmentInterface $shipmentOne,
ShipmentInterface $shipmentTwo,
ShippingMethodInterface $shippingMethodOne,
ShippingMethodInterface $shippingMethodTwo,
Collection $channelsCollectionOne,
Collection $channelsCollectionTwo,
): void {
$this->initialize($context);
$value->getShipments()->willReturn(new ArrayCollection([$shipmentOne->getWrappedObject(), $shipmentTwo->getWrappedObject()]));
$value->getChannel()->willReturn($channel);
$shipmentOne->getMethod()->willReturn($shippingMethodOne);
$shipmentTwo->getMethod()->willReturn($shippingMethodTwo);
$shippingMethodOne->isEnabled()->willReturn(true);
$shippingMethodTwo->isEnabled()->willReturn(true);
$shippingMethodOne->getChannels()->willReturn($channelsCollectionOne);
$shippingMethodTwo->getChannels()->willReturn($channelsCollectionTwo);
$shippingMethodOne->getName()->willReturn('Shipping method one');
$shippingMethodTwo->getName()->willReturn('Shipping method two');
$channelsCollectionOne->contains($channel)->willReturn(true);
$channelsCollectionTwo->contains($channel)->willReturn(true);
$context->addViolation('sylius.order.shipping_method_not_available', ['%shippingMethodName%' => 'Shipping method one'])->shouldNotBeCalled();
$context->addViolation('sylius.order.shipping_method_not_available', ['%shippingMethodName%' => 'Shipping method two'])->shouldNotBeCalled();
$eligibilityChecker->isEligible($shipmentOne, $shippingMethodOne)->willReturn(true);
$eligibilityChecker->isEligible($shipmentTwo, $shippingMethodTwo)->willReturn(true);
$this->validate($value, new OrderShippingMethodEligibility());
}
function it_adds_violation_if_shipment_does_not_match_with_shipping_method(
ShippingMethodEligibilityCheckerInterface $eligibilityChecker,
OrderInterface $value,
ShipmentInterface $shipment,
ShippingMethodInterface $shippingMethod,
Collection $channelsCollection,
ChannelInterface $channel,
ExecutionContextInterface $executionContext,
): void {
$this->initialize($executionContext);
$constraint = new OrderShippingMethodEligibility();
$value->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()]));
$value->getChannel()->willReturn($channel);
$shipment->getMethod()->willReturn($shippingMethod);
$eligibilityChecker->isEligible($shipment, $shippingMethod)->willReturn(false);
$shippingMethod->getName()->willReturn('InPost');
$shippingMethod->isEnabled()->willReturn(true);
$shippingMethod->getChannels()->willReturn($channelsCollection);
$channelsCollection->contains($channel)->willReturn(true);
$executionContext
->addViolation(
'sylius.order.shipping_method_eligibility',
['%shippingMethodName%' => 'InPost'],
)
->shouldBeCalled()
;
$this->validate($value, $constraint);
}
function it_does_not_add_a_violation_if_shipment_matches_with_shipping_method(
ShippingMethodEligibilityCheckerInterface $eligibilityChecker,
OrderInterface $value,
ShipmentInterface $shipment,
ShippingMethodInterface $shippingMethod,
Collection $channelsCollection,
ChannelInterface $channel,
ExecutionContextInterface $executionContext,
): void {
$this->initialize($executionContext);
$constraint = new OrderShippingMethodEligibility();
$value->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()]));
$value->getChannel()->willReturn($channel);
$shipment->getMethod()->willReturn($shippingMethod);
$eligibilityChecker->isEligible($shipment, $shippingMethod)->willReturn(true);
$shippingMethod->getName()->willReturn('InPost');
$shippingMethod->isEnabled()->willReturn(true);
$shippingMethod->getChannels()->willReturn($channelsCollection);
$channelsCollection->contains($channel)->willReturn(true);
$executionContext
->addViolation(
'sylius.order.shipping_method_eligibility',
['%shippingMethodName%' => 'InPost'],
)
->shouldNotBeCalled()
;
$this->validate($value, $constraint);
}
}

View file

@ -152,8 +152,12 @@ class OrderController extends ResourceController
private function resetChangesOnCart(OrderInterface $cart): void private function resetChangesOnCart(OrderInterface $cart): void
{ {
if (!$this->manager->contains($cart)) {
return;
}
$this->manager->refresh($cart); $this->manager->refresh($cart);
foreach ($cart->getItems() as $item) { foreach($cart->getItems() as $item) {
$this->manager->refresh($item); $this->manager->refresh($item);
} }
} }