This commit is contained in:
Mateusz 2026-06-24 07:12:33 +00:00 committed by GitHub
commit 057651da11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 122 additions and 9 deletions

View file

@ -52,6 +52,16 @@ Feature: Maintaining cart after authorization
Then there should be one item in my cart
And this item should have name "Stark T-Shirt"
@api @ui @mink:chromedriver
Scenario: Having cart maintained after logging in when the guest has addressed the cart with a typo email and then corrected it
When I add "Stark T-Shirt" product to the cart
And I addressed the cart with email "robb@strak.com"
And I addressed the cart with email "robb@stark.com"
And I log in as "robb@stark.com" with "KingInTheNorth" password
And I see the summary of my cart
Then there should be one item in my cart
And this item should have name "Stark T-Shirt"
@api @ui @mink:chromedriver
Scenario: Having cart maintained after registration
When I add product "Stark T-Shirt" to the cart

View file

@ -54,7 +54,7 @@ final readonly class BlameCartHandler
throw new UnprocessableCartException();
}
if (null !== $cart->getCustomer()) {
if (!$cart->isCreatedByGuest()) {
throw new ConflictHttpException('There is an assigned customer to this cart');
}

View file

@ -65,14 +65,14 @@ final class BlameCartHandlerTest extends TestCase
->method('findCartByTokenValue')
->with('TOKEN')
->willReturn($this->cart);
$this->cart->expects(self::once())->method('getCustomer')->willReturn(null);
$this->cart->expects(self::once())->method('isCreatedByGuest')->willReturn(true);
$this->user->expects(self::once())->method('getCustomer')->willReturn($this->customerMock);
$this->cart->expects(self::once())->method('setCustomerWithAuthorization')->with($this->customerMock);
$this->orderProcessor->expects(self::once())->method('process')->with($this->cart);
$this->handler->__invoke(new BlameCart('sylius@example.com', 'TOKEN'));
}
public function testThrowsAnExceptionIfCartIsOccupied(): void
public function testThrowsAnExceptionIfCartIsNotCreatedByGuest(): void
{
$this->shopUserRepository->expects(self::once())
->method('findOneByEmail')
@ -81,7 +81,7 @@ final class BlameCartHandlerTest extends TestCase
$this->orderRepository->expects(self::once())
->method('findCartByTokenValue')
->with('TOKEN')->willReturn($this->cart);
$this->cart->expects(self::once())->method('getCustomer')->willReturn($this->customerMock);
$this->cart->expects(self::once())->method('isCreatedByGuest')->willReturn(false);
self::expectException(ConflictHttpException::class);
$this->handler->__invoke(new BlameCart('sylius@example.com', 'TOKEN'));
}

View file

@ -233,12 +233,20 @@ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInte
ChannelInterface $channel,
CustomerInterface $customer,
): ?OrderInterface {
$completedCheckoutsQueryBuilder = $this->createQueryBuilder('completedOrder')
->select('completedOrder.id')
->andWhere('completedOrder.channel = :channel')
->andWhere('completedOrder.customer = :customer')
->andWhere('completedOrder.checkoutCompletedAt > o.createdAt')
;
return $this->createQueryBuilder('o')
->andWhere('o.state = :state')
->andWhere('o.channel = :channel')
->andWhere('o.customer = :customer')
->andWhere('o.items IS NOT EMPTY')
->andWhere('o.createdByGuest = :createdByGuest')
->andWhere(sprintf('NOT EXISTS (%s)', $completedCheckoutsQueryBuilder->getDQL()))
->setParameter('state', OrderInterface::STATE_CART)
->setParameter('channel', $channel)
->setParameter('customer', $customer)

View file

@ -61,7 +61,7 @@ final class ShopCartBlamerListener
private function blame(ShopUserInterface $user): void
{
$cart = $this->getCart();
if (null === $cart || null !== $cart->getCustomer()) {
if (null === $cart || !$cart->isCreatedByGuest()) {
return;
}

View file

@ -103,7 +103,7 @@ final class ShopCartBlamerListenerTest extends TestCase
$this->sectionResolver->expects($this->once())->method('getSection')->willReturn($shopSection);
$this->cartContext->expects($this->once())->method('getCart')->willReturn($cart);
$cart->expects($this->once())->method('getCustomer')->willReturn(null);
$cart->expects($this->once())->method('isCreatedByGuest')->willReturn(true);
$userEvent->expects($this->once())->method('getUser')->willReturn($user);
$user->expects($this->once())->method('getCustomer')->willReturn($customer);
$cart->expects($this->once())->method('setCustomerWithAuthorization')->with($customer);
@ -128,7 +128,7 @@ final class ShopCartBlamerListenerTest extends TestCase
$this->sectionResolver->expects($this->once())->method('getSection')->willReturn($shopSection);
$this->cartContext->expects($this->once())->method('getCart')->willReturn($cart);
$cart->expects($this->once())->method('getCustomer')->willReturn(null);
$cart->expects($this->once())->method('isCreatedByGuest')->willReturn(true);
$token->expects($this->once())->method('getUser')->willReturn($user);
$user->expects($this->once())->method('getCustomer')->willReturn($customer);
$cart->expects($this->once())->method('setCustomerWithAuthorization')->with($customer);
@ -137,6 +137,28 @@ final class ShopCartBlamerListenerTest extends TestCase
}
public function testDoesNothingIfGivenCartHasBeenBlamedInPast(): void
{
/** @var OrderInterface&MockObject $cart */
$cart = $this->createMock(OrderInterface::class);
/** @var Request&MockObject $request */
$request = $this->createMock(Request::class);
/** @var TokenInterface&MockObject $token */
$token = $this->createMock(TokenInterface::class);
/** @var ShopUserInterface&MockObject $user */
$user = $this->createMock(ShopUserInterface::class);
/** @var ShopSection&MockObject $shopSection */
$shopSection = $this->createMock(ShopSection::class);
$this->sectionResolver->expects($this->once())->method('getSection')->willReturn($shopSection);
$this->cartContext->expects($this->once())->method('getCart')->willReturn($cart);
$cart->expects($this->once())->method('isCreatedByGuest')->willReturn(false);
$token->expects($this->once())->method('getUser')->willReturn($user);
$cart->expects($this->never())->method('setCustomerWithAuthorization');
$this->shopCartBlamerListener->onInteractiveLogin(new InteractiveLoginEvent($request, $token));
}
public function testBlamesCartWhenGuestCartAlreadyHasCustomerButIsStillGuestCart(): void
{
/** @var OrderInterface&MockObject $cart */
$cart = $this->createMock(OrderInterface::class);
@ -153,9 +175,10 @@ final class ShopCartBlamerListenerTest extends TestCase
$this->sectionResolver->expects($this->once())->method('getSection')->willReturn($shopSection);
$this->cartContext->expects($this->once())->method('getCart')->willReturn($cart);
$cart->expects($this->once())->method('getCustomer')->willReturn($customer);
$cart->expects($this->once())->method('isCreatedByGuest')->willReturn(true);
$token->expects($this->once())->method('getUser')->willReturn($user);
$cart->expects($this->never())->method('setCustomerWithAuthorization');
$user->expects($this->once())->method('getCustomer')->willReturn($customer);
$cart->expects($this->once())->method('setCustomerWithAuthorization')->with($customer);
$this->shopCartBlamerListener->onInteractiveLogin(new InteractiveLoginEvent($request, $token));
}

View file

@ -110,6 +110,78 @@ final class CartTest extends JsonApiTestCase
self::assertSame([], $response['items']);
}
#[Test]
public function it_does_not_return_a_cart_abandoned_before_the_customer_last_completed_checkout(): void
{
$this->setUpDefaultPostHeaders();
$this->setUpShopUserContext();
$this->loadFixturesFromFiles([
'authentication/shop_user.yaml',
'channel/channel.yaml',
'cart.yaml',
'country.yaml',
'shipping_method.yaml',
'payment_method.yaml',
]);
$this->placeOrder(
tokenValue: 'placedOrderToken',
email: 'shop@example.com',
checkoutCompletedAt: new \DateTimeImmutable(),
);
$abandonedCartToken = $this->pickUpCart(tokenValue: 'abandonedCartToken', email: 'shop@example.com');
$this->addItemToCart('MUG_BLUE', 3, $abandonedCartToken);
$abandonedCart = $this->orderRepository->findCartByTokenValue($abandonedCartToken);
$abandonedCart->setCreatedAt(new \DateTime('-1 hour'));
$this->get('doctrine.orm.entity_manager')->flush();
$this->requestPost(uri: '/api/v2/shop/orders', body: []);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_CREATED);
$response = json_decode($this->client->getResponse()->getContent(), true, flags: \JSON_THROW_ON_ERROR);
self::assertNotSame($abandonedCartToken, $response['tokenValue']);
self::assertSame([], $response['items']);
}
#[Test]
public function it_returns_the_customer_cart_created_after_the_last_completed_checkout(): void
{
$this->setUpDefaultPostHeaders();
$this->setUpShopUserContext();
$this->loadFixturesFromFiles([
'authentication/shop_user.yaml',
'channel/channel.yaml',
'cart.yaml',
'country.yaml',
'shipping_method.yaml',
'payment_method.yaml',
]);
$this->placeOrder(
tokenValue: 'placedOrderToken',
email: 'shop@example.com',
checkoutCompletedAt: new \DateTimeImmutable('-1 hour'),
);
$newCartToken = $this->pickUpCart(tokenValue: 'newCartToken', email: 'shop@example.com');
$this->addItemToCart('MUG_BLUE', 3, $newCartToken);
$this->requestPost(uri: '/api/v2/shop/orders', body: []);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_CREATED);
$response = json_decode($this->client->getResponse()->getContent(), true, flags: \JSON_THROW_ON_ERROR);
self::assertSame($newCartToken, $response['tokenValue']);
self::assertCount(1, $response['items']);
}
#[Test]
public function it_gets_an_empty_cart_as_guest(): void
{