Verified on Shopware 6.7

Understanding the Cart Pipeline

Shopware 6 processes the cart through a pipeline of Collectors and Processors:

Cart Request
    → Collector 1 (gather data)
    → Collector 2 (gather data)
    → ...
    → Processor 1 (modify cart)
    → Processor 2 (modify cart)
    → ...
    → Final Cart

Collectors fetch data needed for calculations (prices, rules, external data).
Processors modify the cart (add items, change prices, add discounts, validate).

Example 1: Minimum Order Validation

Prevent checkout if the cart total is below a threshold:

// src/Cart/MinimumOrderValidator.php
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\Error\Error;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class MinimumOrderValidator implements CartProcessorInterface
{
    private const MINIMUM_ORDER_VALUE = 25.00;

    public function process(
        CartDataCollection $data,
        Cart $original,
        Cart $toCalculate,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void {
        $total = $toCalculate->getPrice()->getTotalPrice();

        if ($total > 0 && $total < self::MINIMUM_ORDER_VALUE) {
            $toCalculate->addErrors(
                new MinimumOrderValueError(self::MINIMUM_ORDER_VALUE, $total)
            );
        }
    }
}

The custom error class:

// src/Cart/Error/MinimumOrderValueError.php
use Shopware\Core\Checkout\Cart\Error\Error;

class MinimumOrderValueError extends Error
{
    private float $minimum;
    private float $current;

    public function __construct(float $minimum, float $current)
    {
        $this->minimum = $minimum;
        $this->current = $current;
        parent::__construct();
    }

    public function getId(): string
    {
        return 'minimum-order-value';
    }

    public function getMessageKey(): string
    {
        return 'minimum-order-value-not-reached';
    }

    public function getLevel(): int
    {
        return self::LEVEL_ERROR;  // Blocks checkout
    }

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

    public function getParameters(): array
    {
        return [
            'minimum' => $this->minimum,
            'current' => $this->current,
        ];
    }
}

Register in services.xml:

<service id="YourPlugin\Cart\MinimumOrderValidator">
    <tag name="shopware.cart.processor" priority="4000"/>
</service>

Example 2: Automatic Percentage Discount

Add an automatic 10% discount when cart exceeds a threshold:

// src/Cart/AutoDiscountProcessor.php
class AutoDiscountProcessor implements CartProcessorInterface
{
    private const DISCOUNT_THRESHOLD = 100.00;
    private const DISCOUNT_PERCENTAGE = 10;

    public function process(
        CartDataCollection $data,
        Cart $original,
        Cart $toCalculate,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void {
        $lineItems = $toCalculate->getLineItems()->filterType(LineItem::PRODUCT_LINE_ITEM_TYPE);
        $productTotal = $lineItems->getPrices()->sum()->getTotalPrice();

        if ($productTotal < self::DISCOUNT_THRESHOLD) {
            // Remove discount if it exists and threshold not met
            $toCalculate->getLineItems()->remove('auto-discount');
            return;
        }

        // Calculate discount amount
        $discountAmount = -1 * ($productTotal * self::DISCOUNT_PERCENTAGE / 100);

        $discount = new LineItem(
            'auto-discount',
            LineItem::DISCOUNT_LINE_ITEM,
            null,
            1
        );

        $discount->setLabel(sprintf('%d%% Discount (order over %.2f)', self::DISCOUNT_PERCENTAGE, self::DISCOUNT_THRESHOLD));
        $discount->setGood(false);
        $discount->setRemovable(false);
        $discount->setStackable(false);

        $discount->setPriceDefinition(
            new AbsolutePriceDefinition($discountAmount)
        );

        $toCalculate->add($discount);
    }
}

Example 3: B2B Customer Group Pricing

Apply different pricing tiers based on customer group:

// src/Cart/B2BPricingCollector.php
class B2BPricingCollector implements CartDataCollectorInterface
{
    public function collect(
        CartDataCollection $data,
        Cart $original,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void {
        $customerGroup = $context->getCurrentCustomerGroup();

        // Fetch tier configuration for this customer group
        $tiers = $this->tierRepository->search(
            (new Criteria())->addFilter(
                new EqualsFilter('customerGroupId', $customerGroup->getId())
            ),
            $context->getContext()
        );

        // Store in CartDataCollection for the processor
        $data->set('b2b_pricing_tiers', $tiers);
    }
}

// src/Cart/B2BPricingProcessor.php
class B2BPricingProcessor implements CartProcessorInterface
{
    public function process(
        CartDataCollection $data,
        Cart $original,
        Cart $toCalculate,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void {
        $tiers = $data->get('b2b_pricing_tiers');
        if (!$tiers || $tiers->count() === 0) {
            return;
        }

        foreach ($toCalculate->getLineItems() as $lineItem) {
            if ($lineItem->getType() !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
                continue;
            }

            $quantity = $lineItem->getQuantity();
            $applicableTier = $this->findApplicableTier($tiers, $quantity);

            if ($applicableTier) {
                $discountPercent = $applicableTier->getDiscountPercentage();
                $originalPrice = $lineItem->getPrice()->getUnitPrice();
                $newPrice = $originalPrice * (1 - $discountPercent / 100);

                $lineItem->setPriceDefinition(
                    new QuantityPriceDefinition(
                        $newPrice,
                        $context->buildTaxRules($lineItem->getPrice()->getTaxRules()),
                        $quantity
                    )
                );
            }
        }
    }

    private function findApplicableTier(EntityCollection $tiers, int $quantity): ?TierEntity
    {
        $applicable = null;
        foreach ($tiers as $tier) {
            if ($quantity >= $tier->getMinQuantity()) {
                if (!$applicable || $tier->getMinQuantity() > $applicable->getMinQuantity()) {
                    $applicable = $tier;
                }
            }
        }
        return $applicable;
    }
}

Register both with priorities:

<!- Collector runs first to gather data ->
<service id="YourPlugin\Cart\B2BPricingCollector">
    <tag name="shopware.cart.collector" priority="5000"/>
</service>

<!- Processor runs after to apply pricing ->
<service id="YourPlugin\Cart\B2BPricingProcessor">
    <tag name="shopware.cart.processor" priority="5000"/>
</service>

Example 4: Bundle Discount (Buy X + Y, Get Z% Off)

class BundleDiscountProcessor implements CartProcessorInterface
{
    // Define bundles: if both products are in cart, apply discount
    private array $bundles = [
        [
            'products' => ['PROD-001', 'PROD-002'],
            'discount_percent' => 15,
            'label' => 'Bundle: Starter Kit -15%',
        ],
    ];

    public function process(
        CartDataCollection $data,
        Cart $original,
        Cart $toCalculate,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void {
        $lineItems = $toCalculate->getLineItems();

        foreach ($this->bundles as $bundle) {
            $bundleKey = 'bundle-' . md5(implode('-', $bundle['products']));

            // Check if all bundle products are in cart
            $allPresent = true;
            $bundleTotal = 0;

            foreach ($bundle['products'] as $productNumber) {
                $found = $lineItems->filter(function (LineItem $item) use ($productNumber) {
                    return $item->getPayloadValue('productNumber') === $productNumber;
                });

                if ($found->count() === 0) {
                    $allPresent = false;
                    break;
                }

                $bundleTotal += $found->getPrices()->sum()->getTotalPrice();
            }

            if (!$allPresent) {
                $lineItems->remove($bundleKey);
                continue;
            }

            $discountAmount = -1 * ($bundleTotal * $bundle['discount_percent'] / 100);

            $discount = new LineItem($bundleKey, LineItem::DISCOUNT_LINE_ITEM, null, 1);
            $discount->setLabel($bundle['label']);
            $discount->setGood(false);
            $discount->setRemovable(false);
            $discount->setPriceDefinition(new AbsolutePriceDefinition($discountAmount));

            $toCalculate->add($discount);
        }
    }
}

Priority Guide

Priority determines execution order (higher = earlier):

Priority 10000: Data collectors (fetch external data)
Priority  5000: Price modifications (B2B pricing, special prices)
Priority  4000: Discounts (coupons, bundles, auto-discounts)
Priority  3000: Validation (minimum order, stock checks)

Common Gotchas

  1. Always use $toCalculate, not $original - $original is the unmodified cart, $toCalculate is what you should modify
  2. Remove discounts when conditions aren't met - otherwise stale line items persist
  3. Price recalculation - after modifying prices, Shopware recalculates totals automatically
  4. Don't block checkout in collectors - only processors should add errors
  5. Test with tax - B2B (net) and B2C (gross) calculations differ. Test both.

Need custom cart logic for your Shopware shop? We've built it all.