diff --git a/features/being_unable_to_reorder_the_order_placed_by_another_customer.feature b/features/being_unable_to_reorder_the_order_placed_by_another_customer.feature new file mode 100644 index 0000000..3501d71 --- /dev/null +++ b/features/being_unable_to_reorder_the_order_placed_by_another_customer.feature @@ -0,0 +1,21 @@ +@reordering +Feature: Being unable to reorder the order placed by another customer + In order to maintain shop security + As a Store Owner + I want Customer to be the only person allowed to reorder their previously placed order + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "Angel T-Shirt" + And the store ships everywhere for free + And the store allows paying with "Cash on Delivery" + And there is a customer "Rick Sanchez" identified by an email "rick.sanchez@wubba-lubba-dub-dub.com" and a password "Morty" + And there is a customer "Morty Smith" identified by an email "morty.smith@wubba-lubba-dub-dub.com" and a password "Rick" + And a customer "Morty Smith" placed an order "#00000666" + And the customer bought a single "Angel T-Shirt" + And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment + + @application + Scenario: Being unable to reorder the order placed by another customer + When the customer "rick.sanchez@wubba-lubba-dub-dub.com" tries to reorder the order "#00000666" + Then the order "#00000666" should not be reordered diff --git a/spec/Checker/OrderCustomerRelationCheckerSpec.php b/spec/Checker/OrderCustomerRelationCheckerSpec.php new file mode 100644 index 0000000..df1e624 --- /dev/null +++ b/spec/Checker/OrderCustomerRelationCheckerSpec.php @@ -0,0 +1,53 @@ +shouldImplement(OrderCustomerRelationCheckerInterface::class); + } + + function it_returns_true_when_order_was_placed_by_customer( + CustomerInterface $orderCustomer, + CustomerInterface $customer, + OrderInterface $order + ): void { + $orderCustomer->getId()->willReturn(1); + $customer->getId()->willReturn(1); + + $order->getCustomer()->willReturn($orderCustomer); + + $this->wasOrderPlacedByCustomer($order, $customer)->shouldReturn(true); + } + + function it_returns_false_when_order_was_not_placed_by_customer( + CustomerInterface $orderCustomer, + CustomerInterface $customer, + OrderInterface $order + ): void { + $orderCustomer->getId()->willReturn(1); + $customer->getId()->willReturn(2); + + $order->getCustomer()->willReturn($orderCustomer); + + $this->wasOrderPlacedByCustomer($order, $customer)->shouldReturn(false); + } + + function it_returns_false_when_order_has_no_customer_assigned( + CustomerInterface $customer, + OrderInterface $order + ): void { + $order->getCustomer()->willReturn(null); + + $this->wasOrderPlacedByCustomer($order, $customer)->shouldReturn(false); + } +} diff --git a/spec/Reorder/ReordererSpec.php b/spec/Reorder/ReordererSpec.php index 9daedd6..511dac7 100644 --- a/spec/Reorder/ReordererSpec.php +++ b/spec/Reorder/ReordererSpec.php @@ -6,14 +6,17 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; +use Nette\InvalidStateException; use PhpSpec\ObjectBehavior; use Sylius\Bundle\MoneyBundle\Formatter\MoneyFormatterInterface; use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\OrderItemInterface; use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\PromotionInterface; use Sylius\Component\Order\Processor\OrderProcessorInterface; +use Sylius\CustomerReorderPlugin\Checker\OrderCustomerRelationCheckerInterface; use Sylius\CustomerReorderPlugin\Factory\OrderFactoryInterface; use Sylius\CustomerReorderPlugin\Reorder\Reorderer; use Sylius\CustomerReorderPlugin\Reorder\ReordererInterface; @@ -32,7 +35,8 @@ function let( MoneyFormatterInterface $moneyFormatter, Session $session, ReorderEligibilityChecker $reorderEligibilityChecker, - ReorderEligibilityCheckerResponseProcessorInterface $reorderEligibilityCheckerResponseProcessor + ReorderEligibilityCheckerResponseProcessorInterface $reorderEligibilityCheckerResponseProcessor, + OrderCustomerRelationCheckerInterface $orderCustomerRelationChecker ): void { $this->beConstructedWith( $orderFactory, @@ -41,7 +45,8 @@ function let( $moneyFormatter, $session, $reorderEligibilityChecker, - $reorderEligibilityCheckerResponseProcessor + $reorderEligibilityCheckerResponseProcessor, + $orderCustomerRelationChecker ); } @@ -59,7 +64,9 @@ function it_creates_and_persists_reorder_from_existing_order( OrderFactoryInterface $orderFactory, EntityManagerInterface $entityManager, ReorderEligibilityChecker $reorderEligibilityChecker, + OrderCustomerRelationCheckerInterface $orderCustomerRelationChecker, ChannelInterface $channel, + CustomerInterface $customer, OrderInterface $order, OrderInterface $reorder, OrderItemInterface $firstOrderItem, @@ -68,6 +75,8 @@ function it_creates_and_persists_reorder_from_existing_order( $order->getTotal()->willReturn(100); $order->getCurrencyCode()->willReturn('USD'); + $orderCustomerRelationChecker->wasOrderPlacedByCustomer($order, $customer)->willReturn(true); + $reorder->getTotal()->willReturn(100); $orderFactory->createFromExistingOrder($order, $channel)->willReturn($reorder); @@ -81,14 +90,16 @@ function it_creates_and_persists_reorder_from_existing_order( $secondOrderItem->getWrappedObject() ])); - $this->reorder($order, $channel); + $this->reorder($order, $channel, $customer); } function it_checks_if_orders_totals_differ( OrderFactoryInterface $orderFactory, EntityManagerInterface $entityManager, ReorderEligibilityChecker $reorderEligibilityChecker, + OrderCustomerRelationCheckerInterface $orderCustomerRelationChecker, ChannelInterface $channel, + CustomerInterface $customer, OrderInterface $order, OrderInterface $reorder, MoneyFormatterInterface $moneyFormatter, @@ -101,6 +112,8 @@ function it_checks_if_orders_totals_differ( $order->getCurrencyCode()->willReturn('USD'); $order->getPromotions()->willReturn($promotions); + $orderCustomerRelationChecker->wasOrderPlacedByCustomer($order, $customer)->willReturn(true); + $reorder->getTotal()->willReturn(150); $reorder->getPromotions()->willReturn($promotions); @@ -122,14 +135,16 @@ function it_checks_if_orders_totals_differ( $entityManager->persist($reorder)->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); - $this->reorder($order, $channel); + $this->reorder($order, $channel, $customer); } function it_checks_if_promotion_is_no_longer_available( OrderFactoryInterface $orderFactory, EntityManagerInterface $entityManager, ReorderEligibilityChecker $reorderEligibilityChecker, + OrderCustomerRelationCheckerInterface $orderCustomerRelationChecker, ChannelInterface $channel, + CustomerInterface $customer, OrderInterface $order, OrderInterface $reorder, MoneyFormatterInterface $moneyFormatter, @@ -144,6 +159,8 @@ function it_checks_if_promotion_is_no_longer_available( $secondPromotion->getWrappedObject() ])); + $orderCustomerRelationChecker->wasOrderPlacedByCustomer($order, $customer)->willReturn(true); + $firstPromotion->getName()->willReturn('test_promotion_01'); $secondPromotion->getName()->willReturn('test_promotion_02'); @@ -169,14 +186,16 @@ function it_checks_if_promotion_is_no_longer_available( $entityManager->persist($reorder)->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); - $this->reorder($order, $channel); + $this->reorder($order, $channel, $customer); } function it_checks_if_price_of_any_item_has_changed( OrderFactoryInterface $orderFactory, EntityManagerInterface $entityManager, ReorderEligibilityChecker $reorderEligibilityChecker, + OrderCustomerRelationCheckerInterface $orderCustomerRelationChecker, ChannelInterface $channel, + CustomerInterface $customer, OrderInterface $order, OrderInterface $reorder, OrderItemInterface $firstOrderItem, @@ -194,6 +213,8 @@ function it_checks_if_price_of_any_item_has_changed( $secondOrderItem->getWrappedObject() ])); + $orderCustomerRelationChecker->wasOrderPlacedByCustomer($order, $customer)->willReturn(true); + $reorder->getItems()->willReturn(new ArrayCollection([ $firstOrderItem->getWrappedObject(), $secondOrderItem->getWrappedObject() @@ -213,14 +234,16 @@ function it_checks_if_price_of_any_item_has_changed( $entityManager->persist($reorder)->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); - $this->reorder($order, $channel); + $this->reorder($order, $channel, $customer); } function it_checks_if_any_item_is_out_of_stock( OrderFactoryInterface $orderFactory, EntityManagerInterface $entityManager, ReorderEligibilityChecker $reorderEligibilityChecker, + OrderCustomerRelationCheckerInterface $orderCustomerRelationChecker, ChannelInterface $channel, + CustomerInterface $customer, OrderInterface $order, OrderInterface $reorder, OrderItemInterface $firstOrderItem, @@ -244,6 +267,8 @@ function it_checks_if_any_item_is_out_of_stock( $secondOrderItem->getWrappedObject() ])); + $orderCustomerRelationChecker->wasOrderPlacedByCustomer($order, $customer)->willReturn(true); + $reorder->getItems()->willReturn(new ArrayCollection([ $firstOrderItem->getWrappedObject() ])); @@ -261,6 +286,26 @@ function it_checks_if_any_item_is_out_of_stock( $entityManager->persist($reorder)->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); - $this->reorder($order, $channel); + $this->reorder($order, $channel, $customer); + } + + function it_does_not_create_reorder_when_order_does_not_belong_to_given_customer( + OrderInterface $order, + ChannelInterface $channel, + CustomerInterface $firstCustomer, + CustomerInterface $secondCustomer, + OrderCustomerRelationCheckerInterface $orderCustomerRelationChecker + ): void { + $firstCustomer->getId()->willReturn('1'); + $secondCustomer->getId()->willReturn('2'); + + $order->getCustomer()->willReturn($firstCustomer); + + $orderCustomerRelationChecker->wasOrderPlacedByCustomer($order, $secondCustomer)->shouldBeCalled(); + + $this + ->shouldThrow(InvalidStateException::class) + ->during('reorder', [$order, $channel, $secondCustomer]) + ; } } diff --git a/src/Checker/OrderCustomerRelationChecker.php b/src/Checker/OrderCustomerRelationChecker.php new file mode 100644 index 0000000..f8f9048 --- /dev/null +++ b/src/Checker/OrderCustomerRelationChecker.php @@ -0,0 +1,22 @@ +getCustomer(); + + return + null !== $orderCustomer && + $orderCustomer->getId() === $customer->getId() + ; + } +} diff --git a/src/Checker/OrderCustomerRelationCheckerInterface.php b/src/Checker/OrderCustomerRelationCheckerInterface.php new file mode 100644 index 0000000..d3d997e --- /dev/null +++ b/src/Checker/OrderCustomerRelationCheckerInterface.php @@ -0,0 +1,13 @@ +cartSessionStorage = $cartSessionStorage; $this->channelContext = $channelContext; $this->cartContext = $cartContext; + $this->customerContext = $customerContext; $this->orderRepository = $orderRepository; $this->reorderer = $reorderService; $this->urlGenerator = $urlGenerator; @@ -67,10 +74,13 @@ public function __invoke(Request $request): Response $channel = $this->channelContext->getChannel(); assert($channel instanceof ChannelInterface); + /** @var CustomerInterface $customer */ + $customer = $this->customerContext->getCustomer(); + $reorder = null; try { - $reorder = $this->reorderer->reorder($order, $channel); + $reorder = $this->reorderer->reorder($order, $channel, $customer); } catch (InvalidStateException $exception) { $this->session->getFlashBag()->add('info', $exception->getMessage()); diff --git a/src/Reorder/Reorderer.php b/src/Reorder/Reorderer.php index cd0bfab..21439bb 100644 --- a/src/Reorder/Reorderer.php +++ b/src/Reorder/Reorderer.php @@ -8,8 +8,10 @@ use Nette\InvalidStateException; use Sylius\Bundle\MoneyBundle\Formatter\MoneyFormatterInterface; use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Order\Processor\OrderProcessorInterface; +use Sylius\CustomerReorderPlugin\Checker\OrderCustomerRelationCheckerInterface; use Sylius\CustomerReorderPlugin\Factory\OrderFactoryInterface; use Sylius\CustomerReorderPlugin\ReorderEligibility\ReorderEligibilityChecker; use Sylius\CustomerReorderPlugin\ReorderEligibility\ResponseProcessing\ReorderEligibilityCheckerResponseProcessorInterface; @@ -38,6 +40,9 @@ final class Reorderer implements ReordererInterface /** @var ReorderEligibilityCheckerResponseProcessorInterface */ private $reorderEligibilityCheckerResponseProcessor; + /** @var OrderCustomerRelationCheckerInterface */ + private $orderCustomerRelationCheckerInterface; + public function __construct( OrderFactoryInterface $orderFactory, EntityManagerInterface $entityManager, @@ -45,7 +50,8 @@ public function __construct( MoneyFormatterInterface $moneyFormatter, Session $session, ReorderEligibilityChecker $reorderEligibilityChecker, - ReorderEligibilityCheckerResponseProcessorInterface $reorderEligibilityCheckerResponseProcessor + ReorderEligibilityCheckerResponseProcessorInterface $reorderEligibilityCheckerResponseProcessor, + OrderCustomerRelationCheckerInterface $orderCustomerRelationChecker ) { $this->orderFactory = $orderFactory; $this->entityManager = $entityManager; @@ -54,10 +60,18 @@ public function __construct( $this->session = $session; $this->reorderEligibilityChecker = $reorderEligibilityChecker; $this->reorderEligibilityCheckerResponseProcessor = $reorderEligibilityCheckerResponseProcessor; + $this->orderCustomerRelationCheckerInterface = $orderCustomerRelationChecker; } - public function reorder(OrderInterface $order, ChannelInterface $channel): OrderInterface - { + public function reorder( + OrderInterface $order, + ChannelInterface $channel, + CustomerInterface $customer + ): OrderInterface { + if (!$this->orderCustomerRelationCheckerInterface->wasOrderPlacedByCustomer($order, $customer)) { + throw new InvalidStateException("The customer is not the order's owner."); + } + $reorder = $this->orderFactory->createFromExistingOrder($order, $channel); assert($reorder instanceof OrderInterface); diff --git a/src/Reorder/ReordererInterface.php b/src/Reorder/ReordererInterface.php index 13fe0d0..d78c439 100644 --- a/src/Reorder/ReordererInterface.php +++ b/src/Reorder/ReordererInterface.php @@ -5,9 +5,14 @@ namespace Sylius\CustomerReorderPlugin\Reorder; use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; interface ReordererInterface { - public function reorder(OrderInterface $order, ChannelInterface $channel): OrderInterface; + public function reorder( + OrderInterface $order, + ChannelInterface $channel, + CustomerInterface $customer + ): OrderInterface; } diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 03b092b..83e2d1c 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -8,6 +8,7 @@ + @@ -21,6 +22,7 @@ + @@ -66,5 +68,6 @@ + diff --git a/tests/Behat/Context/Reorder/Application/ReorderContext.php b/tests/Behat/Context/Reorder/Application/ReorderContext.php new file mode 100644 index 0000000..75d9dde --- /dev/null +++ b/tests/Behat/Context/Reorder/Application/ReorderContext.php @@ -0,0 +1,63 @@ +orderRepository = $orderRepository; + $this->customerRepository = $customerRepository; + $this->reorderer = $reorderer; + } + + /** + * @When the customer :customerEmail tries to reorder the order :orderNumber + */ + public function theCustomerTriesToReorderTheOrder(string $customerEmail, string $orderNumber): void + { + /** @var OrderInterface $order */ + $order = $this->orderRepository->findOneByNumber($orderNumber); + + /** @var CustomerInterface $customer */ + $customer = $this->customerRepository->findOneBy(['email' => $customerEmail]); + + try { + $this->reorderer->reorder($order, $order->getChannel(), $customer); + } catch (InvalidStateException $exception) { + return; + } + + throw new \Exception("Reorder should fail"); + } + + /** + * @Then the order :orderNumber should not be reordered + */ + public function theOrderShouldNotBeReordered(string $orderNumber): void + { + // skipped intentionally - not relevant as the condition was checked in previous step + } +} diff --git a/tests/Behat/Context/Reorder/ReorderContext.php b/tests/Behat/Context/Reorder/Ui/ReorderContext.php similarity index 99% rename from tests/Behat/Context/Reorder/ReorderContext.php rename to tests/Behat/Context/Reorder/Ui/ReorderContext.php index 0fff470..22b2456 100644 --- a/tests/Behat/Context/Reorder/ReorderContext.php +++ b/tests/Behat/Context/Reorder/Ui/ReorderContext.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Sylius\CustomerReorderPlugin\Behat\Context\Reorder; +namespace Tests\Sylius\CustomerReorderPlugin\Behat\Context\Reorder\Ui; use Behat\Behat\Context\Context; use Behat\Mink\Session; diff --git a/tests/Behat/Resources/services.xml b/tests/Behat/Resources/services.xml index 173f55c..a87d110 100644 --- a/tests/Behat/Resources/services.xml +++ b/tests/Behat/Resources/services.xml @@ -2,7 +2,7 @@ - + @@ -14,6 +14,14 @@ + + + + + + + + diff --git a/tests/Behat/Resources/suites.yml b/tests/Behat/Resources/suites.yml index 845776a..5a078f8 100644 --- a/tests/Behat/Resources/suites.yml +++ b/tests/Behat/Resources/suites.yml @@ -1,8 +1,8 @@ default: suites: - reorders: + reorders_ui: contexts_services: - - sylius_customer_reorder.behat.context.setup.reorder + - sylius_customer_reorder.behat.context.ui.reorder - sylius.behat.context.setup.channel - sylius.behat.context.setup.product @@ -29,3 +29,34 @@ default: - sylius.behat.context.ui.shop.checkout.addressing filters: tags: "@reordering && @ui" + reorders_application: + contexts_services: + - sylius_customer_reorder.behat.context.application.reorder + + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.customer + - sylius.behat.context.setup.product + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.order + - sylius.behat.context.setup.shop_security + - sylius.behat.context.setup.promotion + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.promotion + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.user + + - sylius.behat.context.hook.doctrine_orm + - sylius.behat.context.ui.shop.checkout.complete + - sylius.behat.context.ui.shop.account + - sylius.behat.context.ui.shop.cart + - sylius.behat.context.ui.shop.checkout.addressing + filters: + tags: "@reordering && @application"