Skip to content
Permalink
Browse files Browse the repository at this point in the history
NEXT-23325 - Add product line item validator for duplicate line items…
… with stock issues
  • Loading branch information
mstegmeyer committed Dec 21, 2022
1 parent 2b80089 commit 4fce120
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 2 deletions.
@@ -0,0 +1,6 @@
---
title: Fix quantity issues on duplicate product line items
issue: NEXT-23325
---
# Core
* Added `Shopware\Core\Content\Product\Cart\ProductLineItemValidator` to check product quantity limits across product line items
4 changes: 4 additions & 0 deletions src/Core/Checkout/DependencyInjection/cart.xml
Expand Up @@ -203,6 +203,10 @@
<argument type="tagged" tag="shopware.cart.validator"/>
</service>

<service id="Shopware\Core\Content\Product\Cart\ProductLineItemValidator">
<tag name="shopware.cart.validator"/>
</service>

<service id="Shopware\Core\Checkout\Cart\Processor">
<argument type="service" id="Shopware\Core\Checkout\Cart\Validator"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\Price\AmountCalculator"/>
Expand Down
64 changes: 64 additions & 0 deletions src/Core/Content/Product/Cart/ProductLineItemValidator.php
@@ -0,0 +1,64 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Content\Product\Cart;

use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartValidatorInterface;
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

/**
* @package checkout
*/
class ProductLineItemValidator implements CartValidatorInterface
{
public function validate(Cart $cart, ErrorCollection $errors, SalesChannelContext $context): void
{
$productLineItems = array_filter($cart->getLineItems()->getFlat(), static function (LineItem $lineItem) {
return $lineItem->getType() === LineItem::PRODUCT_LINE_ITEM_TYPE;
});

foreach ($productLineItems as $lineItem) {
$productId = $lineItem->getReferencedId();
if ($productId === null) {
continue;
}
$totalQuantity = $this->getTotalQuantity($productId, $productLineItems);

$quantityInformation = $lineItem->getQuantityInformation();
if ($quantityInformation === null) {
continue;
}

$minPurchase = $quantityInformation->getMinPurchase();
$available = $quantityInformation->getMaxPurchase() ?? 0;
$steps = $quantityInformation->getPurchaseSteps() ?? 1;

if ($available >= $totalQuantity) {
continue;
}

$maxAvailable = (int) (floor(($available - $minPurchase) / $steps) * $steps + $minPurchase);

$cart->addErrors(
new ProductStockReachedError($productId, (string) $lineItem->getLabel(), $maxAvailable, false),
);
}
}

/**
* @param LineItem[] $productLineItems
*/
private function getTotalQuantity(string $productId, array $productLineItems): int
{
$totalQuantity = 0;
foreach ($productLineItems as $lineItem) {
if ($lineItem->getReferencedId() === $productId) {
$totalQuantity += $lineItem->getQuantity();
}
}

return $totalQuantity;
}
}
12 changes: 10 additions & 2 deletions src/Core/Content/Product/Cart/ProductStockReachedError.php
Expand Up @@ -22,7 +22,9 @@ class ProductStockReachedError extends Error
*/
protected $quantity;

public function __construct(string $id, string $name, int $quantity)
protected bool $resolved;

public function __construct(string $id, string $name, int $quantity, bool $resolved = true)
{
$this->id = $id;

Expand All @@ -35,6 +37,7 @@ public function __construct(string $id, string $name, int $quantity)
parent::__construct($this->message);
$this->name = $name;
$this->quantity = $quantity;
$this->resolved = $resolved;
}

public function getParameters(): array
Expand Down Expand Up @@ -64,11 +67,16 @@ public function getMessageKey(): string

public function getLevel(): int
{
return self::LEVEL_WARNING;
return $this->resolved ? self::LEVEL_WARNING : self::LEVEL_ERROR;
}

public function blockOrder(): bool
{
return true;
}

public function isPersistent(): bool
{
return $this->resolved;
}
}
@@ -0,0 +1,101 @@
<?php declare(strict_types=1);

namespace Shopware\Tests\Unit\Core\Content\Product\Cart;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\LineItem\QuantityInformation;
use Shopware\Core\Content\Product\Cart\ProductLineItemFactory;
use Shopware\Core\Content\Product\Cart\ProductLineItemValidator;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

/**
* @internal
* @covers \Shopware\Core\Content\Product\Cart\ProductLineItemValidator
*/
class ProductLineItemValidatorTest extends TestCase
{
public function testValidateOnDuplicateProductsAtMaxPurchase(): void
{
$cart = new Cart(Uuid::randomHex(), Uuid::randomHex());
$builder = new ProductLineItemFactory();
$cart->add(
$builder
->create('product-1')
->setQuantityInformation(
(new QuantityInformation())
->setMinPurchase(1)
->setMaxPurchase(1)
->setPurchaseSteps(1)
)
);
$cart->add(
$builder
->create('product-2')
->setReferencedId('product-1')
->setQuantityInformation(
(new QuantityInformation())
->setMinPurchase(1)
->setMaxPurchase(1)
->setPurchaseSteps(1)
)
);

static::assertCount(0, $cart->getErrors());

$validator = new ProductLineItemValidator();
$validator->validate($cart, $cart->getErrors(), $this->createMock(SalesChannelContext::class));

static::assertCount(1, $cart->getErrors());
}

public function testValidateOnDuplicateProductsWithSafeQuantity(): void
{
$cart = new Cart(Uuid::randomHex(), Uuid::randomHex());
$builder = new ProductLineItemFactory();
$cart->add(
$builder
->create('product-1')
->setQuantityInformation(
(new QuantityInformation())
->setMinPurchase(1)
->setMaxPurchase(3)
->setPurchaseSteps(1)
)
);
$cart->add(
$builder
->create('product-2')
->setReferencedId('product-1')
->setQuantityInformation(
(new QuantityInformation())
->setMinPurchase(1)
->setMaxPurchase(3)
->setPurchaseSteps(1)
)
);

static::assertCount(0, $cart->getErrors());

$validator = new ProductLineItemValidator();
$validator->validate($cart, $cart->getErrors(), $this->createMock(SalesChannelContext::class));

static::assertCount(0, $cart->getErrors());
}

public function testValidateOnDuplicateProductsWithoutQuantityInformation(): void
{
$cart = new Cart(Uuid::randomHex(), Uuid::randomHex());
$builder = new ProductLineItemFactory();
$cart->add($builder->create('product-1'));
$cart->add($builder->create('product-2')->setReferencedId('product-1'));

static::assertCount(0, $cart->getErrors());

$validator = new ProductLineItemValidator();
$validator->validate($cart, $cart->getErrors(), $this->createMock(SalesChannelContext::class));

static::assertCount(0, $cart->getErrors());
}
}

0 comments on commit 4fce120

Please sign in to comment.