Skip to content

Discounts API

Manage discounts and discount codes

By integrating with the Discount feature you can automate the process of managing discounts, streamlining the whole process and automating business rules.

For example, you can automatically create a discount when a customer places their 3rd order, encouraging them to make another purchase and increase their chances of becoming a loyal customer.

You can manage discounts using data migrations, REST API, or the PHP API by using the Ibexa\Contracts\Discounts\DiscountServiceInterface service.

The core concepts when working with discounts through the APIs are listed below.

Types

When using the PHP API, the discount type defines where the discount can be applied.

Discounts are applied in two places, listed in the DiscountType class:

  • Product catalog - catalog discounts are activated when browsing the product catalog and do not require any action from the customer to be activated
  • Cart - cart discounts can activate when entering the cart, if the right conditions are met. They may also require entering a discount code to be activated

Regardless of activation place, discounts always apply to products and reduce their base price.

To define when a discount activates and how the price is reduced, use rules and conditions. They make use of the Symfony Expression language. Use the expression values provided below when using data migrations or when parsing REST API responses.

Rules

Discount rules define how the calculate the price reduction. The following discount rule types are available in the \Ibexa\Discounts\Value\DiscountRule namespace:

Rule type Identifier Description Expression value
FixedAmount fixed_amount Deducts the specified amount, for example 10 EUR, from the base price discount_amount
Percentage percentage Deducts the specified percentage, for example -10%, from the base price discount_percentage

Only a single discount can be applied to a given product, and a discount can only have a single rule.

Conditions

With conditions you can narrow down the scenarios in which the discount applies. The following conditions are available in the \Ibexa\Discounts\Value\DiscountCondition and \Ibexa\DiscountsCodes\Value\DiscountCondition namespaces:

Condition Applies to Identifier Description Expression values
IsInCategory Cart, Catalog is_in_category Checks if the product belongs to specified product categories categories
IsInCurrency Cart, Catalog is_in_currency Checks if the product has price in the specified currency currency_code
IsInRegions Cart, Catalog is_in_regions Checks if the customer is making the purchase in one of the specified regions regions
IsProductInArray Cart, Catalog is_product_in_array Checks if the product belongs to the group of selected products product_codes
IsUserInCustomerGroup Cart, Catalog is_user_in_customer_group Check if the customer belongs to specified customer groups customer_groups
IsProductInQuantityInCart Cart is_product_in_quantity_in_cart Checks if the required minimum quantity of a given product is present in the cart quantity
MinimumPurchaseAmount Cart minimum_purchase_amount Checks if purchase amount in the cart exceeds the specified minimum minimum_purchase_amount
IsValidDiscountCode Cart is_valid_discount_code Checks if the correct discount code has been provided and how many times it was used by the customer discount_code, usage_count

When multiple conditions are specified, all of them must be met.

Priority

You can set discount priority as a number between 1 and 10 to indicate which discount should have higher priority when choosing the one to apply.

Start and end date

Discounts can be permanent, or valid only in a specified time frame.

Every discount has a start date, which defaults to the date when the discount was created. The end date can be set to null to make the discount permanent.

Status

You can disable a discount anytime to stop it from being active, even if the conditions enforced by start and end date are met.

Only disabled discounts can be deleted.

Discount translations

The discount has four properties that can be translated:

Property Usage
Name Internal information for store managers
Description Internal information for store managers
Promotion label Information displayed to customers
Promotion description Information displayed to customers

Use the DiscountTranslationStruct to provide translations for discounts.

Discount codes

To activate a cart discount only after a proper discount code is provided, you need to:

  1. Create a discount code using the DiscountCodeServiceInterface::createDiscountCode() method
  2. Attach it to a discount by using the IsValidDiscountCode condition

Set the usedLimit property to the number of times a single customer can use this code, or to null to make the usage unlimited.

The DiscountCodeServiceInterface::registerUsage() method is used to track the number of times a discount code has been used.

Example API usage

The example below contains a Command creating a cart discount. The discount:

  • has the highest possible priority value
  • rule deducts 10 EUR from the base price of the product
  • is permanent
  • depends on
    • being bought from Germany or France
    • 2 products
    • a summer10 discount code which can be used only 10 times, but a single customer can use the code multiple times
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
<?php

declare(strict_types=1);

namespace App\Command;

use DateTimeImmutable;
use Ibexa\Contracts\Core\Collection\ArrayMap;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Discounts\DiscountServiceInterface;
use Ibexa\Contracts\Discounts\Value\DiscountType;
use Ibexa\Contracts\Discounts\Value\Struct\DiscountCreateStruct;
use Ibexa\Contracts\Discounts\Value\Struct\DiscountTranslationStruct;
use Ibexa\Contracts\DiscountsCodes\DiscountCodeServiceInterface;
use Ibexa\Contracts\DiscountsCodes\Value\Struct\DiscountCodeCreateStruct;
use Ibexa\Discounts\Value\DiscountCondition\IsInCurrency;
use Ibexa\Discounts\Value\DiscountCondition\IsInRegions;
use Ibexa\Discounts\Value\DiscountCondition\IsProductInArray;
use Ibexa\Discounts\Value\DiscountRule\FixedAmount;
use Ibexa\DiscountsCodes\Value\DiscountCondition\IsValidDiscountCode;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class ManageDiscountsCommand extends Command
{
    protected static $defaultName = 'discounts:manage';

    private DiscountServiceInterface $discountService;

    private DiscountCodeServiceInterface $discountCodeService;

    private PermissionResolver $permissionResolver;

    private UserService $userService;

    public function __construct(
        UserService $userService,
        PermissionResolver $permissionResolver,
        DiscountServiceInterface $discountService,
        DiscountCodeServiceInterface $discountCodeService
    ) {
        $this->userService = $userService;
        $this->discountService = $discountService;
        $this->discountCodeService = $discountCodeService;
        $this->permissionResolver = $permissionResolver;

        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->permissionResolver->setCurrentUserReference(
            $this->userService->loadUserByLogin('admin')
        );

        $now = new DateTimeImmutable();

        $discountCodeCreateStruct = new DiscountCodeCreateStruct(
            'summer10',
            10, // Global usage limit
            null, // Unlimited usage per customer
            $this->permissionResolver->getCurrentUserReference()->getUserId(),
            $now
        );
        $discountCode = $this->discountCodeService->createDiscountCode($discountCodeCreateStruct);

        $discountCreateStruct = new DiscountCreateStruct();
        $discountCreateStruct
            ->setIdentifier('discount_identifier')
            ->setType(DiscountType::CART)
            ->setPriority(10)
            ->setEnabled(true)
            ->setUser($this->userService->loadUserByLogin('admin'))
            ->setRule(new FixedAmount(10))
            ->setStartDate($now)
            ->setConditions([
                new IsInRegions(['germany', 'france']),
                new IsProductInArray(['product-1', 'product-2']),
                new IsInCurrency('EUR'),
                new IsValidDiscountCode(
                    $discountCode->getCode(),
                    $discountCode->getGlobalLimit(),
                    $discountCode->getUsedLimit()
                ),
            ])
            ->setTranslations([
                new DiscountTranslationStruct('eng-GB', 'Discount name', 'This is a discount description', 'Promotion Label', 'Promotion Description'),
                new DiscountTranslationStruct('ger-DE', 'Discount name (German)', 'Description (German)', 'Promotion Label (German)', 'Promotion Description (German)'),
            ])
            ->setEndDate(null) // Permanent discount
            ->setCreatedAt($now)
            ->setUpdatedAt($now)
            ->setContext(new ArrayMap(['custom_context' => 'custom_value']));

        $this->discountService->createDiscount($discountCreateStruct);

        return Command::SUCCESS;
    }
}

Similarly, use the deleteDiscount, deleteTranslation, disableDiscount, enableDiscount, and updateDiscount methods from the DiscountServiceInterface to manage the discounts. You can always attach additional logic to the Discounts API by listening to the available events.

You can search for Discounts using the DiscountServiceInterface::findDiscounts() method. To learn more about the available search options, see Discounts' Search Criteria and Sort Clauses.

For discount codes, you can query the database for discount code usage using DiscountCodeServiceInterface::findCodeUsages() and DiscountCodeUsageQuery.

Retrieve applied discounts

The applied discounts change final product pricing. To learn more about working with prices, see Price API.

The example below shows how you can use:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php declare(strict_types=1);

namespace App\Command;

use Exception;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\OrderManagement\OrderServiceInterface;
use Ibexa\Contracts\ProductCatalog\CurrencyServiceInterface;
use Ibexa\Contracts\ProductCatalog\PriceResolverInterface;
use Ibexa\Contracts\ProductCatalog\ProductPriceServiceInterface;
use Ibexa\Contracts\ProductCatalog\ProductServiceInterface;
use Ibexa\Contracts\ProductCatalog\Values\Price\PriceContext;
use Ibexa\Contracts\ProductCatalog\Values\Price\PriceEnvelopeInterface;
use Ibexa\Discounts\Value\Price\Stamp\DiscountStamp;
use Ibexa\OrderManagement\Discounts\Value\DiscountsData;
use Money\Money;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class OrderPriceCommand extends Command
{
    protected static $defaultName = 'app:discounts:prices';

    private PermissionResolver $permissionResolver;

    private UserService $userService;

    private ProductServiceInterface $productService;

    private OrderServiceInterface $orderService;

    private ProductPriceServiceInterface $productPriceService;

    private CurrencyServiceInterface $currencyService;

    private PriceResolverInterface $priceResolver;

    public function __construct(
        PermissionResolver $permissionResolver,
        UserService $userService,
        ProductServiceInterface $productService,
        OrderServiceInterface $orderService,
        ProductPriceServiceInterface $productPriceService,
        CurrencyServiceInterface $currencyService,
        PriceResolverInterface $priceResolver
    ) {
        parent::__construct();

        $this->permissionResolver = $permissionResolver;
        $this->userService = $userService;
        $this->productService = $productService;
        $this->orderService = $orderService;
        $this->productPriceService = $productPriceService;
        $this->currencyService = $currencyService;
        $this->priceResolver = $priceResolver;
    }

    public function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->permissionResolver->setCurrentUserReference($this->userService->loadUserByLogin('admin'));

        $productCode = 'product_code_control_unit_0';
        $orderIdentifier = '4315bc58-1e96-4f21-82a0-15f736cbc4bc';
        $currencyCode = 'EUR';

        $output->writeln('Product data:');
        $product = $this->productService->getProduct($productCode);
        $currency = $this->currencyService->getCurrencyByCode($currencyCode);

        $basePrice = $this->productPriceService->getPriceByProductAndCurrency($product, $currency);
        $resolvedPrice = $this->priceResolver->resolvePrice($product, new PriceContext($currency));

        if ($resolvedPrice === null) {
            throw new Exception('Could not resolve price for the product');
        }

        $output->writeln(sprintf('Base price: %s', $this->formatPrice($basePrice->getMoney())));
        $output->writeln(sprintf('Discounted price: %s', $this->formatPrice($resolvedPrice->getMoney())));

        if ($resolvedPrice instanceof PriceEnvelopeInterface) {
            /** @var \Ibexa\Discounts\Value\Price\Stamp\DiscountStamp $discountStamp */
            foreach ($resolvedPrice->all(DiscountStamp::class) as $discountStamp) {
                $output->writeln(
                    sprintf(
                        'Discount applied: %s , new amount: %s',
                        $discountStamp->getDiscount()->getName(),
                        $this->formatPrice(
                            $discountStamp->getNewPrice()
                        )
                    )
                );
            }
        }

        $output->writeln('Order details:');

        $order = $this->orderService->getOrderByIdentifier($orderIdentifier);
        foreach ($order->getItems() as $item) {
            /** @var ?DiscountsData $discountData */
            $discountData = $item->getContext()['discount_data'] ?? null;
            if ($discountData instanceof DiscountsData) {
                $output->writeln(
                    sprintf(
                        'Product bought with discount: %s, base price: %s, discounted price: %s',
                        $item->getProduct()->getName(),
                        $this->formatPrice($discountData->getOriginalPrice()),
                        $this->formatPrice(
                            $item->getValue()->getUnitPriceGross()
                        )
                    )
                );
            } else {
                $output->writeln(
                    sprintf(
                        'Product bought with original price: %s, price: %s',
                        $item->getProduct()->getName(),
                        $this->formatPrice(
                            $item->getValue()->getUnitPriceGross()
                        )
                    )
                );
            }
        }

        return Command::SUCCESS;
    }

    private function formatPrice(Money $money): string
    {
        return $money->getAmount() / 100.0 . ' ' . $money->getCurrency()->getCode();
    }
}