diff --git a/spec/Validator/InStockWishlistValidatorSpec.php b/spec/Validator/InStockWishlistValidatorSpec.php
new file mode 100644
index 00000000..d3f529ce
--- /dev/null
+++ b/spec/Validator/InStockWishlistValidatorSpec.php
@@ -0,0 +1,176 @@
+beConstructedWith($availabilityChecker, $requestStack, $router);
+ }
+
+ public function it_is_initializable(): void
+ {
+ $this->shouldHaveType(InStockWishlistValidator::class);
+ }
+
+ public function it_should_throw_exception_if_value_is_not_instance_of_command_interface(
+ Constraint $constraint,
+ stdClass $value,
+ ): void {
+ $this->shouldThrow(\Exception::class)->during(
+ 'validate',
+ [$value, $constraint],
+ );
+ }
+
+ public function it_should_throw_exception_if_constraint_is_not_instance_of_command_interface(
+ Constraint $constraint,
+ AddToCartCommandInterface $value,
+ ): void {
+ $this->shouldThrow(\Exception::class)->during(
+ 'validate',
+ [$value, $constraint],
+ );
+ }
+
+ public function it_should_throw_exception_if_request_is_null(
+ RequestStack $requestStack,
+ AddToCartCommandInterface $value,
+ ): void {
+ $constraint = new CartItemAvailability();
+
+ $requestStack->getCurrentRequest()->willReturn(null);
+
+ $this->shouldThrow(\Exception::class)->during(
+ 'validate',
+ [$value, $constraint],
+ );
+ }
+
+ public function it_should_do_nothing_if_the_request_is_not_add_to_cart(
+ RequestStack $requestStack,
+ AddToCartCommandInterface $value,
+ Request $request,
+ Router $router,
+ ): void {
+ $constraint = new CartItemAvailability();
+ $exampleArray = [
+ '_route' => 'bitbag_sylius_wishlist_plugin_shop_locale_wishlist_remove_selected_products',
+ '_controller' => 'bitbag_sylius_wishlist_plugin.controller.action.remove_selected_products_from_wishlist',
+ '_locale' => 'en_US',
+ 'wishlistId' => '4',
+ ];
+
+ $requestStack->getCurrentRequest()->willReturn($request);
+ $request->getPathInfo()->willReturn('/en_US/wishlist/4/products/delete');
+ $router->match('/en_US/wishlist/4/products/delete')->willReturn($exampleArray);
+
+ $this->validate($value, $constraint)->shouldReturn(null);
+ }
+
+ public function it_should_do_nothing_if_current_stock_is_not_empty(
+ AvailabilityCheckerInterface $availabilityChecker,
+ RequestStack $requestStack,
+ AddToCartCommandInterface $value,
+ Request $request,
+ Router $router,
+ OrderItemInterface $cartItem,
+ ExecutionContextInterface $context,
+ ProductVariantInterface $variant,
+ OrderInterface $order,
+ ): void {
+ $constraint = new CartItemAvailability();
+ $exampleArray = ['_route' => 'bitbag_sylius_wishlist_plugin_shop_locale_wishlist_add_selected_products',
+ '_controller' => 'bitbag_sylius_wishlist_plugin.controller.action.add_selected_products_to_cart',
+ '_locale' => 'en_US',
+ 'wishlistId' => '4',
+ ];
+
+ $requestStack->getCurrentRequest()->willReturn($request);
+ $request->getPathInfo()->willReturn('/en_US/wishlist/4/products/add');
+ $router->match('/en_US/wishlist/4/products/add')->willReturn($exampleArray);
+ $value->getCartItem()->willReturn($cartItem);
+ $cartItem->getVariant()->willReturn($variant);
+ $cartItem->getQuantity()->willReturn(5);
+ $value->getCart()->willReturn($order);
+ $itemsCollection = new ArrayCollection([$cartItem->getWrappedObject()]);
+ $order->getItems()->willReturn($itemsCollection);
+ $cartItem->equals(Argument::any())->willReturn(false);
+ $availabilityChecker->isStockSufficient($variant, 5)->willReturn(true);
+
+ $context->addViolation(Argument::any(), Argument::any())->shouldNotBeCalled();
+
+ $this->validate($value, $constraint)->shouldReturn(null);
+ }
+
+ public function it_should_do_build_violation_if_current_stock_is_empty_and_user_added_to_cart_item_from_wishlist(
+ AvailabilityCheckerInterface $availabilityChecker,
+ RequestStack $requestStack,
+ AddToCartCommandInterface $value,
+ Request $request,
+ Router $router,
+ OrderItemInterface $cartItem,
+ OrderInterface $order,
+ ProductVariantInterface $variant,
+ ExecutionContextInterface $context,
+ ): void {
+ $constraint = new CartItemAvailability();
+ $constraint->message = 'sylius.cart_item.not_available';
+ $exampleArray = ['_route' => 'bitbag_sylius_wishlist_plugin_shop_locale_wishlist_add_selected_products',
+ '_controller' => 'bitbag_sylius_wishlist_plugin.controller.action.add_selected_products_to_cart',
+ '_locale' => 'en_US',
+ 'wishlistId' => '4',
+ ];
+
+ $requestStack->getCurrentRequest()->willReturn($request);
+ $request->getPathInfo()->willReturn('/en_US/wishlist/4/products/add');
+ $router->match('/en_US/wishlist/4/products/add')->willReturn($exampleArray);
+ $value->getCartItem()->willReturn($cartItem);
+ $cartItem->getVariant()->willReturn($variant);
+ $cartItem->getQuantity()->willReturn(5);
+ $value->getCart()->willReturn($order);
+ $itemsCollection = new ArrayCollection([$cartItem->getWrappedObject()]);
+ $order->getItems()->willReturn($itemsCollection);
+ $cartItem->equals(Argument::any())->willReturn(false);
+ $availabilityChecker->isStockSufficient($variant, 5)->willReturn(false);
+ $variant->getInventoryName()->willReturn('Red T-shirt');
+
+ $context->addViolation(
+ $constraint->message,
+ ['%itemName%' => 'Red T-shirt'],
+ )->shouldBeCalled();
+
+ $this->initialize($context);
+ $this->validate($value, $constraint);
+ }
+}
diff --git a/src/Resources/config/services/validator.xml b/src/Resources/config/services/validator.xml
new file mode 100644
index 00000000..ea329c06
--- /dev/null
+++ b/src/Resources/config/services/validator.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/validation/AddToCartCommand.xml b/src/Resources/config/validation/AddToCartCommand.xml
new file mode 100644
index 00000000..7369f259
--- /dev/null
+++ b/src/Resources/config/validation/AddToCartCommand.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Validator/Constraints/InStockWishlistConstraint.php b/src/Validator/Constraints/InStockWishlistConstraint.php
new file mode 100644
index 00000000..3e57c7b7
--- /dev/null
+++ b/src/Validator/Constraints/InStockWishlistConstraint.php
@@ -0,0 +1,31 @@
+requestStack->getCurrentRequest();
+
+ Assert::notNull($request);
+
+ try {
+ $route = $this->router->match($request->getPathInfo());
+ } catch (ResourceNotFoundException $exception) {
+ throw new AccessDeniedHttpException('Access denied');
+ }
+
+ if (array_key_exists('_route', $route) &&
+ InStockWishlistConstraint::ADD_PRODUCTS_ROUTE !== $route['_route']) {
+ return;
+ }
+
+ /** @var OrderItemInterface $cartItem */
+ $cartItem = $value->getCartItem();
+
+ /** @var StockableInterface $variant */
+ $variant = $cartItem->getVariant();
+
+ $isStockSufficient = $this->availabilityChecker->isStockSufficient(
+ $variant,
+ $cartItem->getQuantity() + $this->getExistingCartItemQuantityFromCart($value->getCart(), $cartItem),
+ );
+
+ if (!$isStockSufficient) {
+ $this->context->addViolation(
+ $constraint->message,
+ ['%itemName%' => $variant->getInventoryName()],
+ );
+ }
+ }
+
+ private function getExistingCartItemQuantityFromCart(OrderInterface $cart, OrderItemInterface $cartItem): int
+ {
+ foreach ($cart->getItems() as $existingCartItem) {
+ if ($existingCartItem->equals($cartItem)) {
+ return $existingCartItem->getQuantity();
+ }
+ }
+
+ return 0;
+ }
+}