Fix being unable to pick up latest non-empty cart started in shop and continued in API

Co-authored-by: Maxime Veber <nek.dev@gmail.com>
This commit is contained in:
Jakub Tobiasz 2023-02-08 09:59:16 +01:00
parent 854e3e7ceb
commit fb70c5e34a
No known key found for this signature in database
GPG key ID: 6434250CB3525233
7 changed files with 262 additions and 101 deletions

View file

@ -15,6 +15,7 @@ namespace Sylius\Bundle\ApiBundle\CommandHandler\Cart;
use Doctrine\Persistence\ObjectManager;
use Sylius\Bundle\ApiBundle\Command\Cart\PickupCart;
use Sylius\Bundle\CoreBundle\Factory\OrderFactoryInterface;
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
use Sylius\Component\Core\Model\AddressInterface;
use Sylius\Component\Core\Model\ChannelInterface;
@ -27,12 +28,13 @@ use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Sylius\Component\Resource\Generator\RandomnessGeneratorInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Webmozart\Assert\Assert;
/** @experimental */
final class PickupCartHandler implements MessageHandlerInterface
{
public function __construct(
private FactoryInterface $cartFactory,
private OrderFactoryInterface $cartFactory,
private OrderRepositoryInterface $cartRepository,
private ChannelRepositoryInterface $channelRepository,
private ObjectManager $orderManager,
@ -41,42 +43,44 @@ final class PickupCartHandler implements MessageHandlerInterface
) {
}
public function __invoke(PickupCart $pickupCart)
public function __invoke(PickupCart $pickupCart): OrderInterface
{
/** @var ChannelInterface $channel */
$channel = $this->channelRepository->findOneByCode($pickupCart->getChannelCode());
Assert::notNull($channel);
$customer = null;
if ($pickupCart->getEmail() !== null) {
/** @var CustomerInterface|null $customer */
$customer = $this->customerRepository->findOneBy(['email' => $pickupCart->getEmail()]);
}
if ($customer !== null) {
/** @var OrderInterface|null $cart */
$cart = $this->cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, $customer);
if ($cart !== null) {
return $cart;
}
if (null === $customer) {
return $this->createCart($channel, null, $pickupCart);
}
/** @var OrderInterface $cart */
$cart = $this->cartFactory->createNew();
$activeCart = $this->cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, $customer);
/** @var CurrencyInterface $currency */
$currency = $channel->getBaseCurrency();
$cart->setChannel($channel);
$cart->setLocaleCode($this->getLocaleCode($pickupCart->localeCode, $channel));
$cart->setCurrencyCode($currency->getCode());
$cart->setTokenValue($pickupCart->tokenValue ?? $this->generator->generateUriSafeString(10));
if ($customer !== null) {
$cart->setCustomerWithAuthorization($customer);
$cart->setBillingAddress($this->getDefaultAddress($customer));
if (null === $activeCart) {
return $this->createCart($channel, $customer, $pickupCart);
}
if (null === $activeCart->getTokenValue()) {
$activeCart->setTokenValue($pickupCart->tokenValue ?? $this->generator->generateUriSafeString(10));
$this->orderManager->persist($activeCart);
}
return $activeCart;
}
private function createCart(ChannelInterface $channel, ?CustomerInterface $customer, PickupCart $pickupCart): OrderInterface
{
$cart = $this->cartFactory->createNewCart(
$channel,
$customer,
$this->getLocaleCode($pickupCart->getLocaleCode(), $channel),
$pickupCart->tokenValue ?? $this->generator->generateUriSafeString(10)
);
$this->orderManager->persist($cart);
return $cart;
@ -112,16 +116,4 @@ final class PickupCartHandler implements MessageHandlerInterface
return $localeCode;
}
private function getDefaultAddress(CustomerInterface $customer): ?AddressInterface
{
$defaultAddress = $customer->getDefaultAddress();
if (null !== $defaultAddress) {
$clonedAddress = clone $defaultAddress;
$clonedAddress->setCustomer(null);
return $clonedAddress;
}
return null;
}
}

View file

@ -18,23 +18,21 @@ use Doctrine\Persistence\ObjectManager;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\ApiBundle\Command\Cart\PickupCart;
use Sylius\Bundle\CoreBundle\Factory\OrderFactoryInterface;
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
use Sylius\Component\Core\Model\AddressInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\CustomerRepositoryInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Currency\Model\CurrencyInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Sylius\Component\Resource\Generator\RandomnessGeneratorInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
final class PickupCartHandlerSpec extends ObjectBehavior
{
function let(
FactoryInterface $cartFactory,
OrderFactoryInterface $cartFactory,
OrderRepositoryInterface $cartRepository,
ChannelRepositoryInterface $channelRepository,
ObjectManager $orderManager,
@ -57,7 +55,7 @@ final class PickupCartHandlerSpec extends ObjectBehavior
}
function it_picks_up_a_new_cart_for_logged_in_shop_user(
FactoryInterface $cartFactory,
OrderFactoryInterface $cartFactory,
OrderRepositoryInterface $cartRepository,
ChannelRepositoryInterface $channelRepository,
CustomerInterface $customer,
@ -66,44 +64,32 @@ final class PickupCartHandlerSpec extends ObjectBehavior
CustomerRepositoryInterface $customerRepository,
OrderInterface $cart,
ChannelInterface $channel,
CurrencyInterface $currency,
LocaleInterface $locale,
AddressInterface $address,
): void {
$pickupCart = new PickupCart();
$pickupCart->setChannelCode('code');
$pickupCart->setEmail('sample@email.com');
$channelRepository->findOneByCode('code')->willReturn($channel);
$channel->getBaseCurrency()->willReturn($currency);
$channel->getDefaultLocale()->willReturn($locale);
$customerRepository->findOneBy(['email' => 'sample@email.com'])->willReturn($customer);
$customer->getDefaultAddress()->willReturn($address);
$cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, $customer)->willReturn(null);
$generator->generateUriSafeString(10)->willReturn('urisafestr');
$currency->getCode()->willReturn('USD');
$locale->getCode()->willReturn('en_US');
$channel->getLocales()->willReturn(new ArrayCollection([$locale->getWrappedObject()]));
$cartFactory->createNew()->willReturn($cart);
$cart->setCustomerWithAuthorization($customer)->shouldBeCalled();
$cart->setBillingAddress($address)->shouldBeCalled();
$cart->setChannel($channel)->shouldBeCalled();
$cart->setCurrencyCode('USD')->shouldBeCalled();
$cart->setLocaleCode('en_US')->shouldBeCalled();
$cart->setTokenValue('urisafestr')->shouldBeCalled();
$cartFactory->createNewCart($channel, $customer, 'en_US', 'urisafestr')->willReturn($cart);
$orderManager->persist($cart)->shouldBeCalled();
$this($pickupCart);
}
function it_picks_up_a_new_cart_for_logged_in_shop_user_when_the_user_has_no_default_address(
FactoryInterface $cartFactory,
OrderFactoryInterface $cartFactory,
OrderRepositoryInterface $cartRepository,
ChannelRepositoryInterface $channelRepository,
CustomerInterface $customer,
@ -112,7 +98,6 @@ final class PickupCartHandlerSpec extends ObjectBehavior
CustomerRepositoryInterface $customerRepository,
OrderInterface $cart,
ChannelInterface $channel,
CurrencyInterface $currency,
LocaleInterface $locale,
): void {
$pickupCart = new PickupCart();
@ -120,35 +105,24 @@ final class PickupCartHandlerSpec extends ObjectBehavior
$pickupCart->setEmail('sample@email.com');
$channelRepository->findOneByCode('code')->willReturn($channel);
$channel->getBaseCurrency()->willReturn($currency);
$channel->getDefaultLocale()->willReturn($locale);
$customerRepository->findOneBy(['email' => 'sample@email.com'])->willReturn($customer);
$customer->getDefaultAddress()->willReturn(null);
$cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, $customer)->willReturn(null);
$generator->generateUriSafeString(10)->willReturn('urisafestr');
$currency->getCode()->willReturn('USD');
$locale->getCode()->willReturn('en_US');
$channel->getLocales()->willReturn(new ArrayCollection([$locale->getWrappedObject()]));
$cartFactory->createNew()->willReturn($cart);
$cart->setCustomerWithAuthorization($customer)->shouldBeCalled();
$cart->setBillingAddress(null)->shouldBeCalled();
$cart->setChannel($channel)->shouldBeCalled();
$cart->setCurrencyCode('USD')->shouldBeCalled();
$cart->setLocaleCode('en_US')->shouldBeCalled();
$cart->setTokenValue('urisafestr')->shouldBeCalled();
$cartFactory->createNewCart($channel, $customer, 'en_US', 'urisafestr')->willReturn($cart);
$orderManager->persist($cart)->shouldBeCalled();
$this($pickupCart);
}
function it_picks_up_an_existing_cart_for_logged_in_shop_user(
FactoryInterface $cartFactory,
function it_picks_up_an_existing_cart_with_token_for_logged_in_shop_user(
OrderRepositoryInterface $cartRepository,
ChannelRepositoryInterface $channelRepository,
CustomerRepositoryInterface $customerRepository,
@ -166,63 +140,76 @@ final class PickupCartHandlerSpec extends ObjectBehavior
$customerRepository->findOneBy(['email' => 'sample@email.com'])->willReturn($customer);
$cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, $customer)->willReturn($cart);
$cart->getTokenValue()->willReturn('token');
$cartFactory->createNew()->willReturn($cart);
$cart->setCustomerWithAuthorization($customer)->shouldNotBeCalled();
$cart->setChannel($channel)->shouldNotBeCalled();
$orderManager->persist(Argument::any())->shouldNotBeCalled();
$orderManager->persist($cart)->shouldNotBeCalled();
$this($pickupCart);
}
function it_picks_up_an_existing_cart_without_token_for_logged_in_shop_user(
OrderRepositoryInterface $cartRepository,
ChannelRepositoryInterface $channelRepository,
CustomerRepositoryInterface $customerRepository,
CustomerInterface $customer,
ObjectManager $orderManager,
OrderInterface $cart,
ChannelInterface $channel,
RandomnessGeneratorInterface $generator,
): void {
$pickupCart = new PickupCart();
$pickupCart->setChannelCode('code');
$pickupCart->setEmail('sample@email.com');
$channelRepository->findOneByCode('code')->willReturn($channel);
$customerRepository->findOneBy(['email' => 'sample@email.com'])->willReturn($customer);
$generator->generateUriSafeString(10)->willReturn('urisafestr');
$cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, $customer)->willReturn($cart);
$orderManager->persist($cart);
$this($pickupCart);
}
function it_picks_up_a_cart_for_visitor(
FactoryInterface $cartFactory,
OrderFactoryInterface $cartFactory,
OrderRepositoryInterface $cartRepository,
ChannelRepositoryInterface $channelRepository,
ObjectManager $orderManager,
RandomnessGeneratorInterface $generator,
OrderInterface $cart,
ChannelInterface $channel,
CurrencyInterface $currency,
LocaleInterface $locale,
): void {
$pickupCart = new PickupCart();
$pickupCart->setChannelCode('code');
$channelRepository->findOneByCode('code')->willReturn($channel);
$channel->getBaseCurrency()->willReturn($currency);
$channel->getDefaultLocale()->willReturn($locale);
$cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, Argument::any())->shouldNotBeCalled(null);
$generator->generateUriSafeString(10)->willReturn('urisafestr');
$currency->getCode()->willReturn('USD');
$locale->getCode()->willReturn('en_US');
$channel->getLocales()->willReturn(new ArrayCollection([$locale->getWrappedObject()]));
$cartFactory->createNew()->willReturn($cart);
$cart->setCustomerWithAuthorization(Argument::any())->shouldNotBeCalled();
$cart->setChannel($channel)->shouldBeCalled();
$cart->setCurrencyCode('USD')->shouldBeCalled();
$cart->setLocaleCode('en_US')->shouldBeCalled();
$cart->setTokenValue('urisafestr')->shouldBeCalled();
$cartFactory->createNewCart($channel, null, 'en_US', 'urisafestr')->willReturn($cart);
$orderManager->persist($cart)->shouldBeCalled();
$this($pickupCart);
}
function it_picks_up_a_cart_with_locale_code_for_visitor(
FactoryInterface $cartFactory,
OrderFactoryInterface $cartFactory,
OrderRepositoryInterface $cartRepository,
ChannelRepositoryInterface $channelRepository,
ObjectManager $orderManager,
RandomnessGeneratorInterface $generator,
OrderInterface $cart,
ChannelInterface $channel,
CurrencyInterface $currency,
LocaleInterface $locale,
): void {
$pickupCart = new PickupCart();
@ -230,7 +217,6 @@ final class PickupCartHandlerSpec extends ObjectBehavior
$pickupCart->localeCode = 'en_US';
$channelRepository->findOneByCode('code')->willReturn($channel);
$channel->getBaseCurrency()->willReturn($currency);
$channel->getDefaultLocale()->willReturn($locale);
$locale->getCode()->willReturn('en_US');
$channel->getLocales()->willReturn(new ArrayCollection([$locale->getWrappedObject()]));
@ -238,29 +224,21 @@ final class PickupCartHandlerSpec extends ObjectBehavior
$cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, Argument::any())->shouldNotBeCalled(null);
$generator->generateUriSafeString(10)->willReturn('urisafestr');
$currency->getCode()->willReturn('USD');
$locale->getCode()->willReturn('en_US');
$cartFactory->createNew()->willReturn($cart);
$cart->setCustomerWithAuthorization(Argument::any())->shouldNotBeCalled();
$cart->setChannel($channel)->shouldBeCalled();
$cart->setCurrencyCode('USD')->shouldBeCalled();
$cart->setLocaleCode('en_US')->shouldBeCalled();
$cart->setTokenValue('urisafestr')->shouldBeCalled();
$cartFactory->createNewCart($channel, null, 'en_US', 'urisafestr')->willReturn($cart);
$orderManager->persist($cart)->shouldBeCalled();
$this($pickupCart);
}
function it_throws_exception_if_locale_code_is_not_correct(
FactoryInterface $cartFactory,
OrderFactoryInterface $cartFactory,
OrderRepositoryInterface $cartRepository,
ChannelRepositoryInterface $channelRepository,
RandomnessGeneratorInterface $generator,
OrderInterface $cart,
ChannelInterface $channel,
CurrencyInterface $currency,
LocaleInterface $locale,
): void {
$pickupCart = new PickupCart();
@ -268,7 +246,6 @@ final class PickupCartHandlerSpec extends ObjectBehavior
$pickupCart->localeCode = 'ru_RU';
$channelRepository->findOneByCode('code')->willReturn($channel);
$channel->getBaseCurrency()->willReturn($currency);
$channel->getDefaultLocale()->willReturn($locale);
$locale->getCode()->willReturn('en_US');
$locales = new ArrayCollection([]);
@ -277,11 +254,8 @@ final class PickupCartHandlerSpec extends ObjectBehavior
$cartRepository->findLatestNotEmptyCartByChannelAndCustomer($channel, Argument::any())->shouldNotBeCalled(null);
$generator->generateUriSafeString(10)->willReturn('urisafestr');
$currency->getCode()->willReturn('USD');
$cartFactory->createNew()->willReturn($cart);
$cart->setCustomerWithAuthorization(Argument::any())->shouldNotBeCalled();
$cart->setChannel($channel)->shouldBeCalled();
$cartFactory->createNewCart($channel, null, 'en_US', 'urisafestr')->willReturn($cart);
$this
->shouldThrow(\InvalidArgumentException::class)

View file

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Factory;
use Sylius\Component\Core\Model\AddressInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
final class OrderFactory implements OrderFactoryInterface
{
public function __construct (
private FactoryInterface $decoratedFactory,
) {
}
public function createNew(): OrderInterface
{
return $this->decoratedFactory->createNew();
}
public function createNewCart(
ChannelInterface $channel,
?CustomerInterface $customer,
string $localeCode,
?string $tokenValue = null,
): OrderInterface {
$cart = $this->createNew();
$cart->setState(OrderInterface::STATE_CART);
$cart->setChannel($channel);
$cart->setLocaleCode($localeCode);
$cart->setCurrencyCode($channel->getBaseCurrency()->getCode());
if (null !== $tokenValue) {
$cart->setTokenValue($tokenValue);
}
if (null !== $customer) {
$cart->setCustomerWithAuthorization($customer);
$cart->setBillingAddress($this->getDefaultAddress($customer));
}
return $cart;
}
private function getDefaultAddress(CustomerInterface $customer): ?AddressInterface
{
$defaultAddress = $customer->getDefaultAddress();
if (null !== $defaultAddress) {
$clonedAddress = clone $defaultAddress;
$clonedAddress->setCustomer(null);
return $clonedAddress;
}
return null;
}
}

View file

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Factory;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
interface OrderFactoryInterface extends FactoryInterface
{
public function createNewCart(
ChannelInterface $channel,
?CustomerInterface $customer,
string $localeCode,
?string $tokenValue = null,
): OrderInterface;
}

View file

@ -21,6 +21,7 @@
<import resource="services/context.xml" />
<import resource="services/dashboard.xml" />
<import resource="services/emails.xml" />
<import resource="services/factories.xml" />
<import resource="services/fixtures.xml" />
<import resource="services/fixtures_listeners.xml" />
<import resource="services/fixtures_factories.xml" />

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of the Sylius package.
(c) Paweł Jędrzejewski
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults public="true" />
<service id="sylius_core.factory.order" class="Sylius\Bundle\CoreBundle\Factory\OrderFactory" decorates="sylius.factory.order">
<argument type="service" id="sylius_core.factory.order.inner" />
</service>
<service id="Sylius\Bundle\CoreBundle\Factory\OrderFactoryInterface" alias="sylius_core.factory.order" />
</services>
</container>

View file

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\CoreBundle\Factory;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\Address;
use Sylius\Component\Core\Model\Channel;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\Customer;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\Order;
use Sylius\Component\Currency\Model\Currency;
use Sylius\Component\Resource\Factory\FactoryInterface;
final class OrderFactorySpec extends ObjectBehavior
{
public function let(FactoryInterface $baseFactory): void
{
$this->beConstructedWith($baseFactory);
}
public function it_creates_an_order(FactoryInterface $baseFactory): void
{
$someOrder = new Order();
$baseFactory->createNew()->willReturn($someOrder);
$this->createNew()->shouldReturn($someOrder);
}
public function it_creates_a_cart(FactoryInterface $baseFactory): void {
$someOrder = new Order();
$baseFactory->createNew()->willReturn($someOrder);
$defaultAddress = new Address();
$defaultAddress->setStreet('123 Main St');
$customer = new Customer();
$customer->setDefaultAddress($defaultAddress);
$currency = new Currency();
$currency->setCode('USD');
$channel = new Channel();
$channel->setBaseCurrency($currency);
$cart = $this->createNewCart(
$channel,
$customer,
'en_US',
'mytoken',
);
$cart->getState()->shouldReturn(Order::STATE_CART);
$cart->getChannel()->shouldReturn($channel);
$cart->getLocaleCode()->shouldReturn('en_US');
$cart->getCurrencyCode()->shouldReturn('USD');
$cart->getTokenValue()->shouldReturn('mytoken');
$cart->getCustomer()->shouldReturn($customer);
$cart->getBillingAddress()->getStreet()->shouldReturn('123 Main St');
}
}