Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions code_samples/discounts/config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:

services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'

# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

App\Discounts\CustomDiscountValueFormatter:
tags:
- name: ibexa.discounts.value.formatter
rule_type: 'referral_rule'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Discounts;

use Ibexa\Contracts\Discounts\DiscountValueFormatterInterface;
use Ibexa\Contracts\Discounts\Value\DiscountInterface;
use Money\Money;

final class PurchaseParityValueFormatter implements DiscountValueFormatterInterface
{
public function format(DiscountInterface $discount, ?Money $money = null): string

Check failure on line 13 in code_samples/discounts/src/Discounts/PurchaseParityValueFormatter.php

View workflow job for this annotation

GitHub Actions / Validate code samples (8.3)

Parameter #1 $discount (Ibexa\Contracts\Discounts\Value\DiscountInterface) of method App\Discounts\PurchaseParityValueFormatter::format() is not contravariant with parameter #1 $discountRule (Ibexa\Contracts\Discounts\Value\DiscountRuleInterface) of method Ibexa\Contracts\Discounts\DiscountValueFormatterInterface::format().

Check failure on line 13 in code_samples/discounts/src/Discounts/PurchaseParityValueFormatter.php

View workflow job for this annotation

GitHub Actions / Validate code samples (8.3)

Method App\Discounts\PurchaseParityValueFormatter::format() should return string but return statement is missing.
{
}
}
3 changes: 2 additions & 1 deletion docs/discounts/discounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ You can also extend the feature, for example, by creating custom pricing rules,
"discounts/discounts_guide",
"discounts/install_discounts",
"discounts/configure_discounts",
"discounts/discounts_api"
"discounts/discounts_api",
"discounts/extend_discounts"
], columns=2) =]]
158 changes: 158 additions & 0 deletions docs/discounts/extend_discounts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---

Check warning on line 1 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L1

[Ibexa.ReadingLevel] The grade level is 12.99. Aim for 8th grade or lower by using shorter sentences and words.
Raw output
{"message": "[Ibexa.ReadingLevel] The grade level is 12.99. Aim for 8th grade or lower by using shorter sentences and words.", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 1, "column": 1}}}, "severity": "WARNING"}
description: Extend Discounts by adding your own rules and conditions
editions:

Check warning on line 3 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L3

[Ibexa.EOLWhitespace] Remove whitespace characters from the end of the line.
Raw output
{"message": "[Ibexa.EOLWhitespace] Remove whitespace characters from the end of the line.", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 3, "column": 10}}}, "severity": "WARNING"}
- lts-update
- commerce
month_change: true
---

# Extend Discounts

By extending [Discounts](discounts_guide.md), you can increase flexibility and control over how promotions are applied to suit your unique business rules.
Together with the existing [events](event_reference.md) and the [Discounts PHP API](discounts_api.md), extending discounts gives you the ability to cover additional use cases related to selling products.

!!! tip

If you prefer learning from videos, two presentations from Ibexa Summit 2025 cover the Discounts feature:

- Konrad Oboza: [Introduction to the Discounts system in Ibexa DXP](https://www.youtube.com/watch?v=kTgtxY38srw)
- Paweł Niedzielski: [Extending new Discounts to suit your needs](https://www.youtube.com/watch?v=pDJxEKJLwPs)

## Create custom conditions

With custom [conditions](discounts_api.md#conditions) you can create more advanced discounts that apply only in specific scenarios.

The following example create discounts valid for your customers only on the anniversary of their account creation.
Having a custom condition allows you to model this scenario using a single discount, hiding all the complexity within the condition.

The logic for both the conditions and rules is specified using [Symfony's expression language](https://symfony.com/doc/current/components/expression_language.html).

### Available expressions

The following expressions are available for conditions and rules:

| Type | Name | Value | Available for |

Check warning on line 34 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L34

[Ibexa.EOLWhitespace] Remove whitespace characters from the end of the line.
Raw output
{"message": "[Ibexa.EOLWhitespace] Remove whitespace characters from the end of the line.", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 34, "column": 40}}}, "severity": "WARNING"}
| --- | --- | --- | --- |
| Function | `get_current_region()` | [Region object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-RegionInterface.html) of the current siteaccess.| Conditions, rules |

Check failure on line 36 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L36

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 36, "column": 94}}}, "severity": "ERROR"}

Check failure on line 36 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L36

[Ibexa.DomainTermCapitalization] Use 'SiteAccess' instead of 'siteaccess'
Raw output
{"message": "[Ibexa.DomainTermCapitalization] Use 'SiteAccess' instead of 'siteaccess'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 36, "column": 169}}}, "severity": "ERROR"}
| Function | `is_in_category()` | `true/false`, depending if a product belongs to given [product categories](pim_guide.md#product-categories).| Conditions, rules |
| Function | `is_user_in_customer_group()` | `true/false`, depending if an user belongs to given [customer groups](customer_groups.md). | Conditions, rules |
| Function | `calculate_purchase_amount()` | Purchase amount, calculated for all products in the cart before the discounts are applied.| Conditions, rules |
| Function | <nobr>`is_product_in_product_codes()`</nobr> | `true/false`, depending if the product is part of the given list.| Conditions, rules |
| Variable | `cart` | [Cart object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Cart-Value-CartInterface.html) associated with current context.| Conditions, rules |

Check failure on line 41 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L41

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 41, "column": 76}}}, "severity": "ERROR"}
| Variable | `currency` | [Currency object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-CurrencyInterface.html) of the current siteaccess. | Conditions, rules |

Check failure on line 42 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L42

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 42, "column": 84}}}, "severity": "ERROR"}

Check failure on line 42 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L42

[Ibexa.DomainTermCapitalization] Use 'SiteAccess' instead of 'siteaccess'
Raw output
{"message": "[Ibexa.DomainTermCapitalization] Use 'SiteAccess' instead of 'siteaccess'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 42, "column": 161}}}, "severity": "ERROR"}
| Variable | `customer_group` | [Customer group object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-CustomerGroupInterface.html) associated with given price context or the current user.| Conditions, rules |

Check failure on line 43 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L43

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 43, "column": 96}}}, "severity": "ERROR"}
| Variable | `amount` | Original price of the product | Rules |
| Variable | `product` | [Product object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-ProductInterface.html)| Rules |

Check failure on line 45 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L45

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 45, "column": 82}}}, "severity": "ERROR"}

### Custom expressions

You can create your own variables and functions to make creating the conditions easier.
To create the condition checking the registration date, the following example uses an additional variable and a function:

- `current_user`, a variable with the current [User object](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-User-User.html)

Check failure on line 52 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L52

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 52, "column": 100}}}, "severity": "ERROR"}

To add it, create a class implementing the [`DiscountVariablesResolverInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountVariablesResolverInterface.html):

Check failure on line 54 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L54

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 54, "column": 122}}}, "severity": "ERROR"}

``` php
todo: verify
```

And mark it as a service using the `ibexa.discounts.expression_language.variable_resolver` service tag:

``` yaml
todo: verify
```

- `is_anniversary()`, a function returning a boolean value indicating if the two dates passed as arguments fall on the same day.

Check failure on line 66 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L66

[Ibexa.Lists] Do not put fullstops at the end of bullets
Raw output
{"message": "[Ibexa.Lists] Do not put fullstops at the end of bullets", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 66, "column": 128}}}, "severity": "ERROR"}

``` php
todo: verify
```

Mark it as a service using the `ibexa.discounts.expression_language.function` service tag and specify the function name in the service definition.

``` yaml
todo: verify
```

Two new expressions are now available for use in custom conditions and rules.

### Implement custom condition

Now, create the condition by creating a class implementing the [`DiscountConditionInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-DiscountConditionInterface.html).

Check failure on line 82 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L82

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 82, "column": 134}}}, "severity": "ERROR"}

``` php
todo: verify
```

The expression can evaluate to `true` or `false` depending on the custom expressions values.
An additional variable, `date`, is defined to store the current date for comparison.

For each condition class you must create a dedicated condition factory, a class implementing the `\Ibexa\Discounts\Repository\DiscountCondition\DiscountConditionFactoryInterface` inteface.

This allows you to create conditions when working in the context of the Symfony service container.

``` php
todo
```

Mark it as a service using the `ibexa.discounts.condition.factory` service tag and specify the condition's identifier.

``` yaml
todo
```

To learn how to integrate the custom conditions into the back office, see [Extend Discounts wizard](extend_discounts_wizard.md).

## Create custom rules

To implement a custom rule, create a class implementing the [`DiscountRuleInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-Value-DiscountRuleInterface.html).

Check failure on line 109 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L109

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 109, "column": 126}}}, "severity": "ERROR"}

The following example implements a [purchasing power parity](https://en.wikipedia.org/wiki/Purchasing_power_parity) discount, adjusting product's price in the cart based on buyer's region.

``` php
todo
```

As with conditions, create a dedicated rule factory.

``` php
todo
```

Mark it as a service using the `ibexa.discounts.condition.factory` service tag and specify the rule's type.

``` yaml
todo
```

To learn how to integrate the custom rules into the back office, see [Extend Discounts wizard](extend_discounts_wizard.md).

### Custom discount formatting

You can adjust how each discount type is displayed when using the [`ibexa_discounts_render_discount_badge` Twig function](discounts_twig_functions.md#ibexa_discounts_render_discount_badge) by implementing a custom formatter.

To do it, create a class implementing the [`DiscountValueFormatterInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountValueFormatterInterface.html) and use the `ibexa.discounts.value.formatter` service tag:

Check failure on line 135 in docs/discounts/extend_discounts.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts.md#L135

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts.md", "range": {"start": {"line": 135, "column": 118}}}, "severity": "ERROR"}

``` php
todo
```

``` yaml
todo
```

## Change discount priority

You can change the [the defualt discount priority](discounts_guide.md#discounts-priority) by creating a class implementing the [`DiscountPrioritizationStrategyInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountPrioritizationStrategyInterface.html) and aliasing to it the default implementation.

The example below decorates the default implementation to prioritize recently created discounts above all the others.

``` php
todo
```

``` yaml
todo
```

19 changes: 19 additions & 0 deletions docs/discounts/extend_discounts_wizard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---

Check warning on line 1 in docs/discounts/extend_discounts_wizard.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts_wizard.md#L1

[Ibexa.ReadingLevel] The grade level is 15.67. Aim for 8th grade or lower by using shorter sentences and words.
Raw output
{"message": "[Ibexa.ReadingLevel] The grade level is 15.67. Aim for 8th grade or lower by using shorter sentences and words.", "location": {"path": "docs/discounts/extend_discounts_wizard.md", "range": {"start": {"line": 1, "column": 1}}}, "severity": "WARNING"}
description: Integrate custom rules and conditions into the back office forms.
editions:

Check warning on line 3 in docs/discounts/extend_discounts_wizard.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts_wizard.md#L3

[Ibexa.EOLWhitespace] Remove whitespace characters from the end of the line.
Raw output
{"message": "[Ibexa.EOLWhitespace] Remove whitespace characters from the end of the line.", "location": {"path": "docs/discounts/extend_discounts_wizard.md", "range": {"start": {"line": 3, "column": 10}}}, "severity": "WARNING"}
- lts-update
- commerce
month_change: true
---

## Extend Discounts wizard

To allow using your [custom conditions and rules](extend_discounts.md#create-custom-conditions) by the store managers, you need to integrate them into the back office discounts creation form.

The [`DiscountFormMapperInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Discounts-DiscountFormMapperInterface.html) is the service responsible for translating the form data into structures used by the PHP API.

Check failure on line 13 in docs/discounts/extend_discounts_wizard.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/discounts/extend_discounts_wizard.md#L13

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/discounts/extend_discounts_wizard.md", "range": {"start": {"line": 13, "column": 76}}}, "severity": "ERROR"}

The form uses a data driver approach, where the mapper provides all the data to the form and the form adjusts and created the fields as neccessary.

### Condition

### Rules
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ nav:
- Install Discounts: discounts/install_discounts.md
- Customize Discounts: discounts/configure_discounts.md
- Discounts API: discounts/discounts_api.md
- Extend Discounts: discounts/extend_discounts.md
- Customer management:
- Customer Portal: customer_management/customer_portal.md
- Customer Portal guide: customer_management/customer_portal_guide.md
Expand Down
Loading