Skip to content

[Discounts] Described Discounts API #2783

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: discounts-rest
Choose a base branch
from
Open

Conversation

mnocon
Copy link
Contributor

@mnocon mnocon commented Jun 10, 2025

This PR describes how you can use the Discounts PHP API and the core concepts needed to work with it.

Previews:

@mnocon mnocon changed the base branch from discounts to discounts-rest June 10, 2025 22:11
Copy link

@mnocon mnocon marked this pull request as ready for review June 11, 2025 06:41
@mnocon mnocon requested review from Steveb-p and konradoboza June 11, 2025 06:41
new IsInRegions(['germany', 'france']),
new IsProductInArray(['product-1', 'product-2']),
new IsInCurrency('EUR'),
new IsValidDiscountCode($discountCode->getCode(), $discountCode->getUsedLimit()),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I'm not sure about the $discountCode->getUsedLimit() part - which value should be passed here, the current usage or the max usage?

If I understand correctly, the condition calls the resolver which takes a single argument - so the second argument to IsValidDiscountCode is actually not used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe @Steveb-p can clarify, but to me it's the max usage.

$discountCode = $this->discountCodeService->createDiscountCode($discountCodeCreateStruct);

$discountCreateStruct = new DiscountCreateStruct();
$discountCreateStruct->setIdentifier('discount_identifier')
Copy link
Contributor Author

@mnocon mnocon Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've chosen the fluent API usage over the constructor, as this is self-documenting for the code sample - I'd just use the constructor in "standard" code.

I hope it will be clear for the readers that they can just use the constructor.

## Search

You can search for Discounts using the [`DiscountServiceInterface::findDiscounts()](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountServiceInterface.html#method_findDiscounts) method.
To learn more about the available search options, see Discounts' Search Criteria and Sort Clauses.

This comment was marked as outdated.

Copy link
Contributor

@konradoboza konradoboza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very solid overall 💪 A few remarks from my end:

new IsInRegions(['germany', 'france']),
new IsProductInArray(['product-1', 'product-2']),
new IsInCurrency('EUR'),
new IsValidDiscountCode($discountCode->getCode(), $discountCode->getUsedLimit()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe @Steveb-p can clarify, but to me it's the max usage.

@mnocon
Copy link
Contributor Author

mnocon commented Jun 11, 2025

@konradoboza thank you for your suggestions, I've applied them in e141ad2

Please note that I've added a small note on Discount Statuses.

@mnocon mnocon requested a review from konradoboza June 11, 2025 08:15
@mnocon mnocon requested a review from a team June 11, 2025 08:26
@ezrobot ezrobot requested review from adriendupuis, dabrt and julitafalcondusza and removed request for a team June 11, 2025 08:26
Copy link

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/discounts/src/Command/ManageDiscountsCommand.php


code_samples/discounts/src/Command/ManageDiscountsCommand.php

docs/discounts/discounts_api.md@117:``` php hl_lines="60-66 68-92"
docs/discounts/discounts_api.md@118:[[= include_file('code_samples/discounts/src/Command/ManageDiscountsCommand.php') =]]
docs/discounts/discounts_api.md@119:```

001⫶<?php
002⫶
003⫶declare(strict_types=1);
004⫶
005⫶namespace App\Command;
006⫶
007⫶use DateTimeImmutable;
008⫶use Ibexa\Contracts\Core\Collection\ArrayMap;
009⫶use Ibexa\Contracts\Core\Repository\PermissionResolver;
010⫶use Ibexa\Contracts\Core\Repository\UserService;
011⫶use Ibexa\Contracts\Discounts\DiscountServiceInterface;
012⫶use Ibexa\Contracts\Discounts\Value\DiscountType;
013⫶use Ibexa\Contracts\Discounts\Value\Struct\DiscountCreateStruct;
014⫶use Ibexa\Contracts\Discounts\Value\Struct\DiscountTranslationStruct;
015⫶use Ibexa\Contracts\DiscountsCodes\DiscountCodeServiceInterface;
016⫶use Ibexa\Contracts\DiscountsCodes\Value\Struct\DiscountCodeCreateStruct;
017⫶use Ibexa\Discounts\Value\DiscountCondition\IsInCurrency;
018⫶use Ibexa\Discounts\Value\DiscountCondition\IsInRegions;
019⫶use Ibexa\Discounts\Value\DiscountCondition\IsProductInArray;
020⫶use Ibexa\Discounts\Value\DiscountRule\FixedAmount;
021⫶use Ibexa\DiscountsCodes\Value\DiscountCondition\IsValidDiscountCode;
022⫶use Symfony\Component\Console\Command\Command;
023⫶use Symfony\Component\Console\Input\InputInterface;
024⫶use Symfony\Component\Console\Output\OutputInterface;
025⫶
026⫶final class ManageDiscountsCommand extends Command
027⫶{
028⫶ protected static $defaultName = 'discounts:manage';
029⫶
030⫶ private DiscountServiceInterface $discountService;
031⫶
032⫶ private DiscountCodeServiceInterface $discountCodeService;
033⫶
034⫶ private PermissionResolver $permissionResolver;
035⫶
036⫶ private UserService $userService;
037⫶
038⫶ public function __construct(
039⫶ UserService $userSerice,
040⫶ PermissionResolver $permissionResolver,
041⫶ DiscountServiceInterface $discountService,
042⫶ DiscountCodeServiceInterface $discountCodeService
043⫶ ) {
044⫶ $this->userService = $userSerice;
045⫶ $this->discountService = $discountService;
046⫶ $this->discountCodeService = $discountCodeService;
047⫶ $this->permissionResolver = $permissionResolver;
048⫶
049⫶ parent::__construct();
050⫶ }
051⫶
052⫶ protected function execute(InputInterface $input, OutputInterface $output): int
053⫶ {
054⫶ $this->permissionResolver->setCurrentUserReference(
055⫶ $this->userService->loadUserByLogin('admin')
056⫶ );
057⫶
058⫶ $now = new DateTimeImmutable();
059⫶
060❇️ $discountCodeCreateStruct = new DiscountCodeCreateStruct(
061❇️ 'summer10',
062❇️ null, // Unlimited usage
063❇️ $this->permissionResolver->getCurrentUserReference()->getUserId(),
064❇️ $now
065❇️ );
066❇️ $discountCode = $this->discountCodeService->createDiscountCode($discountCodeCreateStruct);
067⫶
068❇️ $discountCreateStruct = new DiscountCreateStruct();
069❇️ $discountCreateStruct
070❇️ ->setIdentifier('discount_identifier')
071❇️ ->setType(DiscountType::CART)
072❇️ ->setPriority(10)
073❇️ ->setEnabled(true)
074❇️ ->setUser($this->userService->loadUserByLogin('admin'))
075❇️ ->setRule(new FixedAmount(10))
076❇️ ->setStartDate($now)
077❇️ ->setConditions([
078❇️ new IsInRegions(['germany', 'france']),
079❇️ new IsProductInArray(['product-1', 'product-2']),
080❇️ new IsInCurrency('EUR'),
081❇️ new IsValidDiscountCode($discountCode->getCode(), $discountCode->getUsedLimit()),
082❇️ ])
083❇️ ->setTranslations([
084❇️ new DiscountTranslationStruct('eng-GB', 'Discount name', 'This is a discount description', 'Promotion Label', 'Promotion Description'),
085❇️ new DiscountTranslationStruct('ger-DE', 'Discount name (German)', 'Description (German)', 'Promotion Label (German)', 'Promotion Description (German)'),
086❇️ ])
087❇️ ->setEndDate(null) // Permanent discount
088❇️ ->setCreatedAt($now)
089❇️ ->setUpdatedAt($now)
090❇️ ->setContext(new ArrayMap(['custom_context' => 'custom_value']));
091❇️
092❇️ $this->discountService->createDiscount($discountCreateStruct);
093⫶
094⫶ return Command::SUCCESS;
095⫶ }
096⫶}

Download colorized diff

Copy link
Contributor

@adriendupuis adriendupuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm lost with expression values and condition Identifiers in REST. Maybe it's only for migration, not REST?


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 local customer.

You can manage discounts using [data migrations](importing_data.md#discounts), [REST API](/api/rest_api/rest_api_reference/rest_api_reference.html#discounts), or the PHP API by using the [Ibexa\Contracts\Discounts\DiscountServiceInterface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountServiceInterface.html) service.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
You can manage discounts using [data migrations](importing_data.md#discounts), [REST API](/api/rest_api/rest_api_reference/rest_api_reference.html#discounts), or the PHP API by using the [Ibexa\Contracts\Discounts\DiscountServiceInterface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountServiceInterface.html) service.
You can manage discounts using [data migrations](importing_data.md#discounts), [REST API](/api/rest_api/rest_api_reference/rest_api_reference.html#discounts), or the PHP API by using the [`Ibexa\Contracts\Discounts\DiscountServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountServiceInterface.html) service.


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](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-DiscountType.html) class:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Discounts are applied in two places, listed in the [DiscountType](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-DiscountType.html) class:
Discounts are applied in two places, listed in the [`DiscountType`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-DiscountType.html) class:


To define when a discount activates and how the price is reduced, use rules and conditions.
They make use of the [Symfony Expression language]([[= symfony_doc=]]//components/expression_language.html).
Use the expression values provided below when using data migrations or the REST API to pass the right values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get where and how to use those expression values, especially when looking at REST.

The "Rules" table below has discount_percentage but /api/rest_api/rest_api_reference/rest_api_reference.html#discounts-create-discount use

        "rule": {
            "type": "percentage",
            "amount": 10
        },

The "Conditions" table says for is_in_category that expression value is categories but the REST example uses

        "conditions": [
            {
                "class": "Ibexa\\Discounts\\Value\\DiscountCondition\\IsInCategory",
                "parameters": [["1", "2"]]
            }
        ]

It seems a bit more understandable on migration side.
Identifiers (like is_in_currency) and expression values (like currency_code) can be found on https://ez-systems-developer-documentation--2783.com.readthedocs.build/en/2783/content_management/data_migration/importing_data/#discounts example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, what I had in mind was:
"it might be useful to know the expression values when parsing the REST responses", because they are there.

Zrzut ekranu 2025-06-13 o 11 24 49

But they are not needed when using the REST API to create.

Which one would be clearer to you - drop the REST API mention or mention that they are visible in REST responses?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I continue on this while reviewing #2780 and I see that the expression values

I don't know now how to clarify that yet.
Somehow this confusion is "REST API's fault" which can't be fixed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mnocon Lets be just straight.

Suggested change
Use the expression values provided below when using data migrations or the REST API to pass the right values.
Use the expression values provided below when using data migrations or when parsing REST API responses.

Comment on lines +43 to +44
| `Ibexa\Discounts\Value\DiscountRule\FixedAmount` | `fixed_amount` | Deducts the specified amount, for example 10 EUR, from the base price | `discount_amount` |
| `Ibexa\Discounts\Value\DiscountRule\Percentage` | `percentage` | Deducts the specified percentage, for example -10%, from the base price | `discount_percentage` |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I'll add some nobr tag to have the new lines only on Rule type and Description columns

Suggested change
| `Ibexa\Discounts\Value\DiscountRule\FixedAmount` | `fixed_amount` | Deducts the specified amount, for example 10 EUR, from the base price | `discount_amount` |
| `Ibexa\Discounts\Value\DiscountRule\Percentage` | `percentage` | Deducts the specified percentage, for example -10%, from the base price | `discount_percentage` |
| `Ibexa\Discounts\Value\DiscountRule\FixedAmount` | <nobr>`fixed_amount`</nobr> | Deducts the specified amount, for example <nobr>10 EUR</nobr>, from the base price | <nobr>`discount_amount`</nobr> |
| `Ibexa\Discounts\Value\DiscountRule\Percentage` | <nobr>`percentage`</nobr> | Deducts the specified percentage, for example -10%, from the base price | <nobr>`discount_percentage`</nobr> |

Same with the Conditions table.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I was wondering if there's anything I could do to fix this, this is great 🙇

| Promotion label | Information displayed to customers |
| Promotion description | Information displayed to customers |

Use the [DiscountTranslationStruct](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-Struct-DiscountTranslationStruct.html) to provide translations for discounts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Use the [DiscountTranslationStruct](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-Struct-DiscountTranslationStruct.html) to provide translations for discounts.
Use the [`DiscountTranslationStruct`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-Struct-DiscountTranslationStruct.html) to provide translations for discounts.

Comment on lines +65 to +67
### Priority

You can set discount priority as a number between 1 and 10 to indicate which discount should have [higher priority](discounts_guide.md#discounts-priority) when choosing the one to apply.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


## Search

You can search for Discounts using the [`DiscountServiceInterface::findDiscounts()](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountServiceInterface.html#method_findDiscounts) method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
You can search for Discounts using the [`DiscountServiceInterface::findDiscounts()](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountServiceInterface.html#method_findDiscounts) method.
You can search for Discounts using the [`DiscountServiceInterface::findDiscounts()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountServiceInterface.html#method_findDiscounts) method.


- has the highest possible priority value
- deducts 10 EUR from the base price of the product
- is permanent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- is permanent
- is [permanent](#start-and-end-date)

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

- has the highest possible priority value
- deducts 10 EUR from the base price of the product
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- deducts 10 EUR from the base price of the product
- [rule](#rules) a deduction of 10 EUR from the base price of the product

Comment on lines +113 to +115
- is valid in Germany and France
- applies to 2 products
- requires a `summer10` discount code to be activated. The code can be used unlimited number of times
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- is valid in Germany and France
- applies to 2 products
- requires a `summer10` discount code to be activated. The code can be used unlimited number of times
- [depends](#conditions) on
- being bought from Germany or France
- 2 products
- a `summer10` [discount code](#discount-codes) which can be used unlimited number of times

@@ -522,6 +522,8 @@ The provided conditions overwrite any already existing ones.
[[= include_file('code_samples/data_migration/examples/discounts/discount_update.yaml') =]]
```

For a list of available conditions, see [Discounts API](discounts_api.md).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For a list of available conditions, see [Discounts API](discounts_api.md).
For a list of available conditions, see [Discounts API](discounts_api.md#conditions).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants