diff --git a/doc/swagger.yml b/doc/swagger.yml
index c41e14c62..676e5346f 100644
--- a/doc/swagger.yml
+++ b/doc/swagger.yml
@@ -543,6 +543,8 @@ paths:
description: "Invalid input, validation failed."
schema:
$ref: "#/definitions/GeneralError"
+ 403:
+ description: "Not logged in or wrong email"
/taxon-products-by-slug/{slug}:
get:
tags:
diff --git a/spec/Handler/CompleteOrderHandlerSpec.php b/spec/Handler/CompleteOrderHandlerSpec.php
index 844208517..45edd2570 100644
--- a/spec/Handler/CompleteOrderHandlerSpec.php
+++ b/spec/Handler/CompleteOrderHandlerSpec.php
@@ -5,32 +5,48 @@
namespace spec\Sylius\ShopApiPlugin\Handler;
use PhpSpec\ObjectBehavior;
+use Prophecy\Argument;
use SM\Factory\FactoryInterface as StateMachineFactoryInterface;
use SM\StateMachine\StateMachineInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
+use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\Core\OrderCheckoutTransitions;
+use Sylius\Component\Core\Repository\CustomerRepositoryInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
+use Sylius\Component\Resource\Factory\FactoryInterface;
use Sylius\ShopApiPlugin\Command\CompleteOrder;
-use Sylius\ShopApiPlugin\Provider\CustomerProviderInterface;
+use Sylius\ShopApiPlugin\Exception\WrongUserException;
+use Sylius\ShopApiPlugin\Provider\LoggedInUserProviderInterface;
+use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
final class CompleteOrderHandlerSpec extends ObjectBehavior
{
- function let(OrderRepositoryInterface $orderRepository, CustomerProviderInterface $customerProvider, StateMachineFactoryInterface $stateMachineFactory): void
- {
- $this->beConstructedWith($orderRepository, $customerProvider, $stateMachineFactory);
+ function let(
+ OrderRepositoryInterface $orderRepository,
+ CustomerRepositoryInterface $customerRepository,
+ FactoryInterface $customerFactory,
+ LoggedInUserProviderInterface $loggedInUserProvider,
+ StateMachineFactoryInterface $stateMachineFactory
+ ): void {
+ $this->beConstructedWith($orderRepository, $customerRepository, $customerFactory, $loggedInUserProvider, $stateMachineFactory);
}
- function it_handles_order_completion_for_existing_customer(
+ function it_handles_order_completion_for_guest_checkout(
CustomerInterface $customer,
- CustomerProviderInterface $customerProvider,
+ CustomerRepositoryInterface $customerRepository,
+ LoggedInUserProviderInterface $loggedInUserProvider,
+ FactoryInterface $customerFactory,
OrderInterface $order,
OrderRepositoryInterface $orderRepository,
StateMachineFactoryInterface $stateMachineFactory,
StateMachineInterface $stateMachine
): void {
$orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
- $customerProvider->provide('example@customer.com')->willReturn($customer);
+
+ $customerRepository->findOneBy(['email' => 'example@customer.com'])->willReturn(null);
+ $customerFactory->createNew()->willReturn($customer);
+ $loggedInUserProvider->provide()->willThrow(TokenNotFoundException::class);
$stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH)->willReturn($stateMachine);
$stateMachine->can('complete')->willReturn(true);
@@ -42,16 +58,74 @@ function it_handles_order_completion_for_existing_customer(
$this->handle(new CompleteOrder('ORDERTOKEN', 'example@customer.com'));
}
+ function it_throws_an_exception_if_the_email_address_has_already_a_customer(
+ CustomerInterface $customer,
+ CustomerRepositoryInterface $customerRepository,
+ LoggedInUserProviderInterface $loggedInUserProvider,
+ ShopUserInterface $shopUser,
+ OrderInterface $order,
+ OrderRepositoryInterface $orderRepository,
+ StateMachineFactoryInterface $stateMachineFactory,
+ StateMachineInterface $stateMachine
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+
+ $customerRepository->findOneBy(['email' => 'example@customer.com'])->willReturn($customer);
+ $shopUser->getCustomer()->willReturn($customer);
+ $loggedInUserProvider->provide()->willThrow(TokenNotFoundException::class);
+
+ $stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH)->willReturn($stateMachine);
+ $stateMachine->can('complete')->willReturn(true);
+
+ $order->setNotes(Argument::any())->shouldNotBeCalled();
+ $order->setCustomer(Argument::any())->shouldNotBeCalled();
+ $stateMachine->apply(Argument::any())->shouldNotBeCalled();
+
+ $this->shouldThrow(WrongUserException::class)
+ ->during('handle', [new CompleteOrder('ORDERTOKEN', 'example@customer.com')]);
+ }
+
+ function it_handles_order_completetion(
+ CustomerRepositoryInterface $customerRepository,
+ LoggedInUserProviderInterface $loggedInUserProvider,
+ CustomerInterface $loggedInCustomer,
+ ShopUserInterface $shopUser,
+ OrderInterface $order,
+ OrderRepositoryInterface $orderRepository,
+ StateMachineFactoryInterface $stateMachineFactory,
+ StateMachineInterface $stateMachine
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+
+ $customerRepository->findOneBy(Argument::any())->shouldNotBeCalled();
+ $shopUser->getCustomer()->willReturn($loggedInCustomer);
+ $loggedInUserProvider->provide()->willReturn($shopUser);
+
+ $stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH)->willReturn($stateMachine);
+ $stateMachine->can('complete')->willReturn(true);
+
+ $order->setCustomer($loggedInCustomer)->shouldBeCalled();
+ $order->setNotes(null)->shouldBeCalled();
+ $stateMachine->apply('complete')->shouldBeCalled();
+
+ $this->handle(new CompleteOrder('ORDERTOKEN', ''));
+ }
+
function it_handles_order_completion_with_notes(
CustomerInterface $customer,
- CustomerProviderInterface $customerProvider,
+ CustomerRepositoryInterface $customerRepository,
+ LoggedInUserProviderInterface $loggedInUserProvider,
+ ShopUserInterface $shopUser,
OrderInterface $order,
OrderRepositoryInterface $orderRepository,
StateMachineFactoryInterface $stateMachineFactory,
StateMachineInterface $stateMachine
): void {
$orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
- $customerProvider->provide('example@customer.com')->willReturn($customer);
+
+ $customerRepository->findOneBy(['email' => 'example@customer.com'])->willReturn($customer);
+ $shopUser->getCustomer()->willReturn($customer);
+ $loggedInUserProvider->provide()->willReturn($shopUser);
$stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH)->willReturn($stateMachine);
$stateMachine->can('complete')->willReturn(true);
@@ -60,14 +134,46 @@ function it_handles_order_completion_with_notes(
$order->setCustomer($customer)->shouldBeCalled();
$stateMachine->apply('complete')->shouldBeCalled();
- $this->handle(new CompleteOrder('ORDERTOKEN', 'example@customer.com', 'Some notes'));
+ $this->handle(new CompleteOrder('ORDERTOKEN', '', 'Some notes'));
}
- function it_throws_an_exception_if_order_does_not_exist(OrderRepositoryInterface $orderRepository): void
- {
+ function it_throws_an_exception_if_order_does_not_exist(
+ OrderRepositoryInterface $orderRepository
+ ): void {
$orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn(null);
- $this->shouldThrow(\InvalidArgumentException::class)->during('handle', [new CompleteOrder('ORDERTOKEN', 'example@customer.com')]);
+ $this->shouldThrow(\InvalidArgumentException::class)
+ ->during('handle', [new CompleteOrder('ORDERTOKEN', 'example@customer.com')])
+ ;
+ }
+
+ function it_throws_an_exception_if_the_user_is_logged_in_and_provides_email(
+ CustomerInterface $customer,
+ CustomerRepositoryInterface $customerRepository,
+ LoggedInUserProviderInterface $loggedInUserProvider,
+ CustomerInterface $loggedInCustomer,
+ ShopUserInterface $shopUser,
+ OrderInterface $order,
+ OrderRepositoryInterface $orderRepository,
+ StateMachineFactoryInterface $stateMachineFactory,
+ StateMachineInterface $stateMachine
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+
+ $customerRepository->findOneBy(Argument::any())->shouldNotBeCalled();
+ $shopUser->getCustomer()->willReturn($loggedInCustomer);
+ $loggedInUserProvider->provide()->willReturn($shopUser);
+
+ $stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH)->willReturn($stateMachine);
+ $stateMachine->can('complete')->willReturn(true);
+
+ $order->setCustomer($customer)->shouldNotBeCalled();
+ $order->setNotes('Some notes');
+ $stateMachine->apply('complete')->shouldNotBeCalled();
+
+ $this->shouldThrow(\InvalidArgumentException::class)
+ ->during('handle', [new CompleteOrder('ORDERTOKEN', 'example@customer.com', 'Some notes')])
+ ;
}
function it_throws_an_exception_if_order_cannot_be_addressed(
diff --git a/src/Controller/Checkout/CompleteOrderAction.php b/src/Controller/Checkout/CompleteOrderAction.php
index 70dcf89d9..a427f7e66 100644
--- a/src/Controller/Checkout/CompleteOrderAction.php
+++ b/src/Controller/Checkout/CompleteOrderAction.php
@@ -7,11 +7,12 @@
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use League\Tactician\CommandBus;
-use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\ShopApiPlugin\Command\CompleteOrder;
+use Sylius\ShopApiPlugin\Exception\WrongUserException;
+use Sylius\ShopApiPlugin\Provider\LoggedInUserProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
+use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
final class CompleteOrderAction
{
@@ -21,37 +22,42 @@ final class CompleteOrderAction
/** @var CommandBus */
private $bus;
- /** @var TokenStorageInterface */
- private $tokenStorage;
+ /** @var LoggedInUserProviderInterface */
+ private $loggedInUserProvider;
- public function __construct(ViewHandlerInterface $viewHandler, CommandBus $bus, TokenStorageInterface $tokenStorage)
- {
+ public function __construct(
+ ViewHandlerInterface $viewHandler,
+ CommandBus $bus,
+ LoggedInUserProviderInterface $loggedInUserProvider
+ ) {
$this->viewHandler = $viewHandler;
$this->bus = $bus;
- $this->tokenStorage = $tokenStorage;
+ $this->loggedInUserProvider = $loggedInUserProvider;
}
public function __invoke(Request $request): Response
{
- $email = $this->provideUserEmail($request);
-
- $this->bus->handle(new CompleteOrder(
- $request->attributes->get('token'),
- $email,
- $request->request->get('notes')
- ));
-
- return $this->viewHandler->handle(View::create(null, Response::HTTP_NO_CONTENT));
- }
-
- private function provideUserEmail(Request $request): string
- {
- $user = $this->tokenStorage->getToken()->getUser();
-
- if ($user instanceof ShopUserInterface) {
- return $user->getCustomer()->getEmail();
+ try {
+ $this->bus->handle(
+ new CompleteOrder(
+ $request->attributes->get('token'),
+ $request->request->get('email', ''),
+ $request->request->get('notes')
+ )
+ );
+ } catch (WrongUserException $notLoggedInException) {
+ return $this->viewHandler->handle(
+ View::create(
+ 'You need to be logged in with the same user that wants to complete the order',
+ Response::HTTP_UNAUTHORIZED
+ )
+ );
+ } catch (TokenNotFoundException $notLoggedInException) {
+ return $this->viewHandler->handle(
+ View::create('You need to be logged in', Response::HTTP_UNAUTHORIZED)
+ );
}
- return $request->request->get('email');
+ return $this->viewHandler->handle(View::create(null, Response::HTTP_NO_CONTENT));
}
}
diff --git a/src/Exception/WrongUserException.php b/src/Exception/WrongUserException.php
new file mode 100644
index 000000000..6522443ed
--- /dev/null
+++ b/src/Exception/WrongUserException.php
@@ -0,0 +1,11 @@
+orderRepository = $orderRepository;
- $this->customerProvider = $customerProvider;
+ $this->customerRepository = $customerRepository;
$this->stateMachineFactory = $stateMachineFactory;
+ $this->customerFactory = $customerFactory;
+ $this->loggedInUserProvider = $loggedInUserProvider;
}
public function handle(CompleteOrder $completeOrder)
@@ -44,11 +59,42 @@ public function handle(CompleteOrder $completeOrder)
Assert::true($stateMachine->can(OrderCheckoutTransitions::TRANSITION_COMPLETE), sprintf('Order with %s token cannot be completed.', $completeOrder->orderToken()));
- $customer = $this->customerProvider->provide($completeOrder->email());
-
+ $customer = $this->getCustomer($completeOrder->email());
$order->setNotes($completeOrder->notes());
$order->setCustomer($customer);
$stateMachine->apply(OrderCheckoutTransitions::TRANSITION_COMPLETE);
}
+
+ private function getCustomer(string $emailAddress): CustomerInterface
+ {
+ try {
+ $loggedInUser = $this->loggedInUserProvider->provide();
+
+ if ($emailAddress !== '') {
+ throw new \InvalidArgumentException($emailAddress . ' has to be empty');
+
+ throw new \InvalidArgumentException('Can not have a logged in user and an email address');
+ }
+
+ /** @var CustomerInterface $customer */
+ $customer = $loggedInUser->getCustomer();
+
+ return $customer;
+ } catch (TokenNotFoundException $notLoggedIn) {
+ /** @var CustomerInterface|null $customer */
+ $customer = $this->customerRepository->findOneBy(['email' => $emailAddress]);
+
+ // If the customer does not exist then it's normal checkout
+ if ($customer === null) {
+ /** @var CustomerInterface $customer */
+ $customer = $this->customerFactory->createNew();
+ $customer->setEmail($emailAddress);
+
+ return $customer;
+ }
+
+ throw new WrongUserException('Email is already taken');
+ }
+ }
}
diff --git a/src/Provider/LoggedInUserProvider.php b/src/Provider/LoggedInUserProvider.php
index 1493271ea..6547879f2 100644
--- a/src/Provider/LoggedInUserProvider.php
+++ b/src/Provider/LoggedInUserProvider.php
@@ -20,11 +20,16 @@ public function __construct(TokenStorageInterface $tokenStorage)
public function provide(): ShopUserInterface
{
- /** @var ShopUserInterface $user */
- $user = $this->tokenStorage->getToken()->getUser();
+ $token = $this->tokenStorage->getToken();
+ if ($token === null) {
+ throw new TokenNotFoundException('No token found');
+ }
+
+ /** @var ShopUserInterface|null $user */
+ $user = $token->getUser();
if (!$user instanceof ShopUserInterface) {
- throw new TokenNotFoundException();
+ throw new TokenNotFoundException('No logged in user');
}
return $user;
diff --git a/src/Resources/config/services/actions/checkout.xml b/src/Resources/config/services/actions/checkout.xml
index b94d9bfc7..e1d31ef66 100644
--- a/src/Resources/config/services/actions/checkout.xml
+++ b/src/Resources/config/services/actions/checkout.xml
@@ -43,7 +43,7 @@
>
-
+
diff --git a/src/Resources/config/services/handler/cart.xml b/src/Resources/config/services/handler/cart.xml
index f98d12942..c863a0190 100644
--- a/src/Resources/config/services/handler/cart.xml
+++ b/src/Resources/config/services/handler/cart.xml
@@ -101,7 +101,9 @@
-
+
+
+
diff --git a/tests/Controller/CheckoutCompleteOrderApiTest.php b/tests/Controller/CheckoutCompleteOrderApiTest.php
index 49e8ca990..1687d6145 100644
--- a/tests/Controller/CheckoutCompleteOrderApiTest.php
+++ b/tests/Controller/CheckoutCompleteOrderApiTest.php
@@ -12,9 +12,12 @@
use Sylius\ShopApiPlugin\Command\PutSimpleItemToCart;
use Sylius\ShopApiPlugin\Model\Address;
use Symfony\Component\HttpFoundation\Response;
+use Tests\Sylius\ShopApiPlugin\Controller\Utils\ShopUserLoginTrait;
final class CheckoutCompleteOrderApiTest extends JsonApiTestCase
{
+ use ShopUserLoginTrait;
+
/**
* @test
*/
@@ -154,19 +157,8 @@ public function it_allows_to_complete_checkout_without_email_for_logged_in_custo
$bus->handle(new ChooseShippingMethod($token, 0, 'DHL'));
$bus->handle(new ChoosePaymentMethod($token, 0, 'PBC'));
- $data =
-<<client->request('POST', '/shop-api/login_check', [], [], ['CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json'], $data);
-
- $response = json_decode($this->client->getResponse()->getContent(), true);
+ $this->logInUser('oliver@queen.com', '123password');
- $this->client->setServerParameter('HTTP_Authorization', sprintf('Bearer %s', $response['token']));
$this->client->request('PUT', sprintf('/shop-api/WEB_GB/checkout/%s/complete', $token), [], [], [
'CONTENT_TYPE' => 'application/json',
'ACCEPT' => 'application/json',
@@ -181,7 +173,7 @@ public function it_allows_to_complete_checkout_without_email_for_logged_in_custo
*/
public function it_does_not_allow_to_complete_order_in_non_existent_channel()
{
- $this->loadFixturesFromFiles(['shop.yml', 'country.yml', 'shipping.yml', 'payment.yml']);
+ $this->loadFixturesFromFiles(['shop.yml', 'country.yml', 'shipping.yml', 'payment.yml', 'customer.yml']);
$token = 'SDAOSLEFNWU35H3QLI5325';
@@ -226,4 +218,54 @@ public function it_does_not_allow_to_complete_order_in_non_existent_channel()
$response = $this->client->getResponse();
$this->assertResponse($response, 'channel_has_not_been_found_response', Response::HTTP_NOT_FOUND);
}
+
+ /**
+ * @test
+ */
+ public function it_disallows_users_to_complete_checkout_for_someone_else()
+ {
+ $this->loadFixturesFromFiles(['shop.yml', 'country.yml', 'shipping.yml', 'payment.yml', 'customer.yml']);
+
+ $token = 'SDAOSLEFNWU35H3QLI5325';
+
+ /** @var CommandBus $bus */
+ $bus = $this->get('tactician.commandbus');
+ $bus->handle(new PickupCart($token, 'WEB_GB'));
+ $bus->handle(new PutSimpleItemToCart($token, 'LOGAN_MUG_CODE', 5));
+ $bus->handle(new AddressOrder(
+ $token,
+ Address::createFromArray([
+ 'firstName' => 'Sherlock',
+ 'lastName' => 'Holmes',
+ 'city' => 'London',
+ 'street' => 'Baker Street 221b',
+ 'countryCode' => 'GB',
+ 'postcode' => 'NWB',
+ 'provinceName' => 'Greater London',
+ ]), Address::createFromArray([
+ 'firstName' => 'Sherlock',
+ 'lastName' => 'Holmes',
+ 'city' => 'London',
+ 'street' => 'Baker Street 221b',
+ 'countryCode' => 'GB',
+ 'postcode' => 'NWB',
+ 'provinceName' => 'Greater London',
+ ])
+ ));
+ $bus->handle(new ChooseShippingMethod($token, 0, 'DHL'));
+ $bus->handle(new ChoosePaymentMethod($token, 0, 'PBC'));
+
+ $data = <<client->request('PUT', sprintf('/shop-api/WEB_GB/checkout/%s/complete', $token), [], [], [
+ 'CONTENT_TYPE' => 'application/json',
+ 'ACCEPT' => 'application/json',
+ ], $data);
+ $response = $this->client->getResponse();
+ $this->assertResponseCode($response, Response::HTTP_UNAUTHORIZED);
+ }
}