This commit is contained in:
Tomasz Kaliński 2026-06-25 20:58:33 +02:00 committed by GitHub
commit 86a4678a1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1062 additions and 2 deletions

View file

@ -166,6 +166,29 @@
| `ShippingBundle\...\ShippingMethodCalculatorExists` | `invalidShippingCalculator` | `invalidShippingCalculatorMessage` |
| `ShippingBundle\...\ShippingMethodRule` | `invalidType` | `invalidTypeMessage` |
## Promotion
1. New **opt-in per-channel** promotion rule and action types have been added. They let a single
promotion rule/action hold independent configuration per channel, with the `configuration` array
keyed by channel code (e.g. `['WEB_US' => ['count' => 2], 'WEB_GB' => ['count' => 5]]`):
- Rules: `cart_quantity_per_channel`, `customer_group_per_channel`, `nth_order_per_channel`,
`shipping_country_per_channel`, `has_taxon_per_channel`, `contains_product_per_channel`.
- Actions: `order_percentage_discount_per_channel`, `shipping_percentage_discount_per_channel`.
These are **additional** types registered alongside the existing plain ones. Existing promotions
and the plain rule/action types are unchanged, and **no core service is replaced**, so the change
is fully backward compatible and requires no action.
Implementation: two generic decorators in `Sylius\Component\Core\Promotion` unwrap the current
channel's configuration slice and delegate to the standard checker/command:
- `Sylius\Component\Core\Promotion\Checker\Rule\PerChannelRuleChecker`
- `Sylius\Component\Core\Promotion\Action\PerChannelPromotionActionCommand`
The admin forms use new `ChannelBased*ConfigurationType` form types built on top of
`Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType`.
## Deprecations
1. Passing a `Sylius\Component\Core\Calculator\ProductVariantPricesCalculatorInterface` directly to the following catalog-facing classes is deprecated since Sylius 2.3.

View file

@ -0,0 +1,55 @@
@receiving_discount
Feature: Receiving discount with promotion rules and actions configured independently per channel
In order to pay the proper amount while buying promoted goods with different rules and actions per channel
As a Customer
I want to have per-channel promotion rules and actions applied correctly in my current channel
Background:
Given the store operates on a channel named "Web-US" in "USD" currency and with hostname "united.states"
And the store operates on another channel named "Web-GB" in "GBP" currency and with hostname "great.britain"
And the store classifies its products as "T-Shirts" and "Mugs"
And the store has a product "PHP T-Shirt" priced at "$100.00" in "Web-US" channel
And this product is also priced at "£80.00" in "Web-GB" channel
And the product "PHP T-Shirt" belongs to taxon "T-Shirts"
And the store has a product "Symfony T-Shirt" priced at "$60.00" in "Web-US" channel
And this product is also priced at "£40.00" in "Web-GB" channel
And there is a promotion "Holiday promotion"
And I am a logged in customer
@api @todo-ui
Scenario: Receiving an order percentage discount configured independently per channel
Given the promotion gives "10%" discount to every order in the "Web-US" channel and "5%" discount to every order in the "Web-GB" channel
When I changed my current channel to "Web-US"
And I added product "PHP T-Shirt" to the cart
And I check the details of my cart
Then my cart total should be "$90.00"
And my discount should be "-$10.00"
@api @todo-ui
Scenario: Order percentage discount applies the value configured for the current channel
Given the promotion gives "10%" discount to every order in the "Web-US" channel and "5%" discount to every order in the "Web-GB" channel
When I changed my current channel to "Web-GB"
And I added product "PHP T-Shirt" to the cart
And I check the details of my cart
Then my cart total should be "£76.00"
And my discount should be "-£4.00"
@api @todo-ui
Scenario: Receiving a discount when the cart contains the product configured for the current channel
Given the promotion gives "$10.00" discount to every order in the "Web-US" channel and "£5.00" discount to every order in the "Web-GB" channel
And the promotion applies to orders containing product "PHP T-Shirt" in the "Web-US" channel and product "Symfony T-Shirt" in the "Web-GB" channel
When I changed my current channel to "Web-US"
And I added product "PHP T-Shirt" to the cart
And I check the details of my cart
Then my cart total should be "$90.00"
And my discount should be "-$10.00"
@api @todo-ui
Scenario: Receiving a discount when the cart has a product from the taxon configured for the current channel
Given the promotion gives "$10.00" discount to every order in the "Web-US" channel and "£5.00" discount to every order in the "Web-GB" channel
And the promotion applies to orders with a product from taxon "T-Shirts" in the "Web-US" channel and from taxon "Mugs" in the "Web-GB" channel
When I changed my current channel to "Web-US"
And I added product "PHP T-Shirt" to the cart
And I check the details of my cart
Then my cart total should be "$90.00"
And my discount should be "-$10.00"

View file

@ -404,6 +404,72 @@ final readonly class PromotionContext implements Context
$this->objectManager->flush();
}
#[Given('/^([^"]+) gives ("[^"]+%") discount to every order in the ("[^"]+" channel) and ("[^"]+%") discount to every order in the ("[^"]+" channel)$/')]
public function thisPromotionGivesPercentageDiscountToEveryOrderInTheChannelAndInTheChannel(
PromotionInterface $promotion,
float $firstDiscount,
ChannelInterface $firstChannel,
float $secondDiscount,
ChannelInterface $secondChannel,
): void {
$action = $this->actionFactory->createNew();
$action->setType('order_percentage_discount_per_channel');
$action->setConfiguration([
$firstChannel->getCode() => ['percentage' => $firstDiscount],
$secondChannel->getCode() => ['percentage' => $secondDiscount],
]);
$promotion->addChannel($firstChannel);
$promotion->addChannel($secondChannel);
$promotion->addAction($action);
$this->objectManager->flush();
}
#[Given('/^(the promotion) applies to orders containing (product "[^"]+") in the ("[^"]+" channel) and (product "[^"]+") in the ("[^"]+" channel)$/')]
public function thePromotionAppliesToOrdersContainingProductInTheChannelAndInTheChannel(
PromotionInterface $promotion,
ProductInterface $firstProduct,
ChannelInterface $firstChannel,
ProductInterface $secondProduct,
ChannelInterface $secondChannel,
): void {
$rule = $this->ruleFactory->createNew();
$rule->setType('contains_product_per_channel');
$rule->setConfiguration([
$firstChannel->getCode() => ['product_code' => $firstProduct->getCode()],
$secondChannel->getCode() => ['product_code' => $secondProduct->getCode()],
]);
$promotion->addChannel($firstChannel);
$promotion->addChannel($secondChannel);
$promotion->addRule($rule);
$this->objectManager->flush();
}
#[Given('/^(the promotion) applies to orders with a product from (taxon "[^"]+") in the ("[^"]+" channel) and from (taxon "[^"]+") in the ("[^"]+" channel)$/')]
public function thePromotionAppliesToOrdersWithAProductFromTaxonInTheChannelAndInTheChannel(
PromotionInterface $promotion,
TaxonInterface $firstTaxon,
ChannelInterface $firstChannel,
TaxonInterface $secondTaxon,
ChannelInterface $secondChannel,
): void {
$rule = $this->ruleFactory->createNew();
$rule->setType('has_taxon_per_channel');
$rule->setConfiguration([
$firstChannel->getCode() => ['taxons' => [$firstTaxon->getCode()]],
$secondChannel->getCode() => ['taxons' => [$secondTaxon->getCode()]],
]);
$promotion->addChannel($firstChannel);
$promotion->addChannel($secondChannel);
$promotion->addRule($rule);
$this->objectManager->flush();
}
#[Given('/^(this promotion) gives ("(?:€|£|\$)[^"]+") off on every product in the ("[^"]+" channel) and ("(?:€|£|\$)[^"]+") off in the ("[^"]+" channel)$/')]
public function thisPromotionGivesFixedDiscountOnEveryProductInTheChannelAndInTheChannel(
PromotionInterface $promotion,

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Action;
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
use Sylius\Bundle\PromotionBundle\Form\Type\Action\PercentageDiscountConfigurationType;
use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ChannelBasedPercentageDiscountConfigurationType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => PercentageDiscountConfigurationType::class,
'entry_options' => fn (ChannelInterface $channel) => [
'label' => $channel->getName(),
],
]);
}
public function getParent(): string
{
return ChannelCollectionType::class;
}
}

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule;
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
use Sylius\Bundle\PromotionBundle\Form\Type\Rule\CartQuantityConfigurationType;
use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ChannelBasedCartQuantityConfigurationType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => CartQuantityConfigurationType::class,
'entry_options' => fn (ChannelInterface $channel) => [
'label' => $channel->getName(),
],
]);
}
public function getParent(): string
{
return ChannelCollectionType::class;
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule;
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ChannelBasedContainsProductConfigurationType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => ContainsProductConfigurationType::class,
'entry_options' => fn (ChannelInterface $channel) => [
'label' => $channel->getName(),
],
]);
}
public function getParent(): string
{
return ChannelCollectionType::class;
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule;
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ChannelBasedCustomerGroupConfigurationType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => CustomerGroupConfigurationType::class,
'entry_options' => fn (ChannelInterface $channel) => [
'label' => $channel->getName(),
],
]);
}
public function getParent(): string
{
return ChannelCollectionType::class;
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule;
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ChannelBasedHasTaxonConfigurationType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => HasTaxonConfigurationType::class,
'entry_options' => fn (ChannelInterface $channel) => [
'label' => $channel->getName(),
],
]);
}
public function getParent(): string
{
return ChannelCollectionType::class;
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule;
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ChannelBasedNthOrderConfigurationType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => NthOrderConfigurationType::class,
'entry_options' => fn (ChannelInterface $channel) => [
'label' => $channel->getName(),
],
]);
}
public function getParent(): string
{
return ChannelCollectionType::class;
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule;
use Sylius\Bundle\CoreBundle\Form\Type\ChannelCollectionType;
use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ChannelBasedShippingCountryConfigurationType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => ShippingCountryConfigurationType::class,
'entry_options' => fn (ChannelInterface $channel) => [
'label' => $channel->getName(),
],
]);
}
public function getParent(): string
{
return ChannelCollectionType::class;
}
}

View file

@ -44,9 +44,15 @@ sylius_promotion:
order_percentage_discount:
- 'sylius'
- 'sylius_promotion_action_order_percentage_discount'
order_percentage_discount_per_channel:
- 'sylius'
- 'sylius_promotion_action_order_percentage_discount_per_channel'
shipping_percentage_discount:
- 'sylius'
- 'sylius_promotion_action_shipping_percentage_discount'
shipping_percentage_discount_per_channel:
- 'sylius'
- 'sylius_promotion_action_shipping_percentage_discount_per_channel'
order_fixed_discount:
- 'sylius'
- 'sylius_promotion_action_order_fixed_discount'
@ -62,18 +68,36 @@ sylius_promotion:
customer_group:
- 'sylius'
- 'sylius_promotion_rule_customer_group'
customer_group_per_channel:
- 'sylius'
- 'sylius_promotion_rule_customer_group_per_channel'
nth_order:
- 'sylius'
- 'sylius_promotion_rule_nth_order'
nth_order_per_channel:
- 'sylius'
- 'sylius_promotion_rule_nth_order_per_channel'
shipping_country:
- 'sylius'
- 'sylius_promotion_rule_shipping_country'
shipping_country_per_channel:
- 'sylius'
- 'sylius_promotion_rule_shipping_country_per_channel'
has_taxon:
- 'sylius'
- 'sylius_promotion_rule_has_taxon'
has_taxon_per_channel:
- 'sylius'
- 'sylius_promotion_rule_has_taxon_per_channel'
total_of_items_from_taxon:
- 'sylius'
- 'sylius_promotion_rule_total_of_items_from_taxon'
contains_product:
- 'sylius'
- 'sylius_promotion_rule_contains_product'
contains_product_per_channel:
- 'sylius'
- 'sylius_promotion_rule_contains_product_per_channel'
cart_quantity_per_channel:
- 'sylius'
- 'sylius_promotion_rule_cart_quantity_per_channel'

View file

@ -15,9 +15,16 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Sylius\Bundle\CoreBundle\Doctrine\ORM\Promotion\Modifier\AtomicOrderPromotionsUsageModifier;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Action\ChannelBasedFixedDiscountConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Action\ChannelBasedPercentageDiscountConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Action\ChannelBasedUnitFixedDiscountConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Action\ChannelBasedUnitPercentageDiscountConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ChannelBasedCartQuantityConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ChannelBasedContainsProductConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ChannelBasedCustomerGroupConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ChannelBasedHasTaxonConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ChannelBasedItemTotalConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ChannelBasedNthOrderConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ChannelBasedShippingCountryConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ChannelBasedTotalOfItemsFromTaxonConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\ContainsProductConfigurationType;
use Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule\CustomerGroupConfigurationType;
@ -32,6 +39,7 @@ use Sylius\Component\Core\Factory\PromotionRuleFactory;
use Sylius\Component\Core\Factory\PromotionRuleFactoryInterface;
use Sylius\Component\Core\Promotion\Action\FixedDiscountPromotionActionCommand;
use Sylius\Component\Core\Promotion\Action\PercentageDiscountPromotionActionCommand;
use Sylius\Component\Core\Promotion\Action\PerChannelPromotionActionCommand;
use Sylius\Component\Core\Promotion\Action\ShippingPercentageDiscountPromotionActionCommand;
use Sylius\Component\Core\Promotion\Action\UnitFixedDiscountPromotionActionCommand;
use Sylius\Component\Core\Promotion\Action\UnitPercentageDiscountPromotionActionCommand;
@ -46,6 +54,7 @@ use Sylius\Component\Core\Promotion\Checker\Rule\CustomerGroupRuleChecker;
use Sylius\Component\Core\Promotion\Checker\Rule\HasTaxonRuleChecker;
use Sylius\Component\Core\Promotion\Checker\Rule\ItemTotalRuleChecker;
use Sylius\Component\Core\Promotion\Checker\Rule\NthOrderRuleChecker;
use Sylius\Component\Core\Promotion\Checker\Rule\PerChannelRuleChecker;
use Sylius\Component\Core\Promotion\Checker\Rule\ShippingCountryRuleChecker;
use Sylius\Component\Core\Promotion\Checker\Rule\TotalOfItemsFromTaxonRuleChecker;
use Sylius\Component\Core\Promotion\Checker\TaxonInPromotionRuleChecker;
@ -85,23 +94,47 @@ return static function (ContainerConfigurator $container) {
->tag('sylius.promotion_rule_checker', ['type' => 'customer_group', 'label' => 'sylius.form.promotion_rule.customer_group', 'form_type' => CustomerGroupConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.customer_group_per_channel', PerChannelRuleChecker::class)
->args([service('sylius.checker.promotion_rule.customer_group')])
->tag('sylius.promotion_rule_checker', ['type' => 'customer_group_per_channel', 'label' => 'sylius.form.promotion_rule.customer_group_per_channel', 'form_type' => ChannelBasedCustomerGroupConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.nth_order', NthOrderRuleChecker::class)
->args([service('sylius.repository.order')])
->tag('sylius.promotion_rule_checker', ['type' => 'nth_order', 'label' => 'sylius.form.promotion_rule.nth_order', 'form_type' => NthOrderConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.nth_order_per_channel', PerChannelRuleChecker::class)
->args([service('sylius.checker.promotion_rule.nth_order')])
->tag('sylius.promotion_rule_checker', ['type' => 'nth_order_per_channel', 'label' => 'sylius.form.promotion_rule.nth_order_per_channel', 'form_type' => ChannelBasedNthOrderConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.shipping_country', ShippingCountryRuleChecker::class)
->args([service('sylius.repository.country')])
->tag('sylius.promotion_rule_checker', ['type' => 'shipping_country', 'label' => 'sylius.form.promotion_rule.shipping_country', 'form_type' => ShippingCountryConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.shipping_country_per_channel', PerChannelRuleChecker::class)
->args([service('sylius.checker.promotion_rule.shipping_country')])
->tag('sylius.promotion_rule_checker', ['type' => 'shipping_country_per_channel', 'label' => 'sylius.form.promotion_rule.shipping_country_per_channel', 'form_type' => ChannelBasedShippingCountryConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.has_taxon', HasTaxonRuleChecker::class)
->tag('sylius.promotion_rule_checker', ['type' => 'has_taxon', 'label' => 'sylius.form.promotion_rule.has_at_least_one_from_taxons', 'form_type' => HasTaxonConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.has_taxon_per_channel', PerChannelRuleChecker::class)
->args([service('sylius.checker.promotion_rule.has_taxon')])
->tag('sylius.promotion_rule_checker', ['type' => 'has_taxon_per_channel', 'label' => 'sylius.form.promotion_rule.has_at_least_one_from_taxons_per_channel', 'form_type' => ChannelBasedHasTaxonConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.total_of_items_from_taxon', TotalOfItemsFromTaxonRuleChecker::class)
->args([service('sylius.repository.taxon')])
@ -113,6 +146,12 @@ return static function (ContainerConfigurator $container) {
->tag('sylius.promotion_rule_checker', ['type' => 'contains_product', 'label' => 'sylius.form.promotion_rule.contains_product', 'form_type' => ContainsProductConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.contains_product_per_channel', PerChannelRuleChecker::class)
->args([service('sylius.checker.promotion_rule.contains_product')])
->tag('sylius.promotion_rule_checker', ['type' => 'contains_product_per_channel', 'label' => 'sylius.form.promotion_rule.contains_product_per_channel', 'form_type' => ChannelBasedContainsProductConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.item_total', ItemTotalRuleChecker::class)
->tag('sylius.promotion_rule_checker', ['type' => 'item_total', 'label' => 'sylius.form.promotion_rule.item_total', 'form_type' => ChannelBasedItemTotalConfigurationType::class])
@ -123,6 +162,12 @@ return static function (ContainerConfigurator $container) {
->tag('sylius.promotion_rule_checker', ['type' => 'cart_quantity', 'label' => 'sylius.form.promotion_rule.cart_quantity', 'form_type' => CartQuantityConfigurationType::class])
;
$services
->set('sylius.checker.promotion_rule.cart_quantity_per_channel', PerChannelRuleChecker::class)
->args([service('sylius.checker.promotion_rule.cart_quantity')])
->tag('sylius.promotion_rule_checker', ['type' => 'cart_quantity_per_channel', 'label' => 'sylius.form.promotion_rule.cart_quantity_per_channel', 'form_type' => ChannelBasedCartQuantityConfigurationType::class])
;
$services
->set('sylius.command.promotion_action.fixed_discount', FixedDiscountPromotionActionCommand::class)
->args([
@ -156,6 +201,12 @@ return static function (ContainerConfigurator $container) {
->tag('sylius.promotion_action', ['type' => 'order_percentage_discount', 'label' => 'sylius.form.promotion_action.order_percentage_discount', 'form_type' => PercentageDiscountConfigurationType::class])
;
$services
->set('sylius.command.promotion_action.percentage_discount_per_channel', PerChannelPromotionActionCommand::class)
->args([service('sylius.command.promotion_action.percentage_discount')])
->tag('sylius.promotion_action', ['type' => 'order_percentage_discount_per_channel', 'label' => 'sylius.form.promotion_action.order_percentage_discount_per_channel', 'form_type' => ChannelBasedPercentageDiscountConfigurationType::class])
;
$services
->set('sylius.command.promotion_action.unit_percentage_discount', UnitPercentageDiscountPromotionActionCommand::class)
->args([
@ -173,6 +224,12 @@ return static function (ContainerConfigurator $container) {
->tag('sylius.promotion_action', ['type' => 'shipping_percentage_discount', 'label' => 'sylius.form.promotion_action.shipping_percentage_discount', 'form_type' => PercentageDiscountConfigurationType::class])
;
$services
->set('sylius.command.promotion_action.shipping_percentage_discount_per_channel', PerChannelPromotionActionCommand::class)
->args([service('sylius.command.promotion_action.shipping_percentage_discount')])
->tag('sylius.promotion_action', ['type' => 'shipping_percentage_discount_per_channel', 'label' => 'sylius.form.promotion_action.shipping_percentage_discount_per_channel', 'form_type' => ChannelBasedPercentageDiscountConfigurationType::class])
;
$services
->set('sylius.checker.promotion.promotion_coupon_per_customer_usage_limit_eligibility', PromotionCouponPerCustomerUsageLimitEligibilityChecker::class)
->args([service('sylius.repository.order')])

View file

@ -280,6 +280,46 @@
</constraint>
</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelCodeCollection">
<option name="allowExtraFields">true</option>
<option name="groups">
<value>sylius_promotion_action_order_percentage_discount_per_channel</value>
<value>sylius_promotion_action_shipping_percentage_discount_per_channel</value>
</option>
<option name="validateAgainstAllChannels">true</option>
<option name="channelAwarePropertyPath">promotion</option>
<option name="constraints">
<constraint name="Collection">
<option name="allowExtraFields">true</option>
<option name="fields">
<value key="percentage">
<constraint name="NotBlank">
<option name="groups">
<value>sylius_promotion_action_order_percentage_discount_per_channel</value>
<value>sylius_promotion_action_shipping_percentage_discount_per_channel</value>
</option>
</constraint>
<constraint name="Type">
<option name="type">numeric</option>
<option name="groups">
<value>sylius_promotion_action_order_percentage_discount_per_channel</value>
<value>sylius_promotion_action_shipping_percentage_discount_per_channel</value>
</option>
</constraint>
<constraint name="Range">
<option name="min">0</option>
<option name="max">1</option>
<option name="notInRangeMessage">sylius.promotion_action.percentage_discount_configuration.not_in_range</option>
<option name="groups">
<value>sylius_promotion_action_order_percentage_discount_per_channel</value>
<value>sylius_promotion_action_shipping_percentage_discount_per_channel</value>
</option>
</constraint>
</value>
</option>
</constraint>
</option>
</constraint>
</property>
</class>
</constraint-mapping>

View file

@ -178,6 +178,163 @@
</constraint>
</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelCodeCollection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_cart_quantity_per_channel</option>
<option name="validateAgainstAllChannels">true</option>
<option name="channelAwarePropertyPath">promotion</option>
<option name="constraints">
<constraint name="Collection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_cart_quantity_per_channel</option>
<option name="fields">
<value key="count">
<constraint name="NotBlank">
<option name="groups">sylius_promotion_rule_cart_quantity_per_channel</option>
</constraint>
<constraint name="Type">
<option name="type">numeric</option>
<option name="groups">sylius_promotion_rule_cart_quantity_per_channel</option>
</constraint>
</value>
</option>
</constraint>
</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelCodeCollection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_customer_group_per_channel</option>
<option name="validateAgainstAllChannels">true</option>
<option name="channelAwarePropertyPath">promotion</option>
<option name="constraints">
<constraint name="Collection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_customer_group_per_channel</option>
<option name="fields">
<value key="group_code">
<constraint name="NotBlank">
<option name="groups">sylius_promotion_rule_customer_group_per_channel</option>
</constraint>
<constraint name="Type">
<option name="type">string</option>
<option name="groups">sylius_promotion_rule_customer_group_per_channel</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\CustomerGroupCodeExists">
<option name="groups">sylius_promotion_rule_customer_group_per_channel</option>
</constraint>
</value>
</option>
</constraint>
</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelCodeCollection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_nth_order_per_channel</option>
<option name="validateAgainstAllChannels">true</option>
<option name="channelAwarePropertyPath">promotion</option>
<option name="constraints">
<constraint name="Collection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_nth_order_per_channel</option>
<option name="fields">
<value key="nth">
<constraint name="NotBlank">
<option name="groups">sylius_promotion_rule_nth_order_per_channel</option>
</constraint>
<constraint name="Type">
<option name="type">numeric</option>
<option name="groups">sylius_promotion_rule_nth_order_per_channel</option>
</constraint>
</value>
</option>
</constraint>
</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelCodeCollection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_shipping_country_per_channel</option>
<option name="validateAgainstAllChannels">true</option>
<option name="channelAwarePropertyPath">promotion</option>
<option name="constraints">
<constraint name="Collection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_shipping_country_per_channel</option>
<option name="fields">
<value key="country">
<constraint name="NotBlank">
<option name="groups">sylius_promotion_rule_shipping_country_per_channel</option>
</constraint>
<constraint name="Type">
<option name="type">string</option>
<option name="groups">sylius_promotion_rule_shipping_country_per_channel</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\CountryCodeExists">
<option name="groups">sylius_promotion_rule_shipping_country_per_channel</option>
</constraint>
</value>
</option>
</constraint>
</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelCodeCollection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_has_taxon_per_channel</option>
<option name="validateAgainstAllChannels">true</option>
<option name="channelAwarePropertyPath">promotion</option>
<option name="constraints">
<constraint name="Collection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_has_taxon_per_channel</option>
<option name="fields">
<value key="taxons">
<constraint name="All">
<option name="groups">sylius_promotion_rule_has_taxon_per_channel</option>
<option name="constraints">
<constraint name="NotBlank">
<option name="groups">sylius_promotion_rule_has_taxon_per_channel</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\TaxonCodeExists">
<option name="groups">sylius_promotion_rule_has_taxon_per_channel</option>
</constraint>
</option>
</constraint>
</value>
</option>
</constraint>
</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\ChannelCodeCollection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_contains_product_per_channel</option>
<option name="validateAgainstAllChannels">true</option>
<option name="channelAwarePropertyPath">promotion</option>
<option name="constraints">
<constraint name="Collection">
<option name="allowExtraFields">true</option>
<option name="groups">sylius_promotion_rule_contains_product_per_channel</option>
<option name="fields">
<value key="product_code">
<constraint name="NotBlank">
<option name="groups">sylius_promotion_rule_contains_product_per_channel</option>
</constraint>
<constraint name="Type">
<option name="type">string</option>
<option name="groups">sylius_promotion_rule_contains_product_per_channel</option>
</constraint>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\ProductCodeExists">
<option name="groups">sylius_promotion_rule_contains_product_per_channel</option>
</constraint>
</value>
</option>
</constraint>
</option>
</constraint>
</property>
</class>
</constraint-mapping>

View file

@ -35,7 +35,9 @@ sylius:
item_percentage_discount: Item percentage discount
order_fixed_discount: Order fixed discount
order_percentage_discount: Order percentage discount
order_percentage_discount_per_channel: Order percentage discount (per channel)
shipping_percentage_discount: Shipping percentage discount
shipping_percentage_discount_per_channel: Shipping percentage discount (per channel)
filters: Filters
fixed_discount_configuration:
amount: Amount
@ -56,12 +58,18 @@ sylius:
price_range: Price range filter
promotion_rule:
cart_quantity: Cart quantity
cart_quantity_per_channel: Cart quantity (per channel)
contains_product: Contains product
contains_product_per_channel: Contains product (per channel)
customer_group: Customer group
customer_group_per_channel: Customer group (per channel)
has_at_least_one_from_taxons: Has at least one from taxons
has_at_least_one_from_taxons_per_channel: Has at least one from taxons (per channel)
item_total: Item total
nth_order: Nth order
nth_order_per_channel: Nth order (per channel)
shipping_country: Shipping country
shipping_country_per_channel: Shipping country (per channel)
taxonomy: Taxonomy
total_price_of_items_from_taxon: Total price of items from taxon
cart_quantity_configuration:

View file

@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Component\Core\Promotion\Action;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Promotion\Action\PromotionActionCommandInterface;
use Sylius\Component\Promotion\Model\PromotionInterface;
use Sylius\Component\Promotion\Model\PromotionSubjectInterface;
use Sylius\Resource\Exception\UnexpectedTypeException;
final class PerChannelPromotionActionCommand implements PromotionActionCommandInterface
{
public function __construct(private PromotionActionCommandInterface $decorated)
{
}
/** @param array<string, mixed> $configuration */
public function execute(PromotionSubjectInterface $subject, array $configuration, PromotionInterface $promotion): bool
{
if (!$subject instanceof OrderInterface) {
throw new UnexpectedTypeException($subject, OrderInterface::class);
}
$channel = $subject->getChannel();
if ($channel === null) {
return false;
}
$channelCode = $channel->getCode();
if (!isset($configuration[$channelCode]) || !is_array($configuration[$channelCode])) {
return false;
}
return $this->decorated->execute($subject, $configuration[$channelCode], $promotion);
}
/** @param array<string, mixed> $configuration */
public function revert(PromotionSubjectInterface $subject, array $configuration, PromotionInterface $promotion): void
{
if (!$subject instanceof OrderInterface) {
throw new UnexpectedTypeException($subject, OrderInterface::class);
}
$channel = $subject->getChannel();
if ($channel === null) {
return;
}
$channelCode = $channel->getCode();
if (!isset($configuration[$channelCode]) || !is_array($configuration[$channelCode])) {
return;
}
$this->decorated->revert($subject, $configuration[$channelCode], $promotion);
}
}

View file

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Component\Core\Promotion\Checker\Rule;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Promotion\Checker\Rule\RuleCheckerInterface;
use Sylius\Component\Promotion\Exception\UnsupportedTypeException;
use Sylius\Component\Promotion\Model\PromotionSubjectInterface;
final class PerChannelRuleChecker implements RuleCheckerInterface
{
public function __construct(private RuleCheckerInterface $decorated)
{
}
/**
* @param array<string, mixed> $configuration
*
* @throws UnsupportedTypeException
*/
public function isEligible(PromotionSubjectInterface $subject, array $configuration): bool
{
if (!$subject instanceof OrderInterface) {
throw new UnsupportedTypeException($subject, OrderInterface::class);
}
$channel = $subject->getChannel();
if ($channel === null) {
return false;
}
$channelCode = $channel->getCode();
if (!isset($configuration[$channelCode]) || !is_array($configuration[$channelCode])) {
return false;
}
return $this->decorated->isEligible($subject, $configuration[$channelCode]);
}
}

View file

@ -0,0 +1,144 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Tests\Sylius\Component\Core\Promotion\Action;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Promotion\Action\PerChannelPromotionActionCommand;
use Sylius\Component\Promotion\Action\PromotionActionCommandInterface;
use Sylius\Component\Promotion\Model\PromotionInterface;
use Sylius\Component\Promotion\Model\PromotionSubjectInterface;
use Sylius\Resource\Exception\UnexpectedTypeException;
#[CoversClass(PerChannelPromotionActionCommand::class)]
final class PerChannelPromotionActionCommandTest extends TestCase
{
private MockObject&PromotionActionCommandInterface $decorated;
private ChannelInterface&MockObject $channel;
private MockObject&OrderInterface $order;
private MockObject&PromotionInterface $promotion;
private PerChannelPromotionActionCommand $command;
protected function setUp(): void
{
$this->decorated = $this->createMock(PromotionActionCommandInterface::class);
$this->channel = $this->createMock(ChannelInterface::class);
$this->order = $this->createMock(OrderInterface::class);
$this->promotion = $this->createMock(PromotionInterface::class);
$this->command = new PerChannelPromotionActionCommand($this->decorated);
}
public function testShouldImplementPromotionActionCommandInterface(): void
{
$this->assertInstanceOf(PromotionActionCommandInterface::class, $this->command);
}
public function testShouldExecuteDecoratedCommandWithTheChannelConfiguration(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->once())
->method('execute')
->with($this->order, ['percentage' => 0.2], $this->promotion)
->willReturn(true)
;
$this->assertTrue($this->command->execute($this->order, ['WEB_US' => ['percentage' => 0.2]], $this->promotion));
}
public function testShouldNotExecuteDecoratedCommandIfThereIsNoConfigurationForTheOrderChannel(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->never())->method('execute');
$this->assertFalse($this->command->execute($this->order, ['WEB_EU' => ['percentage' => 0.2]], $this->promotion));
}
public function testShouldNotExecuteDecoratedCommandWhenOrderHasNoChannel(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn(null);
$this->decorated->expects($this->never())->method('execute');
$this->assertFalse($this->command->execute($this->order, ['WEB_US' => ['percentage' => 0.2]], $this->promotion));
}
public function testShouldNotExecuteDecoratedCommandWhenChannelConfigurationIsNotAnArray(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->never())->method('execute');
$this->assertFalse($this->command->execute($this->order, ['WEB_US' => 'not-an-array'], $this->promotion));
}
public function testShouldThrowExceptionWhenExecutingIfPromotionSubjectIsNotOrder(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->command->execute($this->createMock(PromotionSubjectInterface::class), [], $this->promotion);
}
public function testShouldRevertDecoratedCommandWithTheChannelConfiguration(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->once())
->method('revert')
->with($this->order, ['percentage' => 0.2], $this->promotion)
;
$this->command->revert($this->order, ['WEB_US' => ['percentage' => 0.2]], $this->promotion);
}
public function testShouldNotRevertDecoratedCommandIfThereIsNoConfigurationForTheOrderChannel(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->never())->method('revert');
$this->command->revert($this->order, ['WEB_EU' => ['percentage' => 0.2]], $this->promotion);
}
public function testShouldNotRevertDecoratedCommandWhenOrderHasNoChannel(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn(null);
$this->decorated->expects($this->never())->method('revert');
$this->command->revert($this->order, ['WEB_US' => ['percentage' => 0.2]], $this->promotion);
}
public function testShouldNotRevertDecoratedCommandWhenChannelConfigurationIsNotAnArray(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->never())->method('revert');
$this->command->revert($this->order, ['WEB_US' => 'not-an-array'], $this->promotion);
}
public function testShouldThrowExceptionWhenRevertingIfPromotionSubjectIsNotOrder(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->command->revert($this->createMock(PromotionSubjectInterface::class), [], $this->promotion);
}
}

View file

@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Tests\Sylius\Component\Core\Promotion\Checker\Rule;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Promotion\Checker\Rule\PerChannelRuleChecker;
use Sylius\Component\Promotion\Checker\Rule\RuleCheckerInterface;
use Sylius\Component\Promotion\Exception\UnsupportedTypeException;
use Sylius\Component\Promotion\Model\PromotionSubjectInterface;
#[CoversClass(PerChannelRuleChecker::class)]
final class PerChannelRuleCheckerTest extends TestCase
{
private MockObject&RuleCheckerInterface $decorated;
private ChannelInterface&MockObject $channel;
private MockObject&OrderInterface $order;
private PerChannelRuleChecker $ruleChecker;
protected function setUp(): void
{
$this->decorated = $this->createMock(RuleCheckerInterface::class);
$this->channel = $this->createMock(ChannelInterface::class);
$this->order = $this->createMock(OrderInterface::class);
$this->ruleChecker = new PerChannelRuleChecker($this->decorated);
}
public function testShouldImplementRuleCheckerInterface(): void
{
$this->assertInstanceOf(RuleCheckerInterface::class, $this->ruleChecker);
}
public function testShouldDelegateToDecoratedCheckerWithTheChannelConfiguration(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->once())
->method('isEligible')
->with($this->order, ['nth' => 5])
->willReturn(true)
;
$this->assertTrue($this->ruleChecker->isEligible($this->order, ['WEB_US' => ['nth' => 5]]));
}
public function testShouldReturnDecoratedCheckerResultForTheOrderChannel(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->once())
->method('isEligible')
->with($this->order, ['nth' => 5])
->willReturn(false)
;
$this->assertFalse($this->ruleChecker->isEligible($this->order, ['WEB_US' => ['nth' => 5]]));
}
public function testShouldReturnFalseAndNotDelegateIfThereIsNoConfigurationForTheOrderChannel(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->never())->method('isEligible');
$this->assertFalse($this->ruleChecker->isEligible($this->order, ['WEB_EU' => ['nth' => 5]]));
}
public function testShouldReturnFalseAndNotDelegateWhenOrderHasNoChannel(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn(null);
$this->decorated->expects($this->never())->method('isEligible');
$this->assertFalse($this->ruleChecker->isEligible($this->order, ['WEB_US' => ['nth' => 5]]));
}
public function testShouldReturnFalseAndNotDelegateWhenChannelConfigurationIsNotAnArray(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->decorated->expects($this->never())->method('isEligible');
$this->assertFalse($this->ruleChecker->isEligible($this->order, ['WEB_US' => 'not-an-array']));
}
public function testShouldThrowExceptionIfPromotionSubjectIsNotOrder(): void
{
$this->expectException(UnsupportedTypeException::class);
$this->ruleChecker->isEligible($this->createMock(PromotionSubjectInterface::class), []);
}
}

View file

@ -38,7 +38,7 @@
},
{
"propertyPath": "actions[5].type",
"message": "Promotion action type is invalid. Available action types are order_fixed_discount, unit_fixed_discount, order_percentage_discount, unit_percentage_discount, shipping_percentage_discount.",
"message": "Promotion action type is invalid. Available action types are order_fixed_discount, unit_fixed_discount, order_percentage_discount, order_percentage_discount_per_channel, unit_percentage_discount, shipping_percentage_discount, shipping_percentage_discount_per_channel.",
"code": null
}
],

View file

@ -53,7 +53,7 @@
},
{
"propertyPath": "rules[8].type",
"message": "Promotion rule type is invalid. Available rule types are customer_group, nth_order, shipping_country, has_taxon, total_of_items_from_taxon, contains_product, item_total, cart_quantity.",
"message": "Promotion rule type is invalid. Available rule types are customer_group, customer_group_per_channel, nth_order, nth_order_per_channel, shipping_country, shipping_country_per_channel, has_taxon, has_taxon_per_channel, total_of_items_from_taxon, contains_product, contains_product_per_channel, item_total, cart_quantity, cart_quantity_per_channel.",
"code": null
}
],