{{ form_errors(form) }}
-
- {{ form_row(form.code) }}
- {{ form_row(form.name) }}
- {{ form_row(form.channels) }}
+
+
+ {{ form_row(form.code) }}
+ {{ form_row(form.name) }}
+ {{ form_row(form.channels) }}
+
+
+ {{ form_row(form.rules) }}
+
+
+ {{ form_row(form.actions) }}
+
+
+
+ {{ translationForm(form.translations) }}
- {{ translationForm(form.translations) }}
diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/CatalogPromotion/theme.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/CatalogPromotion/theme.html.twig
new file mode 100644
index 00000000000..126165a77f1
--- /dev/null
+++ b/src/Sylius/Bundle/AdminBundle/Resources/views/CatalogPromotion/theme.html.twig
@@ -0,0 +1,6 @@
+{% extends '@SyliusAdmin/Form/theme.html.twig' %}
+
+{% block sylius_catalog_promotion_rule_widget %}
+ {{ form_row(form.type) }}
+ {{ form_row(form.configuration, {'remote_url': path('sylius_admin_ajax_all_product_variants_by_phrase'), 'remote_criteria_type': 'contains', 'remote_criteria_name': 'phrase', 'load_edit_url': path('sylius_admin_ajax_all_product_variants_by_codes')}) }}
+{% endblock %}
diff --git a/src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductVariantsToCodesTransformer.php b/src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductVariantsToCodesTransformer.php
new file mode 100644
index 00000000000..cc2f8f555ff
--- /dev/null
+++ b/src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductVariantsToCodesTransformer.php
@@ -0,0 +1,55 @@
+productVariantRepository = $productVariantRepository;
+ }
+
+ /** @throws \InvalidArgumentException */
+ public function transform($value): Collection
+ {
+ Assert::nullOrIsArray($value);
+
+ if (empty($value)) {
+ return new ArrayCollection();
+ }
+
+ return new ArrayCollection($this->productVariantRepository->findBy(['code' => $value]));
+ }
+
+ /** @throws \InvalidArgumentException */
+ public function reverseTransform($productVariants): array
+ {
+ Assert::isInstanceOf($productVariants, Collection::class);
+
+ return array_map(function (ProductVariantInterface $productVariant) {
+ return $productVariant->getCode();
+ }, $productVariants->toArray());
+ }
+}
diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/CatalogPromotionRuleTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/CatalogPromotionRuleTypeExtension.php
new file mode 100644
index 00000000000..75ef894cfb3
--- /dev/null
+++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/CatalogPromotionRuleTypeExtension.php
@@ -0,0 +1,40 @@
+productVariantsToCodesTransformer = $productVariantsToCodesTransformer;
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->add('configuration', ResourceAutocompleteChoiceType::class, [
+ 'label' => 'sylius.ui.variants',
+ 'multiple' => true,
+ 'required' => false,
+ 'choice_name' => 'descriptor',
+ 'choice_value' => 'code',
+ 'resource' => 'sylius.product_variant',
+ ]);
+
+ $builder->get('configuration')->addModelTransformer($this->productVariantsToCodesTransformer);
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [CatalogPromotionRuleType::class];
+ }
+}
diff --git a/src/Sylius/Bundle/CoreBundle/Provider/CatalogPromotionVariantsProvider.php b/src/Sylius/Bundle/CoreBundle/Provider/CatalogPromotionVariantsProvider.php
index e5fa01cba95..b43f4b846bd 100644
--- a/src/Sylius/Bundle/CoreBundle/Provider/CatalogPromotionVariantsProvider.php
+++ b/src/Sylius/Bundle/CoreBundle/Provider/CatalogPromotionVariantsProvider.php
@@ -54,11 +54,11 @@ private function getVariantsProducts(array $configuration, array $variants): arr
continue;
}
- if (!in_array($variant, $variants)) {
- $variants[] = $variant;
+ if (!isset($variants[$variant->getCode()])) {
+ $variants[$variant->getCode()] = $variant;
}
}
- return $variants;
+ return array_values($variants);
}
}
diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/app/sylius/sylius_promotion.yml b/src/Sylius/Bundle/CoreBundle/Resources/config/app/sylius/sylius_promotion.yml
index f614c0216d6..f7e073960a5 100644
--- a/src/Sylius/Bundle/CoreBundle/Resources/config/app/sylius/sylius_promotion.yml
+++ b/src/Sylius/Bundle/CoreBundle/Resources/config/app/sylius/sylius_promotion.yml
@@ -6,6 +6,9 @@ sylius_promotion:
catalog_promotion:
classes:
model: Sylius\Component\Core\Model\CatalogPromotion
+ catalog_promotion_rule:
+ classes:
+ model: Sylius\Component\Core\Model\CatalogPromotionRule
promotion_subject:
classes:
model: "%sylius.model.order.class%"
diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/CatalogPromotionRule.orm.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/CatalogPromotionRule.orm.xml
new file mode 100644
index 00000000000..7400eb91a01
--- /dev/null
+++ b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/CatalogPromotionRule.orm.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml
index ad0458a0a16..7bbc9e2a1b5 100644
--- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml
+++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml
@@ -78,6 +78,10 @@
+
+
+
+
@@ -190,6 +194,9 @@
+
+
+
@@ -261,5 +268,14 @@
+
+
+ %sylius.model.catalog_promotion_rule.class%
+ %sylius.form.type.catalog_promotion.validation_groups%
+
+ Sylius\Component\Core\Model\CatalogPromotionRuleInterface::TYPE_FOR_VARIANTS
+
+
+
diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml
index 0a8a4a05b21..cdb5980ea68 100644
--- a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml
+++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml
@@ -39,6 +39,8 @@ sylius:
coupon: Coupon
catalog_promotion:
channels: Channels
+ rule:
+ for_variants: For variants
checkout:
addressing:
different_billing_address: Use different address for billing?
diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CatalogPromotionRulesValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CatalogPromotionRulesValidator.php
index d5fc8762d53..4419403121f 100644
--- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CatalogPromotionRulesValidator.php
+++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CatalogPromotionRulesValidator.php
@@ -13,8 +13,8 @@
namespace Sylius\Bundle\CoreBundle\Validator\Constraints;
+use Sylius\Component\Core\Model\CatalogPromotionRuleInterface;
use Sylius\Component\Core\Repository\ProductVariantRepositoryInterface;
-use Sylius\Component\Promotion\Model\CatalogPromotionRuleInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
diff --git a/src/Sylius/Bundle/CoreBundle/spec/Provider/CatalogPromotionVariantsProviderSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Provider/CatalogPromotionVariantsProviderSpec.php
index 5806149b692..f003967e869 100644
--- a/src/Sylius/Bundle/CoreBundle/spec/Provider/CatalogPromotionVariantsProviderSpec.php
+++ b/src/Sylius/Bundle/CoreBundle/spec/Provider/CatalogPromotionVariantsProviderSpec.php
@@ -52,6 +52,9 @@ function it_provides_products_with_configured_variants(
$productVariantRepository->findOneBy(['code' => 'PHP_T_SHIRT_XS_BLACK'])->willReturn($secondVariant);
$productVariantRepository->findOneBy(['code' => 'PHP_MUG'])->willReturn(null);
+ $firstVariant->getCode()->willReturn('PHP_T_SHIRT_XS_WHITE');
+ $secondVariant->getCode()->willReturn('PHP_T_SHIRT_XS_BLACK');
+
$this
->provideEligibleVariants($catalogPromotion)
->shouldReturn([$firstVariant, $secondVariant])
diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CatalogPromotionRulesValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CatalogPromotionRulesValidatorSpec.php
index f02bdcfdc41..c30e15bed4d 100644
--- a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CatalogPromotionRulesValidatorSpec.php
+++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CatalogPromotionRulesValidatorSpec.php
@@ -15,13 +15,11 @@
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\CoreBundle\Validator\Constraints\CatalogPromotionRules;
+use Sylius\Component\Core\Model\CatalogPromotionRuleInterface;
use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Core\Repository\ProductVariantRepositoryInterface;
-use Sylius\Component\Promotion\Model\CatalogPromotionRuleInterface;
-use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
-use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
final class CatalogPromotionRulesValidatorSpec extends ObjectBehavior
{
diff --git a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductVariantRepository.php b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductVariantRepository.php
index f339dbfe675..31d341fe053 100644
--- a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductVariantRepository.php
+++ b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductVariantRepository.php
@@ -98,6 +98,16 @@ public function findByCodesAndProductCode(array $codes, string $productCode): ar
;
}
+ public function findByCodes(array $codes): array
+ {
+ return $this->createQueryBuilder('o')
+ ->andWhere('o.code IN (:codes)')
+ ->setParameter('codes', $codes)
+ ->getQuery()
+ ->getResult()
+ ;
+ }
+
public function findOneByIdAndProductId($id, $productId): ?ProductVariantInterface
{
return $this->createQueryBuilder('o')
@@ -129,4 +139,21 @@ public function findByPhraseAndProductCode(string $phrase, string $locale, strin
->getResult()
;
}
+
+ public function findByPhrase(string $phrase, string $locale): array
+ {
+ $expr = $this->getEntityManager()->getExpressionBuilder();
+
+ return $this->createQueryBuilder('o')
+ ->innerJoin('o.translations', 'translation', 'WITH', 'translation.locale = :locale')
+ ->andWhere($expr->orX(
+ 'translation.name LIKE :phrase',
+ 'o.code LIKE :phrase'
+ ))
+ ->setParameter('phrase', '%' . $phrase . '%')
+ ->setParameter('locale', $locale)
+ ->getQuery()
+ ->getResult()
+ ;
+ }
}
diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogAction/PercentageDiscountActionConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogAction/PercentageDiscountActionConfigurationType.php
new file mode 100644
index 00000000000..3ed92008a96
--- /dev/null
+++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogAction/PercentageDiscountActionConfigurationType.php
@@ -0,0 +1,26 @@
+add('amount', PercentType::class, [
+ 'label' => 'sylius.ui.amount',
+ ])
+ ;
+ }
+
+ public function getBlockPrefix(): string
+ {
+ return 'sylius_catalog_promotion_action_percentage_discount_configuration';
+ }
+}
diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionActionType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionActionType.php
new file mode 100644
index 00000000000..db7a9baf59d
--- /dev/null
+++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionActionType.php
@@ -0,0 +1,41 @@
+actionTypes = $actionTypes;
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('type', ChoiceType::class, [
+ 'label' => 'sylius.ui.type',
+ 'choices' => $this->actionTypes,
+ ])
+ ->add('configuration', PercentageDiscountActionConfigurationType::class, [
+ 'label' => false,
+ ])
+ ;
+ }
+
+ public function getBlockPrefix(): string
+ {
+ return 'sylius_catalog_promotion_action';
+ }
+
+}
diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionRuleType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionRuleType.php
new file mode 100644
index 00000000000..04ff25d333b
--- /dev/null
+++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionRuleType.php
@@ -0,0 +1,37 @@
+ruleTypes = $ruleTypes;
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('type', ChoiceType::class, [
+ 'label' => 'sylius.ui.type',
+ 'choices' => $this->ruleTypes,
+ ])
+ ;
+ }
+
+ public function getBlockPrefix(): string
+ {
+ return 'sylius_catalog_promotion_rule';
+ }
+
+}
diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionType.php
index c5b6d35a1d5..8664e6e44c7 100644
--- a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionType.php
+++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionType.php
@@ -16,6 +16,7 @@
use Sylius\Bundle\ResourceBundle\Form\EventSubscriber\AddCodeFormSubscriber;
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Sylius\Bundle\ResourceBundle\Form\Type\ResourceTranslationsType;
+use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -44,6 +45,20 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'entry_type' => $this->catalogPromotionTranslationType,
'label' => 'sylius.form.catalog_promotion.translations',
])
+ ->add('rules', CollectionType::class, [
+ 'label' => 'sylius.ui.rules',
+ 'entry_type' => CatalogPromotionRuleType::class,
+ 'allow_add' => true,
+ 'allow_delete' => true,
+ 'by_reference' => false,
+ ])
+ ->add('actions', CollectionType::class, [
+ 'label' => 'sylius.ui.actions',
+ 'entry_type' => CatalogPromotionActionType::class,
+ 'allow_add' => true,
+ 'allow_delete' => true,
+ 'by_reference' => false,
+ ])
;
}
diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml
index 3152012b9af..596e252fa39 100644
--- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml
+++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml
@@ -43,6 +43,21 @@
+
+ %sylius.model.catalog_promotion_rule.class%
+ %sylius.form.type.catalog_promotion.validation_groups%
+
+
+
+
+ %sylius.model.catalog_promotion_action.class%
+ %sylius.form.type.catalog_promotion.validation_groups%
+
+ Sylius\Component\Promotion\Model\CatalogPromotionActionInterface::TYPE_PERCENTAGE_DISCOUNT
+
+
+
+
%sylius.model.catalog_promotion_translation.class%
%sylius.form.type.catalog_promotion_translation.validation_groups%
diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.en.yml
index cd5086ee140..8bea551237f 100644
--- a/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.en.yml
+++ b/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.en.yml
@@ -3,6 +3,8 @@
sylius:
form:
catalog_promotion:
+ action:
+ percentage_discount: Percentage discount
description: Description
label: Label
name: Name
diff --git a/src/Sylius/Bundle/UiBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/UiBundle/Resources/translations/messages.en.yml
index bb97d53013e..cc9da012ab5 100644
--- a/src/Sylius/Bundle/UiBundle/Resources/translations/messages.en.yml
+++ b/src/Sylius/Bundle/UiBundle/Resources/translations/messages.en.yml
@@ -256,6 +256,7 @@ sylius:
edit_addresses: 'Edit addresses'
edit_admin_user: 'Edit administrator'
edit_association_type: 'Edit Association type'
+ edit_catalog_promotion: 'Edit catalog promotion'
edit_channel: 'Edit channel'
edit_country: 'Edit country'
edit_coupon: 'Edit coupon'
diff --git a/src/Sylius/Component/Core/Model/CatalogPromotionInterface.php b/src/Sylius/Component/Core/Model/CatalogPromotionInterface.php
index 27d0f503739..86f2ea58120 100644
--- a/src/Sylius/Component/Core/Model/CatalogPromotionInterface.php
+++ b/src/Sylius/Component/Core/Model/CatalogPromotionInterface.php
@@ -18,5 +18,4 @@
interface CatalogPromotionInterface extends BaseCatalogPromotionInterface, ChannelsAwareInterface
{
- public const TYPE_CONTAINS_VARIANTS = 'contains_variants';
}
diff --git a/src/Sylius/Component/Core/Model/CatalogPromotionRule.php b/src/Sylius/Component/Core/Model/CatalogPromotionRule.php
new file mode 100644
index 00000000000..4bf598994fe
--- /dev/null
+++ b/src/Sylius/Component/Core/Model/CatalogPromotionRule.php
@@ -0,0 +1,20 @@
+shouldImplement(CatalogPromotionRuleInterface::class);
+ }
+
+ function it_extends_base_catalog_promotion_rule(): void
+ {
+ $this->shouldHaveType(CatalogPromotionRule::class);
+ }
+}
diff --git a/src/Sylius/Component/Product/Repository/ProductVariantRepositoryInterface.php b/src/Sylius/Component/Product/Repository/ProductVariantRepositoryInterface.php
index 42ffc4061fc..db53795d0d6 100644
--- a/src/Sylius/Component/Product/Repository/ProductVariantRepositoryInterface.php
+++ b/src/Sylius/Component/Product/Repository/ProductVariantRepositoryInterface.php
@@ -43,10 +43,22 @@ public function findOneByCodeAndProductCode(string $code, string $productCode):
*/
public function findByCodesAndProductCode(array $codes, string $productCode): array;
+ /**
+ * @param array|string[] $codes
+ *
+ * @return array|ProductVariantInterface[]
+ */
+ public function findByCodes(array $codes): array;
+
public function findOneByIdAndProductId($id, $productId): ?ProductVariantInterface;
/**
* @return array|ProductVariantInterface[]
*/
public function findByPhraseAndProductCode(string $phrase, string $locale, string $productCode): array;
+
+ /**
+ * @return array|ProductVariantInterface[]
+ */
+ public function findByPhrase(string $phrase, string $locale): array;
}
diff --git a/src/Sylius/Component/Promotion/Model/CatalogPromotionRuleInterface.php b/src/Sylius/Component/Promotion/Model/CatalogPromotionRuleInterface.php
index 971f0ce05cf..4cd5b4f9662 100644
--- a/src/Sylius/Component/Promotion/Model/CatalogPromotionRuleInterface.php
+++ b/src/Sylius/Component/Promotion/Model/CatalogPromotionRuleInterface.php
@@ -17,8 +17,6 @@
interface CatalogPromotionRuleInterface extends ResourceInterface
{
- public const TYPE_FOR_VARIANTS = 'for_variants';
-
public function setType(?string $type): void;
public function getType(): ?string;
diff --git a/tests/Api/Admin/CatalogPromotionsTest.php b/tests/Api/Admin/CatalogPromotionsTest.php
index e3dc6f6fa54..794430cee55 100644
--- a/tests/Api/Admin/CatalogPromotionsTest.php
+++ b/tests/Api/Admin/CatalogPromotionsTest.php
@@ -13,8 +13,8 @@
namespace Sylius\Tests\Api\Admin;
-use Sylius\Component\Promotion\Model\CatalogPromotionRuleInterface;
use Sylius\Component\Core\Model\CatalogPromotionInterface;
+use Sylius\Component\Core\Model\CatalogPromotionRuleInterface;
use Sylius\Component\Promotion\Model\CatalogPromotionActionInterface;
use Sylius\Tests\Api\JsonApiTestCase;
use Sylius\Tests\Api\Utils\AdminUserLoginTrait;