diff --git a/features/promotion/managing_promotions/adding_promotion_with_filter.feature b/features/promotion/managing_promotions/adding_promotion_with_filter.feature index 8a490cd5e4..199f3e18d7 100644 --- a/features/promotion/managing_promotions/adding_promotion_with_filter.feature +++ b/features/promotion/managing_promotions/adding_promotion_with_filter.feature @@ -42,6 +42,18 @@ Feature: Adding promotion with filter Then I should be notified that it has been successfully created And the "$10 discount for all T-Shirts!" promotion should appear in the registry + @ui @javascript + Scenario: Adding a promotion with fixed discount for PHP T-Shirt + Given the store has a product "PHP T-Shirt" priced at "$100.00" + And I want to create a new promotion + When I specify its code as "10_for_php_t_shirt" + And I name it "$10 discount for PHP T-Shirts!" + And I add the "Item fixed discount" action configured with amount of "$10" + And I specify that this action should be applied to the "PHP T-Shirt" product + And I add it + Then I should be notified that it has been successfully created + And the "$10 discount for PHP T-Shirts!" promotion should appear in the registry + @ui @javascript Scenario: Adding a promotion with item percentage discount only for products over 10 Given I want to create a new promotion @@ -75,3 +87,15 @@ Feature: Adding promotion with filter And I add it Then I should be notified that it has been successfully created And the "$10 discount for all T-Shirts!" promotion should appear in the registry + + @ui @javascript + Scenario: Adding a promotion with 10% percentage discount for PHP T-Shirt + Given the store has a product "PHP T-Shirt" priced at "$100.00" + And I want to create a new promotion + When I specify its code as "10_for_php_t_shirt" + And I name it "10% discount for PHP T-Shirts!" + And I add the "Item percentage discount" action configured with a percentage value of 10% + And I specify that this action should be applied to the "PHP T-Shirt" product + And I add it + Then I should be notified that it has been successfully created + And the "10% discount for PHP T-Shirts!" promotion should appear in the registry diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php index 0f9445cfa5..7585a9a96e 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php @@ -554,6 +554,14 @@ final class ManagingPromotionsContext implements Context $this->createPage->selectRuleOption('Product', $productName); } + /** + * @When I specify that this action should be applied to the :productName product + */ + public function iSpecifyThatThisActionShouldBeAppliedToTheProduct($productName) + { + $this->createPage->selectFilterOption('Products', $productName); + } + /** * @param string $element * @param string $expectedMessage diff --git a/src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductsToCodesTransformer.php b/src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductsToCodesTransformer.php new file mode 100644 index 0000000000..15b7387503 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductsToCodesTransformer.php @@ -0,0 +1,77 @@ + + */ +final class ProductsToCodesTransformer implements DataTransformerInterface +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param ProductRepositoryInterface $productRepository + */ + public function __construct(ProductRepositoryInterface $productRepository) + { + $this->productRepository = $productRepository; + } + + /** + * {@inheritdoc} + */ + public function transform($value) + { + if (!is_array($value) && !is_null($value)) { + throw new UnexpectedTypeException($value, 'array'); + } + + if (empty($value)) { + return new ArrayCollection(); + } + + return new ArrayCollection($this->productRepository->findBy(['code' => $value])); + } + + /** + * {@inheritdoc} + */ + public function reverseTransform($products) + { + if (!$products instanceof Collection) { + throw new UnexpectedTypeException($products, Collection::class); + } + + if (null === $products) { + return []; + } + + $productCodes = []; + + /** @var ProductInterface $product */ + foreach ($products as $product) { + $productCodes[] = $product->getCode(); + } + + return $productCodes; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Filter/ProductFilterConfigurationType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Filter/ProductFilterConfigurationType.php new file mode 100644 index 0000000000..46491c170d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Filter/ProductFilterConfigurationType.php @@ -0,0 +1,71 @@ + + */ +class ProductFilterConfigurationType extends AbstractType +{ + /** + * @var ProductRepositoryInterface + */ + protected $productRepository; + + /** + * @var DataTransformerInterface + */ + private $productsToCodesTransformer; + + /** + * @param ProductRepositoryInterface $productRepository + * @param DataTransformerInterface $productsToCodesTransformer + */ + public function __construct( + ProductRepositoryInterface $productRepository, + DataTransformerInterface $productsToCodesTransformer + ) { + $this->productRepository = $productRepository; + $this->productsToCodesTransformer = $productsToCodesTransformer; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('products', 'sylius_product_choice', [ + 'label' => 'sylius.form.promotion_rule.product.products', + 'multiple' => true, + 'required' => false, + ]) + ; + + $builder->get('products')->addModelTransformer($this->productsToCodesTransformer); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'sylius_promotion_action_filter_product_configuration'; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/PromotionFiltersType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/PromotionFiltersType.php index 4ce4fc8f35..0fb03970bc 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/PromotionFiltersType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/PromotionFiltersType.php @@ -27,5 +27,6 @@ class PromotionFiltersType extends BasePromotionFiltersType parent::buildForm($builder, $options); $builder->add('taxons_filter', 'sylius_promotion_action_filter_taxon_configuration', ['required' => false]); + $builder->add('products_filter', 'sylius_promotion_action_filter_product_configuration', ['required' => false]); } } diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml index 757ee72b71..50201c5cda 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml @@ -113,6 +113,11 @@ + + + + + @@ -130,6 +135,9 @@ + + + %sylius.model.customer.class% diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml index d4e5a53925..a73b4faae7 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml @@ -125,6 +125,8 @@ sylius: count: Count taxon: taxons: Taxons + product: + products: Products total_of_items_from_taxon: taxon: Taxon amount: Amount diff --git a/src/Sylius/Bundle/CoreBundle/spec/Form/DataTransformer/ProductsToCodesTransformerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Form/DataTransformer/ProductsToCodesTransformerSpec.php new file mode 100644 index 0000000000..b5013b8ea5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Form/DataTransformer/ProductsToCodesTransformerSpec.php @@ -0,0 +1,130 @@ + + */ +final class ProductsToCodesTransformerSpec extends ObjectBehavior +{ + function let(ProductRepositoryInterface $productRepository) + { + $this->beConstructedWith($productRepository); + } + + function it_is_initializable() + { + $this->shouldHaveType(ProductsToCodesTransformer::class); + } + + function it_implements_data_transformer_interface() + { + $this->shouldImplement(DataTransformerInterface::class); + } + + function it_transforms_array_of_products_codes_to_products_collection( + ProductRepositoryInterface $productRepository, + ProductInterface $bow, + ProductInterface $sword + ) { + $productRepository->findBy(['code' => ['bow', 'sword']])->willReturn([$bow, $sword]); + + $products = new ArrayCollection([$bow->getWrappedObject(), $sword->getWrappedObject()]); + + $this->transform(['bow', 'sword'])->shouldBeCollection($products); + } + + function it_transforms_only_existing_products( + ProductRepositoryInterface $productRepository, + ProductInterface $bow + ) { + $productRepository->findBy(['code' => ['bow', 'sword']])->willReturn([$bow]); + + $products = new ArrayCollection([$bow->getWrappedObject()]); + + $this->transform(['bow', 'sword'])->shouldBeCollection($products); + } + + function it_transforms_empty_array_into_empty_collection() + { + $this->transform([])->shouldBeCollection(new ArrayCollection([])); + } + + function it_throws_exception_if_value_to_transform_is_not_array() + { + $this + ->shouldThrow(new UnexpectedTypeException('badObject', 'array')) + ->during('transform', ['badObject']) + ; + } + + function it_reverse_transforms_into_array_of_products_codes( + ProductInterface $axes, + ProductInterface $shields + ) { + $axes->getCode()->willReturn('axes'); + $shields->getCode()->willReturn('shields'); + + $this + ->reverseTransform(new ArrayCollection([$axes->getWrappedObject(), $shields->getWrappedObject()])) + ->shouldReturn(['axes', 'shields']) + ; + } + + function it_throws_exception_if_reverse_transformed_object_is_not_collection() + { + $this + ->shouldThrow(new UnexpectedTypeException('badObject', Collection::class)) + ->during('reverseTransform', ['badObject']) + ; + } + + function it_returns_empty_array_if_passed_collection_is_empty() + { + $this->reverseTransform(new ArrayCollection())->shouldReturn([]); + } + + /** + * {@inheritdoc} + */ + public function getMatchers() + { + return [ + 'beCollection' => function ($subject, $key) { + if (!$subject instanceof Collection || !$key instanceof Collection) { + return false; + } + + if ($subject->count() !== $key->count()) { + return false; + } + + foreach ($subject as $subjectElement) { + if (!$key->contains($subjectElement)) { + return false; + } + } + + return true; + }, + ]; + } +} diff --git a/src/Sylius/Component/Core/Promotion/Filter/TaxonFilter.php b/src/Sylius/Component/Core/Promotion/Filter/TaxonFilter.php index 8d68460178..68f21e4ebe 100644 --- a/src/Sylius/Component/Core/Promotion/Filter/TaxonFilter.php +++ b/src/Sylius/Component/Core/Promotion/Filter/TaxonFilter.php @@ -23,7 +23,7 @@ final class TaxonFilter implements FilterInterface */ public function filter(array $items, array $configuration) { - if (!isset($configuration['filters']['taxons_filter'])) { + if (!isset($configuration['filters']['taxons_filter']) || empty($configuration['filters']['taxons_filter']['taxons'])) { return $items; }