mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-05 20:57:12 +00:00
Add channel-based eligibility and application for promotions
This commit is contained in:
parent
340fce0744
commit
58a7b1e566
10 changed files with 323 additions and 0 deletions
|
|
@ -14,15 +14,62 @@ declare(strict_types=1);
|
|||
namespace Sylius\Bundle\AdminBundle\Form\Extension\Promotion;
|
||||
|
||||
use Sylius\Bundle\PromotionBundle\Form\Type\PromotionActionType;
|
||||
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
|
||||
use Sylius\Component\Core\Promotion\ChannelAwareConfigurationInterface;
|
||||
use Sylius\Component\Promotion\Model\PromotionActionInterface;
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
final class PromotionActionTypeExtension extends AbstractTypeExtension
|
||||
{
|
||||
public function __construct(private ChannelRepositoryInterface $channelRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('type', HiddenType::class);
|
||||
|
||||
$choices = [];
|
||||
foreach ($this->channelRepository->findAll() as $channel) {
|
||||
$choices[$channel->getName()] = $channel->getCode();
|
||||
}
|
||||
$availableCodes = array_values($choices);
|
||||
|
||||
$builder->add(ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY, ChoiceType::class, [
|
||||
'label' => 'sylius.form.promotion_action.channels',
|
||||
'choices' => $choices,
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choice_translation_domain' => false,
|
||||
'mapped' => false,
|
||||
]);
|
||||
|
||||
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) use ($availableCodes): void {
|
||||
$data = $event->getData();
|
||||
if (!$data instanceof PromotionActionInterface) {
|
||||
return;
|
||||
}
|
||||
$storedCodes = $data->getConfiguration()[ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY] ?? [];
|
||||
$event->getForm()->get(ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY)
|
||||
->setData(array_values(array_intersect($storedCodes, $availableCodes)));
|
||||
});
|
||||
|
||||
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void {
|
||||
$data = $event->getData();
|
||||
if (!$data instanceof PromotionActionInterface) {
|
||||
return;
|
||||
}
|
||||
$configuration = $data->getConfiguration();
|
||||
$configuration[ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY] =
|
||||
$event->getForm()->get(ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY)->getData() ?? [];
|
||||
$data->setConfiguration($configuration);
|
||||
});
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
|
|
|
|||
|
|
@ -14,15 +14,62 @@ declare(strict_types=1);
|
|||
namespace Sylius\Bundle\AdminBundle\Form\Extension\Promotion;
|
||||
|
||||
use Sylius\Bundle\PromotionBundle\Form\Type\PromotionRuleType;
|
||||
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
|
||||
use Sylius\Component\Core\Promotion\ChannelAwareConfigurationInterface;
|
||||
use Sylius\Component\Promotion\Model\PromotionRuleInterface;
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
final class PromotionRuleTypeExtension extends AbstractTypeExtension
|
||||
{
|
||||
public function __construct(private ChannelRepositoryInterface $channelRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('type', HiddenType::class);
|
||||
|
||||
$choices = [];
|
||||
foreach ($this->channelRepository->findAll() as $channel) {
|
||||
$choices[$channel->getName()] = $channel->getCode();
|
||||
}
|
||||
$availableCodes = array_values($choices);
|
||||
|
||||
$builder->add(ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY, ChoiceType::class, [
|
||||
'label' => 'sylius.form.promotion_rule.channels',
|
||||
'choices' => $choices,
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choice_translation_domain' => false,
|
||||
'mapped' => false,
|
||||
]);
|
||||
|
||||
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) use ($availableCodes): void {
|
||||
$data = $event->getData();
|
||||
if (!$data instanceof PromotionRuleInterface) {
|
||||
return;
|
||||
}
|
||||
$storedCodes = $data->getConfiguration()[ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY] ?? [];
|
||||
$event->getForm()->get(ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY)
|
||||
->setData(array_values(array_intersect($storedCodes, $availableCodes)));
|
||||
});
|
||||
|
||||
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void {
|
||||
$data = $event->getData();
|
||||
if (!$data instanceof PromotionRuleInterface) {
|
||||
return;
|
||||
}
|
||||
$configuration = $data->getConfiguration();
|
||||
$configuration[ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY] =
|
||||
$event->getForm()->get(ChannelAwareConfigurationInterface::CHANNELS_CONFIGURATION_KEY)->getData() ?? [];
|
||||
$data->setConfiguration($configuration);
|
||||
});
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ return static function (ContainerConfigurator $container) {
|
|||
|
||||
$services
|
||||
->set('sylius_admin.form.extension.type.promotion.promotion_action', PromotionActionTypeExtension::class)
|
||||
->args([service('sylius.repository.channel')])
|
||||
->tag('form.type_extension')
|
||||
;
|
||||
|
||||
|
|
@ -130,6 +131,7 @@ return static function (ContainerConfigurator $container) {
|
|||
|
||||
$services
|
||||
->set('sylius_admin.form.extension.type.promotion.promotion_rule', PromotionRuleTypeExtension::class)
|
||||
->args([service('sylius.repository.channel')])
|
||||
->tag('form.type_extension')
|
||||
;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
</div>
|
||||
<div class="flex-grow-1">
|
||||
{{- form_row(form.configuration, {'label': false}) -}}
|
||||
{{- form_row(form._channels) -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -35,6 +36,7 @@
|
|||
</div>
|
||||
<div class="flex-grow-1">
|
||||
{{- form_row(form.configuration, {'label': false}) -}}
|
||||
{{- form_row(form._channels) -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?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\DependencyInjection\Compiler;
|
||||
|
||||
use Sylius\Component\Core\Promotion\Action\ChannelAwarePromotionApplicator;
|
||||
use Sylius\Component\Core\Promotion\Checker\Eligibility\ChannelAwarePromotionRulesEligibilityChecker;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
final class OverridePromotionServicesPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if ($container->hasDefinition('sylius.checker.promotion.rules_eligibility')) {
|
||||
$container->getDefinition('sylius.checker.promotion.rules_eligibility')
|
||||
->setClass(ChannelAwarePromotionRulesEligibilityChecker::class);
|
||||
}
|
||||
|
||||
if ($container->hasDefinition('sylius.action.applicator.promotion')) {
|
||||
$container->getDefinition('sylius.action.applicator.promotion')
|
||||
->setClass(ChannelAwarePromotionApplicator::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,6 +75,7 @@ sylius:
|
|||
promotion_action:
|
||||
add_product_configuration:
|
||||
product: Product
|
||||
channels: Applicable channels
|
||||
promotion_coupon:
|
||||
per_customer_usage_limit: Per-Customer Usage Limit
|
||||
reusable_from_cancelled_orders: Reusable from cancelled orders
|
||||
|
|
@ -198,6 +199,7 @@ sylius:
|
|||
products: Products filter
|
||||
taxons: Taxons filter
|
||||
promotion_rule:
|
||||
channels: Applicable channels
|
||||
customer_group:
|
||||
group: Customer group
|
||||
has_taxon:
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\CheckStatisticsOrdersT
|
|||
use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\CircularDependencyBreakingErrorListenerPass;
|
||||
use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\IgnoreAnnotationsPass;
|
||||
use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\LazyCacheWarmupPass;
|
||||
use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\OverridePromotionServicesPass;
|
||||
use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\OverrideResourceControllerStateMachinePass;
|
||||
use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\RegisterTaxCalculationStrategiesPass;
|
||||
use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\RegisterUriBasedSectionResolverPass;
|
||||
|
|
@ -93,6 +94,7 @@ final class SyliusCoreBundle extends AbstractResourceBundle
|
|||
$container->addCompilerPass(new Symfony6PrivateServicesPass());
|
||||
$container->addCompilerPass(new TranslatableEntityLocalePass());
|
||||
$container->addCompilerPass(new CheckStatisticsOrdersTotalsProviderTypePass());
|
||||
$container->addCompilerPass(new OverridePromotionServicesPass());
|
||||
$container->addCompilerPass(new OverrideResourceControllerStateMachinePass(), priority: -1024);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
<?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\Core\Promotion\ChannelAwareConfigurationInterface;
|
||||
use Sylius\Component\Promotion\Action\PromotionActionCommandInterface;
|
||||
use Sylius\Component\Promotion\Action\PromotionApplicatorInterface;
|
||||
use Sylius\Component\Promotion\Model\PromotionActionInterface;
|
||||
use Sylius\Component\Promotion\Model\PromotionInterface;
|
||||
use Sylius\Component\Promotion\Model\PromotionSubjectInterface;
|
||||
use Sylius\Component\Registry\ServiceRegistryInterface;
|
||||
|
||||
final class ChannelAwarePromotionApplicator implements PromotionApplicatorInterface, ChannelAwareConfigurationInterface
|
||||
{
|
||||
public function __construct(private ServiceRegistryInterface $registry)
|
||||
{
|
||||
}
|
||||
|
||||
public function apply(PromotionSubjectInterface $subject, PromotionInterface $promotion): void
|
||||
{
|
||||
$applyPromotion = false;
|
||||
foreach ($promotion->getActions() as $action) {
|
||||
if ($this->isActionSkippedForChannel($action, $subject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$configuration = $this->stripChannelKey($action->getConfiguration());
|
||||
$result = $this->getActionCommandByType($action->getType())->execute($subject, $configuration, $promotion);
|
||||
$applyPromotion = $applyPromotion || $result;
|
||||
}
|
||||
|
||||
if ($applyPromotion) {
|
||||
$subject->addPromotion($promotion);
|
||||
}
|
||||
}
|
||||
|
||||
public function revert(PromotionSubjectInterface $subject, PromotionInterface $promotion): void
|
||||
{
|
||||
foreach ($promotion->getActions() as $action) {
|
||||
if ($this->isActionSkippedForChannel($action, $subject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$configuration = $this->stripChannelKey($action->getConfiguration());
|
||||
$this->getActionCommandByType($action->getType())->revert($subject, $configuration, $promotion);
|
||||
}
|
||||
|
||||
$subject->removePromotion($promotion);
|
||||
}
|
||||
|
||||
private function isActionSkippedForChannel(PromotionActionInterface $action, PromotionSubjectInterface $subject): bool
|
||||
{
|
||||
$channels = $action->getConfiguration()[self::CHANNELS_CONFIGURATION_KEY] ?? [];
|
||||
if ($channels === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof OrderInterface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !in_array($subject->getChannel()->getCode(), $channels, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $configuration
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function stripChannelKey(array $configuration): array
|
||||
{
|
||||
unset($configuration[self::CHANNELS_CONFIGURATION_KEY]);
|
||||
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
private function getActionCommandByType(string $type): PromotionActionCommandInterface
|
||||
{
|
||||
return $this->registry->get($type);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?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;
|
||||
|
||||
interface ChannelAwareConfigurationInterface
|
||||
{
|
||||
public const CHANNELS_CONFIGURATION_KEY = '_channels';
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<?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\Eligibility;
|
||||
|
||||
use Sylius\Component\Core\Model\OrderInterface;
|
||||
use Sylius\Component\Core\Promotion\ChannelAwareConfigurationInterface;
|
||||
use Sylius\Component\Promotion\Checker\Eligibility\PromotionEligibilityCheckerInterface;
|
||||
use Sylius\Component\Promotion\Checker\Rule\RuleCheckerInterface;
|
||||
use Sylius\Component\Promotion\Model\PromotionInterface;
|
||||
use Sylius\Component\Promotion\Model\PromotionRuleInterface;
|
||||
use Sylius\Component\Promotion\Model\PromotionSubjectInterface;
|
||||
use Sylius\Component\Registry\ServiceRegistryInterface;
|
||||
|
||||
final class ChannelAwarePromotionRulesEligibilityChecker implements PromotionEligibilityCheckerInterface, ChannelAwareConfigurationInterface
|
||||
{
|
||||
public function __construct(private ServiceRegistryInterface $ruleRegistry)
|
||||
{
|
||||
}
|
||||
|
||||
public function isEligible(PromotionSubjectInterface $promotionSubject, PromotionInterface $promotion): bool
|
||||
{
|
||||
if (!$promotion->hasRules()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($promotion->getRules() as $rule) {
|
||||
if ($this->isRuleSkippedForChannel($rule, $promotionSubject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->isEligibleToRule($promotionSubject, $rule)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function isRuleSkippedForChannel(PromotionRuleInterface $rule, PromotionSubjectInterface $subject): bool
|
||||
{
|
||||
$channels = $rule->getConfiguration()[self::CHANNELS_CONFIGURATION_KEY] ?? [];
|
||||
if ($channels === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof OrderInterface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !in_array($subject->getChannel()->getCode(), $channels, true);
|
||||
}
|
||||
|
||||
private function isEligibleToRule(PromotionSubjectInterface $subject, PromotionRuleInterface $rule): bool
|
||||
{
|
||||
/** @var RuleCheckerInterface $checker */
|
||||
$checker = $this->ruleRegistry->get($rule->getType());
|
||||
|
||||
$configuration = $rule->getConfiguration();
|
||||
unset($configuration[self::CHANNELS_CONFIGURATION_KEY]);
|
||||
|
||||
return $checker->isEligible($subject, $configuration);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue