---
title: 'Cart: Promotions and Discounts | Shopware Community Hub'
description: >-
  Learn how Shopware promotions are represented in the cart and how they differ
  from custom discount line items.
canonical_url: 'https://hub.shopware.com/learn/unit/cart-promotions-and-discounts'
---

# Cart: Promotions and Discounts

<LearningObjectives>

- Understand how Shopware promotions are applied during cart calculation.
- Distinguish between promotion line items and custom discount line items.
- Know when to use the built-in promotion system and when custom discounts are suitable.

</LearningObjectives>

# Cart: Promotions and Discounts

Shopware provides a built-in promotion system that allows merchants to configure discounts, vouchers, and special offers in the administration.

During cart calculation, Shopware evaluates these promotions and applies them automatically to the cart.

As a developer, you usually do not implement promotions in code, but you need to understand how promotions are represented and proceed in the cart.

This learning unit focuses on how promotions appear in the cart, how they differ from custom discount logic, and how both approaches fit into the cart calculation pipeline.

## What This Learning Unit Intentionally Does Not Cover

To avoid confusion, this learning does not implement:

- Custom shipping price calculations.
- Tax manipulation via cart processors.
- Hard-coded shipping or tax logic.

Shipping methods, shipping prices, and taxes are usually configured in the administration and controlled via rules. Implementing them directly in the cart processors is considered as an anti-pattern.

## Plugin Implementation

The `AcademyCartExamples` plugin demonstrates the custom discount logic implemented in this learning unit:

```shell
# Clone the repository into your custom/plugins directory of your Shopware project
git clone https://github.com/ShopwareAcademy/AcademyCartExamples.git custom/plugins/AcademyCartExamples

# Checkout the specific tag for this learning unit
cd custom/plugins/AcademyCartExamples
git checkout LP-03-CO-05-LU-05-cart-promotions-discounts
```

## Promotions in Cart Calculation

In Shopware, promotions are configured in the administration (Marketing -> Promotions). They define rules such as:

- Discount type (percentage, fixed amount, free shipping, etc.)
- Conditions (cart value, customer group, etc.)
- Validity and usage limits.

During cart calculation, Shopware checks which promotions match the current cart and applies them automatically.

### How Promotions Work

Promotions are processed in two main phases:

1. **Collection Phase**: The `PromotionCollector` identifies which promotions are applicable to the current cart.
2. **Processing Phase**: The `PromotionProcessor` applies the promotion logic and adds the corresponding promotion line items to the cart.

### Promotion Line Items

When a promotion is applied, it creates a special line item in the cart.

The following example is simplified and for illustration purposes only. Promotion line items are created internally by Shopware and must never be constructed manually in plugins.

```php
// Promotion line items have specific characteristics
$promotionLineItem = new LineItem(
    'promotion-' . $promotionId,
    LineItem::PROMOTION_LINE_ITEM_TYPE
);

$promotionLineItem->setLabel('10% Discount');
$promotionLineItem->setPrice(new CalculatedPrice(
    -10.00, // Negative price = discount
    -10.00,
    new CalculatedTaxCollection(),
    new TaxRuleCollection()
));
```

**Key facts about promotion line items:**

- They use the line item type `PROMOTION_LINE_ITEM_TYPE`.
- They represent discounts defined in the administration.
- A negative price indicates a discount.
- The calculation logic is handled entirely by Shopware.

Promotion line items are part of Shopware's marketing and rule system. They should be used whenever discount logic can be expressed through configuration instead of code.

## Custom Discounts with Cart Processors

Sometimes you need pricing logic that is not modeled as a Shopware promotion. In that case, you can add a **custom discount line item** in a cart processor.

A use case might be a project-specific B2B discount that depends on your own rules.

To add a custom discount line item, you need to implement a cart processor. In this cart processor, you create a new line item of type `LineItem::DISCOUNT_LINE_ITEM` and add it to the cart.

And always calculate prices using Shopware's price calculators. Avoid setting `CalculatedPrice` manually, because you may break the internal logic of creating the price.

### Example: B2B Discount for Orders Above €500

The following processor checks whether the customer is a B2B customer and adds a discount for orders over €500.

```php
<?php declare(strict_types=1);

namespace AcademyCartExamples\Cart\Processor;

use AcademyCartExamples\Service\AcademyCartService;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\LineItem\CartDataCollection;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class B2BDiscountProcessor implements CartProcessorInterface
{
    public function __construct(
        private readonly AcademyCartService $academyCartService
    ) {
    }

    public function process(
        CartDataCollection $data,
        Cart $original,
        Cart $toCalculate,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void {
        // Only apply to B2B customers; if not, remove discount
        if (false === $this->academyCartService->isB2BCustomer($context)) {
            if ($toCalculate->has(AcademyCartService::B2B_DISCOUNT_ID)) {
                $toCalculate->remove(AcademyCartService::B2B_DISCOUNT_ID);
            }

            return;
        }

        $totalValue = $this->academyCartService->calculateTotalValue($toCalculate->getLineItems());

        // Discount applies only for cart total above €500. Remove discount if below the threshold
        if ($totalValue <= 500.00) {
            if ($toCalculate->has(AcademyCartService::B2B_DISCOUNT_ID)) {
                $toCalculate->remove(AcademyCartService::B2B_DISCOUNT_ID);
            }
            return;
        }

        // Apply additional 5% B2B discount for orders over €500
        $this->academyCartService->addB2BDiscount($toCalculate, $totalValue, $context);
        
    }
}
```

**Explanation:**

- The cart processor enforces a simple rule: the discount applies only to B2B customers and only if the cart total is above €500.
- If the rule no longer matches (e.g., the customer is not B2B or the total drops below the threshold), the discount line item is removed.
- If the rule matches, the discount line item is added (or replaced) without creating duplicates.

And the methods in the service class that add the custom discount line item:

```php
<?php declare(strict_types=1);

namespace AcademyCartExamples\Service;

use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\Price\QuantityPriceCalculator;
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\LineItem\LineItemCollection;
use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;

class AcademyCartService
{
    public const string B2B_DISCOUNT_ID = 'academy-b2b-bonus-discount';

    public function __construct(
        private readonly QuantityPriceCalculator $quantityPriceCalculator
    ) {
    }

    public function isB2BCustomer(SalesChannelContext $context): bool
    {
        // Check if customer has B2B role or company
        $customer = $context->getCustomer();
        if (null === $customer) {
            return false;
        }

        if (null === $customer->getCompany()) {
            return false;
        }

        return true;
    }

    public function calculateTotalValue(LineItemCollection $lineItems): float
    {
        $total = 0.0;
        foreach ($lineItems as $lineItem) {
            if ($lineItem->getType() !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
                continue;
            }


            $price = $lineItem->getPrice();
            if (null === $price) {
                continue;
            }

            $total += $price->getTotalPrice();
        }

        return $total;
    }

    public function addB2BDiscount(Cart $toCalculate, float $totalValue, SalesChannelContext $context): void
    {
        $discountLineItem = new LineItem(
            self::B2B_DISCOUNT_ID,
            LineItem::DISCOUNT_LINE_ITEM
        );

        $discountLineItem->setLabel('B2B Bonus Discount');
        $discountLineItem->setGood(false); // Not a physical product
        $discountLineItem->setStackable(false); // Not stackable

        $discountAmount = $totalValue * 0.05; // 5% additional discount

        $priceDefinition = new QuantityPriceDefinition(
            -$discountAmount,
            new TaxRuleCollection(),
            1
        );

        $discountLineItem->setPriceDefinition($priceDefinition);

        $calculatedPrice = $this->quantityPriceCalculator->calculate($priceDefinition, $context);

        $discountLineItem->setPrice($calculatedPrice);

        $toCalculate->add($discountLineItem);
    }
}
```

**Key explanations:**

A reliable pattern for custom discount line items is:

1. Create the `LineItem`.
2. Define a `PriceDefinition` set it to the line item's price definition property.
3. Calculate the price based on the price definition by applying a calculator (in this case, the `QuantityPriceCalculator`). It is important to let Shopware calculate the price to avoid any errors.
4. Assign the calculated price to the line item's price property.
5. Finally, add the line item to the cart.

This way, your custom discount integrates correctly with Shopware's cart calculation.

### Why Calculators Matter

Shopware uses calculators to ensure that:

- Taxes are applied correctly.
- Rounding rules are respected.
- Cart totals stay consistent across different currencies.
- Recalculations remain stable.

Manually creating a `CalculatedPrice` may look easier, but it bypasses the pricing system and can cause unexpected results.

## Order of Operations

### How Cart Calculation is Structured

Shopware's cart calculation follows a deterministic pipeline. Each step builds on the result on the previous one.

A simplified mental model looks like this:

1. **Product prices** are calculated first
2. **Promotions and discounts** are applied to adjust prices
3. **Shipping costs** are calculated based on final product values
4. **Taxes** are calculated on the final amounts (including shipping)

### Promotions vs. Custom Discounts

Although both reduce the cart total, they serve different purposes.

**Promotions**:

- Are configured in the administration.
- Are evaluated automatically by Shopware.
- Create promotion line items in the cart.
- Are processed by the promotion system.

**Custom discounts**:

- Are implemented in plugins.
- You need to add them manually to the cart by using a cart processor.
- Create generic discount line items (`LineItem::DISCOUNT_LINE_ITEM`) in the cart.
- Are not processed by the promotion system.
- Are fully controlled by your own business logic.

**Rule of thumb:**

- If the logic can be expressed with rules and conditions in the administration, use promotions.
- If the logic is project-specific or technical, use custom discount line items.

## Best Practices

- Always use a stable, predictable ID for custom discounts so they can be safely replaced or removed during cart recalculation.
- Prefer early returns in cart processors as soon as a rule does not match to keep cart logic easy to read and maintain.
- Keep cart processors small and focused; Move complex logic into services and let the processor only orchestrate the flow.
- Avoid cross-responsibility logic in cart processors.

### Testing Strategies

- **Test with different customer types** (B2B vs. B2C).
- **Verify tax calculations** across different countries and regions.
- **Test promotion combinations** to ensure they work correctly together.
- **Validate shipping calculations** with various product combinations.

### Common Pitfalls

- **Don't modify cart state** in ways that break other processors.
- **Ensure proper error handling** for missing data or invalid configurations.
- **Test edge cases** like empty carts, zero-value items, and extreme quantities.

## Summary

Great! In this learning unit, you learned:

- How Shopware applies promotions during cart calculation and how they are represented as promotion line items.
- The difference between promotion line items and custom line items.
- When to rely on Shopware's built-in promotion system and when to implement custom logic.
- How to implement a custom discount using a cart processor.
- Why Shopware's price calculators must be used to ensure correct and stable cart calculation.

With this knowledge, you can make informed decisions about discount logic, avoid common cart calculation pitfalls, and implement pricing behavior that integrates cleanly with Shopware's cart system.
