Add per-channel configuration for promotion rules and actions

This commit is contained in:
Tomasz Kaliński 2026-06-23 11:39:16 +02:00
parent 96233aa29a
commit 6b327f4db8
18 changed files with 852 additions and 2 deletions

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;
@ -123,6 +132,42 @@ 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.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_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_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_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.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.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([
@ -173,6 +218,18 @@ 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.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.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>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\PromotionConfigurationChannelCodes">
<option name="groups">
<value>sylius</value>

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>
<constraint name="Sylius\Bundle\CoreBundle\Validator\Constraints\PromotionConfigurationChannelCodes">
<option name="groups">
<value>sylius</value>

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,57 @@
<?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);
}
$channelCode = $subject->getChannel()->getCode();
if (!isset($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);
}
$channelCode = $subject->getChannel()->getCode();
if (!isset($configuration[$channelCode])) {
return;
}
$this->decorated->revert($subject, $configuration[$channelCode], $promotion);
}
}

View file

@ -0,0 +1,45 @@
<?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);
}
$channelCode = $subject->getChannel()->getCode();
if (!isset($configuration[$channelCode])) {
return false;
}
return $this->decorated->isEligible($subject, $configuration[$channelCode]);
}
}

View file

@ -0,0 +1,110 @@
<?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 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 testShouldThrowExceptionWhenRevertingIfPromotionSubjectIsNotOrder(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->command->revert($this->createMock(PromotionSubjectInterface::class), [], $this->promotion);
}
}

View file

@ -0,0 +1,91 @@
<?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 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, unit_percentage_discount, shipping_percentage_discount, order_percentage_discount_per_channel, 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, nth_order, shipping_country, has_taxon, total_of_items_from_taxon, contains_product, item_total, cart_quantity, customer_group_per_channel, nth_order_per_channel, shipping_country_per_channel, has_taxon_per_channel, contains_product_per_channel, cart_quantity_per_channel.",
"code": null
}
],